← Back

ofxInkStrokeModeler

github.com/n1ckfg/ofxInkStrokeModeler · @n1ckfg
OF 0.12 Graphics

Port of Google's Ink Stroke Modeler that turns noisy mouse, touch or stylus input into smooth, predicted stroke curves.

0 0 Last commit 23d ago Created 24d ago

README

ofxInkStrokeModeler



Smooth, low-latency freehand strokes from mouse, touch or stylus input in openFrameworks, tested with oF 0.12.1.

A port of Google's Ink Stroke Modeler. It turns noisy pointer input into smooth curves and predicts where the stroke is heading, so a line can keep up with the pen. The pen tip is modeled as a mass pulled along the input by a spring. Output is resampled to at least 180 points per second, each with a position, velocity, acceleration and (interpolated) pressure, tilt and orientation.

The library has no dependencies beyond the C++17 standard library. Upstream needs C++20 and Abseil; see REPORT.md for what changed and how the port was verified against the original.

Usage



#include "ofxInkStrokeModeler.h"

ofxInkStrokeModeler stroke;

void ofApp::setup() {
    stroke.setup(); // optional; defaults to the stroke-end predictor
}

void ofApp::mousePressed(int x, int y, int button) {
    stroke.begin(x, y);
}

void ofApp::mouseDragged(int x, int y, int button) {
    stroke.addPoint(x, y);
}

void ofApp::mouseReleased(int x, int y, int button) {
    stroke.end(x, y);
}

void ofApp::draw() {
    ofSetColor(0);
    stroke.draw(4); // 4 px wide, including the prediction
}


Inputs are timestamped with ofGetElapsedTimeMicros(). begin() starts a new stroke and discards the previous one, so keep what you need when a stroke ends:

if (stroke.end(x, y)) {
    strokes.push_back(stroke.getMesh(4, false));
}


Results



getResults() is the smoothed stroke so far, as ink::stroke_model::Results: position, velocity, acceleration, time, pressure, tilt, orientation. Once generated, a result never changes. getPrediction() continues from the last result towards the pointer. It is only valid until the next input, and is empty when no stroke is in progress. Draw it in the stroke's color and the stroke appears to keep up with the pen. getPolyline(), getMesh(width), draw(width) include the prediction by default; pass false to leave it out. getMesh() builds a filled outline with round caps. With a number, the width is scaled by pressure where the input reports it. You can also pass your own function, e.g. thinner where the pen moves fast:

ofMesh mesh = stroke.getMesh([](const ofxInkStrokeModeler::Result & r) {
    float speed = glm::length(ofxInkStrokeModeler::toGlm(r.velocity));
    return ofMap(speed, 0, 4000, 12, 3, true); // pixels per second -> width
});


The static toPolyline() and toMesh() do the same for any vector of results.

Functions return false and log a warning when the modeler rejects an input, e.g. addPoint() with no stroke in progress; getLastError() has the message.

Prediction



stroke.setup(ofxInkStrokeModeler::Predictor::Kalman);


StrokeEnd (default): models the stroke as if the pen lifted at the latest input, so the stroke catches up with it but doesn't go past it. Kalman: estimates the pen's velocity, acceleration and jerk, and also extends the stroke a little past the latest input, by up to 1/60 s. Disabled: no prediction.

Parameters



getDefaultParams() returns the values upstream's tests use, converted from cm to pixels. They assume a 96 dpi screen. On a denser screen, or if your coordinates are in other units, pass the number of units per centimeter:

stroke.setup(ofxInkStrokeModeler::Predictor::Kalman, 220 / 2.54); // 220 dpi


Or change any of the parameters (documented in params.h):

auto params = ofxInkStrokeModeler::getDefaultParams();
params.wobble_smoother_params.is_enabled = false;
params.sampling_params.min_output_rate = 120;
stroke.setup(params);


Stylus and touch



Pass pressure (0 to 1) to begin(), addPoint() and end(). For tilt, orientation or your own timestamps (in seconds), send full inputs:

using Input = ofxInkStrokeModeler::Input;
stroke.addInput(ofxInkStrokeModeler::makeInput(Input::EventType::kMove,
    position, timeInSeconds, pressure, tilt, orientation));


Negative values mean "unknown". Each modeler handles one stroke at a time, so for multi-touch, use one ofxInkStrokeModeler per touch id.

The library itself



The upstream API is available directly in namespace ink::stroke_model, through getModeler() or on its own:

#include "ink_stroke_modeler/stroke_modeler.h"

ink::stroke_model::StrokeModeler modeler;
ink::stroke_model::Status status = modeler.Reset(params);
if (!status.ok()) ofLogError() << status.ToString();


It works as upstream's README describes. The one difference is that absl::Status and absl::StatusOr are ink::stroke_model::Status and StatusOr.

Example



example-basic: draw with the mouse. It shows the raw input, the smoothed stroke, and the prediction in red, and lets you switch predictors.

Tests



tests/upstream-diff/run.sh /path/to/ink-stroke-modeler builds a harness against both upstream and this port and checks that their output is identical. ofxInkStrokeModeler class: make && make RunRelease.

License



Apache License 2.0, like Ink Stroke Modeler; see LICENSE.txt.

Get this addon

HTTPS
SSH