Port of Google's Ink Stroke Modeler that turns noisy mouse, touch or stylus input into smooth, predicted stroke curves.
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 dpiOr 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. tests/wrapperis a headless openFrameworks app that checks the
ofxInkStrokeModeler class: make && make RunRelease.License
Apache License 2.0, like Ink Stroke Modeler; see LICENSE.txt.
ofxAddons