Skip to content

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 12 Sep 06:41
· 33 commits to main since this release
441f515

The strokes look right now, and there is finally a way to prove it rather than judge it.

Pen positions were being quantized to whole pixels

The complaint that started this: strokes that were not obviously broken, just not as good as they should be. Small bumps along a slow curve, visible at 3–4× zoom, absent from Clip Studio Paint and Krita drawing the same line.

The cause was one line. Converting the pen's desktop position to a canvas position went through TopLevel.PointToClient, which takes a PixelPoint — whose members are integers. Every pen position was being forced onto the whole-pixel grid on the way in.

A digitizer reports much finer than a screen pixel, and that precision is the whole reason to open a digitizer context. At the ~2px steps a tablet reports, an integer grid leaves only a handful of directions a segment can point in, so the path stops following the pen and starts zigzagging. Measured: the median turn between consecutive segments went from about 1.5° to 11.3°.

The conversion is done by hand now. The window origin still goes through the integer API — it is genuinely on a pixel boundary, so nothing is lost — and only the pen's own position keeps its fractional part.

Two supporting changes:

  • Wintab's digitizer context is now the default where available, rather than the system context. The system context is quantized by the driver before the application ever sees it.
  • Avalonia is upgraded to 11.3.22. Avalonia had this same bug internally; on an earlier version the positions arrive already quantized no matter what this app does.

Strokes taper properly

  • Width ramps along a segment instead of stepping at every sample, so pressure changes read as a smooth swell rather than a series of steps.
  • A stroke can taper below one pixel. The minimum width was clamped to 1 DIP, which put a floor under every lift-off; it is 0.25 now, so a stroke can thin to a hairline and disappear the way it should.

Tools for looking at what actually happened

Three additions, each built because a question could not otherwise be answered.

A stroke recorder. Draw once, then replay the same pen data as many times as you like with different settings. Recordings go to %LOCALAPPDATA%\PenDynamicsLab\recordings, and replay renders headlessly to a PNG — no window, so comparing ten settings is ten files rather than ten sessions.

A tap test. Stamps a ring of antialiased circles, which separates a surface problem from a stroke problem: if the circles are clean and the strokes are not, the canvas is fine and the fault is downstream.

--selftest. Nine checks covering DPI awareness, window placement, the drawing surface, and the coordinate conversion — with no tablet, no pen input, and no person. Exit code 0 only if everything passes.

SELFTEST PenDynamicsLab
[PASS] L0.dpi-awareness        PerMonitorV2
[PASS] L0.window-placement     client 3712x1973 at 114,65; work area 3840x2052 at 0,0
[PASS] L0.scale                2.25x
[PASS] L1.surface-physical     bitmap 2648x1470, expected 2648x1470 (= ceil(1177x653 logical x 2.25))
[PASS] L1.surface-alignment    origin 1178.00,620.00px
[PASS] L1.presentation-1to1    bitmap 2648x1470 presented at 2648.0x1470.0 device px
[PASS] L2.recording-subpixel   0.0% of 480 recorded points are on whole pixels
[PASS] L3.conversion-snap      0.0% of converted points land on whole device pixels
[PASS] L3.conversion-lossless  mean turn angle in 0.74 deg, out 0.74 deg (delta 0.00)
RESULT 9/9 passed

The last line is the one worth having. A coordinate conversion is a translation and a uniform scale; both preserve angles exactly, so a correct one reproduces the input's turn angle to the decimal. That is a stronger statement than "the strokes look smooth", it needs no threshold, and it is the first time this app's pipeline has been shown correct by measurement rather than by eye.

The checks and their report format come from WinPenKit, so this app is now held to exactly the same yardstick as the Scribble samples.

The window stays on the screen

The window declared 900 device-independent pixels of height. That is 2025px on a 4K display against a 2052px work area, and 1575px on a 1440p one against 1356px — where it could not fit at all.

Pen input is delivered by absolute screen position, so any part of the window below the work area receives nothing: no points, no telemetry, no error, while the app carries on running and painting. The lower part of the canvas simply stops drawing, which reads as a bug in whatever you were testing.

It is now clamped to the monitor's work area, both at startup and when the window moves to a monitor with a different scale. It only ever shrinks, and only when the declared size does not fit, so on a display where it fits nothing changes.

Also in this release

  • Clipboard copy moved to Avalonia's DataTransfer API, replacing the deprecated one.
  • Pull request CI. Nothing verified a PR to this repo before now — only tagged releases were built. Every PR now builds, runs the 229 tests, and must pass --selftest.
  • The Tmds.DBus.Protocol advisory (CVE-2026-39959) is resolved by the Avalonia upgrade.

Verification

The coordinate fix, the width ramp, the hairline taper and the Wintab digitizer default are confirmed on hardware — drawn with a real tablet and judged by eye.

The work-area clamp is verified by measurement rather than by drawing: the window geometry was read back after moving it between displays. --selftest reports 9/9 on a 4K display at 2.25× and on a 1024×720 CI runner at 1.00×.