diff --git a/systems/README.md b/systems/README.md index 619cb47..7239944 100644 --- a/systems/README.md +++ b/systems/README.md @@ -38,3 +38,6 @@ misbehaving, it belongs in `operations/`. - [interactive-data-interfaces](./interactive-data-interfaces.md) — spatial budgets, selection semantics, causal motion, stable updates and verification boundaries +- [simulated-pressure-ink](./simulated-pressure-ink.md) — streaming + perfect-freehand: speed relative to brush size, speed-scaled easing, + time-normalized; stamp circles so the renderer cannot gap diff --git a/systems/simulated-pressure-ink.md b/systems/simulated-pressure-ink.md new file mode 100644 index 0000000..2140fad --- /dev/null +++ b/systems/simulated-pressure-ink.md @@ -0,0 +1,50 @@ +# simulated pressure ink + +A finger or mouse reports no pressure, but a line whose width never changes +reads as a tool, not a hand. Perfect-freehand's answer is to simulate pressure +from speed, and the version that holds up is the streaming one: each sample +updates a small state, nothing looks at the whole stroke, and the stored points +already carry their width so replay and peers need no model at all. + +## the model + +Three passes per sample, all easing (exponential moving averages): + +- **streamline** the position toward the raw sample. Jittery input becomes a + fluid line. The final sample skips the easing so the stroke ends under the + finger instead of a fraction short of it. +- **speed** is distance per nominal sample interval *divided by the brush + size*. A fat brush needs a faster hand to thin. Measuring speed in absolute + px/ms was the mistake that made the earlier attempt feel wrong: one constant + cannot serve a 6 px pen and a 200 px marker. +- **pressure** eases toward `1 - speed` at a rate that itself scales with + speed. A flick thins at once; slowing down thickens gradually. Start at + quarter pressure so a stroke swells in. + +Radius is `size * (0.5 - thinning * (0.5 - pressure))`: nominal pressure paints +exactly `size`, and thinning 0.55 spans 0.45× to 1.55× of it. Thinning 1 lets a +flick reach zero width, so a user-facing knob should stop short of it. + +Normalize both easings to wall-clock time (`1 - (1 - α)^(dt / nominal)`), or a +60 Hz mouse and 120 Hz coalesced touch draw different lines for the same hand +motion. + +## rendering is a separate problem + +Perfect-freehand's whole-stroke outline self-intersects at sharp corners on +sparse input and leaves V-shaped gaps. Do not fix that in the model. Stamp a +circle at every point and a trapezoid between neighbours: every joint is a +full disc, so the stroke cannot gap, and the model only has to produce a +position and a radius. Densify long segments at render time so sparse samples +do not read as beads. + +## sources + +- tinydraw `core/src/ink_stream.cpp`, `core/include/tinydraw/ink_config.h` + (github.com/aliceisjustplaying/tinydraw), read 2026-09-19. A C++ streaming + port of perfect-freehand for an ESP32 finger-drawing device. +- doodl `src/ui/ink.ts`, `src/ui/draw.ts`, commit `55973eb`, 2026-09-19. The + earlier whole-stroke attempt and its corner-gap fix are commits `68db9e8` + and `1474776`, reverted in `3e73da0`; the absolute-speed attempt is `1ab4d35`. +- perfect-freehand `packages/perfect-freehand/src/getStrokePoints.ts`, + `getStrokeOutlinePoints.ts` (github.com/steveruizok/perfect-freehand).