Skip to content

feat: draw the strokes of a pen as wide as the pressure on it - #218

Open
tbolis wants to merge 1 commit into
release/1.2.0from
feature/pen-pressure
Open

tbolis wants to merge 1 commit into
release/1.2.0from
feature/pen-pressure

Conversation

@tbolis

@tbolis tbolis commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Refs #136 (not closed by this: the issue can be answered once it is released).

Change

The issue asks for the pressure of a pen (PointerEvent.pressure) to drive the thickness of the line.

  • New property pressureSensitive (default false). With it, the strokes of the pencil which a pen (a stylus) draws are as wide as the pressure on it: lineWidth is the width at a medium pressure, and a stroke is between a tenth and twice of it. A pen which reports a constant 0.5 draws a line of lineWidth.
  • The strokes of a mouse and of a finger are drawn as before: they have no pressure. The highlighter does not have it.
  • Reading the pressure. The canvas keeps reading the mouse and the touches (fabric's default, and the pinch zoom is made of touch events), so it is not switched to pointer events. A PenSensor listens to the pointerdown and pointermove events of the page, which come before the mouse and touch events of the same input, to know the type of the pointer and its pressure. It listens only while the property is on, and stops when the canvas goes away.
  • The brush. PressureBrush extends fabric's PencilBrush and replaces it as the brush of the free drawing tools. Without a pen stroke it is the same brush. A pen stroke is drawn on the top canvas piece by piece while the pen moves (all at once for a color which is not opaque), and is the same shape when it is lifted.
  • The stroke. The line of the stroke is the smooth one the brush of fabric draws (through the middle of each two points), made into discs which are as wide as the pressure there, with the area which joins each two. Every piece is drawn the same way round, so the union is filled with no hole where they overlap. The pressure is smoothed, so the noise of a pen does not make the width jump.
  • PressureStroke. The stroke is a fabric.Path subclass, so it is selected, moved, scaled, erased and copied as any path, and it is one undo step. It is saved as its discs ([x, y, r]) and not as the commands of the path: a stroke of 150 points was 166 KB of path commands against 8 KB for a stroke of the pencil, and is a few kilobytes as discs. The path is made again when it is loaded by value, defaultValue or fromJSON().
  • Showcase: a Pen pressure switch for the pencil (on by default there).
  • README (a "Pen pressure" section and the properties table), the types.ts documentation, and the CHANGELOG.

Verification

  • 43 new tests, 563 in all, passing on React 19 and on React 18. Format, lint, typecheck and yarn build pass.
    • geometry.spec.ts (18) renders with node-canvas, so it reads real pixels: the width at each end and between, the six directions including a narrower and a wider end, a disc inside another, a sharp turn of a wide stroke with no hole, and the sampler (the pressure along the stroke, the end at the last point, the smoothing, the distance between discs).
    • PressureBrush.spec.tsx (25) drives the brush with pen pointer events: the width for pressures 0.25, 0.5 and 1, the limits, a stroke which gets wider, a tap, the place of the stroke, a color which is not opaque, one undo step and path:created, save and load, the size of the saved stroke, copy, move with undo, the stroke stopped by the canvas (a second finger), points which are too close, a mouse and a touch, the property off or turned off, the highlighter, and the listeners (not added when the property is off, removed on unmount and when it is turned off).
    • Four deliberate breakages of the brush and the highlighter (every pointer as a pen, no listening, the highlighter not turning it off, the pressure ignored) each fail some of them.
  • In headless Chrome on the built showcase, with the pen input of the DevTools protocol (pointerType: pen and force), at a line weight of 20: a stroke with force from 0.1 to 1 is 5, 21 and 38 px wide at its start, middle and end; a pen at 0.5 is 20 px; a mouse stroke is 20 px as before; with the switch off a pen stroke is 20 px at any pressure. Undo and redo bring the strokes back as wide as before, and there were no page errors.

Notes

  • Not tried on a physical stylus, on an iPad (Apple Pencil in Safari) or on an Android pen: only the synthetic pen events of Chrome. It relies on the browser sending the pointer event before the mouse or touch event of the same input, which the Pointer Events specification says it does.
  • A PressureStroke is a new type in toJSON(), which a version without this cannot load.
  • lineStyle does not apply to a pen stroke (it has no dashes), and neither does the straight line of the shift key.
  • The path of a PressureStroke in memory is long (a stroke of 150 points is about 2600 commands), as the capsules are many small shapes. The canvas caches objects, so it is drawn from the cache.
  • feat: group and ungroup the selected objects #217 also changes CHANGELOG.md and SketchField.tsx, so the second of the two to merge needs a conflict resolution.

The pressureSensitive property makes the strokes of the pencil which a pen draws as
wide as the pressure on it, read from PointerEvent.pressure: lineWidth is the width at a
medium pressure, and a stroke is between a tenth and twice of it. The strokes of a mouse
and of a finger are drawn as before.

The canvas keeps reading the mouse and the touches (the pinch zoom is made of them). A
sensor listens to the pointer events of the page, which come before them, to know the
type of the pointer and its pressure, and only while the property is on.

A stroke is a filled path, the outline of the discs along the stroke, so that the canvas
selects, moves, erases and copies it as any path. It is the type PressureStroke, and it
is saved as the discs: the commands of the path are twenty times as long. The showcase
has a Pen pressure switch for the pencil.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant