Skip to content

Brush: average along the path so slow strokes lose the mouse's pixel staircase - #18

Merged
pathanin merged 1 commit into
masterfrom
master-6f5h9b
Oct 20, 2026
Merged

pathanin merged 1 commit into
masterfrom
master-6f5h9b

Conversation

@pathanin

Copy link
Copy Markdown
Owner

Problem

A mouse reports whole pixels, so a slow, shallow stroke arrives as a staircase that repeats every 1 / slope px. The lazy brush smooths over only its radius (4 px at the default smoothing). Most of each step therefore reached the curve, and slow strokes came out faceted along their edges.

Change

  • Core.Stroke(p, radius, step, span) takes a new span. With it, the brush's path is resampled every step. Each point is then replaced by a triangular average of span points on either side. The window shrinks near the ends, so the stroke still starts at the pointerdown point and ends exactly at the release point.
  • Because of the resampling, the window has a fixed length whatever the event rate (1000 Hz coalesced, 125 Hz, Safari's per-frame events).
  • In the app (brushStroke() in pointer.js), points are 2 screen px apart and the span is 3 × smoothing. Smoothing 0 behaves exactly as before.
  • Side effect: slow strokes store fewer segments than with the old 0.5 px minimum step.
  • task.md's brush-smoothing decision is updated.

Measurements (simulated whole-pixel mouse input, no benchmark)

How far quantization pushes the curve off the curve drawn from exact input, ends excluded:

path before after
shallow line (slope 1/40) 0.38 px 0.20 px
S curve 0.26 px 0.16 px
circle 0.28 px 0.20 px

The trade-off is that a sharp corner rounds a little more: about 3.4 px from the true corner, versus 2.0 px before.

Tests

  • New unit tests in test/core.test.js:
    • the staircase is reduced, and a wider span reduces it further
    • the stroke keeps its ends and continuity, and tail() equals finish()
    • the averaging gives the same result at different event rates
    • clicks and short moves still draw
  • npm test: 84 pass.
  • e2e, run in the bundled Chromium instead of Chrome: smoke 313, objects 197, alpha 105, history 86, preview 40, filledge 13. All pass.

Generated by Claude Code

…staircase

A mouse reports whole pixels, so a shallow line arrives as a staircase that
repeats every 1 / slope px. The lazy brush filters over only its radius
(4 px at the default), so most of each step reached the curve and slow
strokes came out faceted.

Core.Stroke now takes a span: the brush's path is resampled every step and
each point becomes a triangular average of span points either side, shrunk
near the ends so the stroke still starts at pointerdown and ends exactly at
release. Resampling makes the window a fixed length whatever the event rate.
The app uses points 2 screen px apart and a span of 3 x smoothing; smoothing
0 is unchanged. Fewer, evenly spaced points also mean fewer segments per
slow stroke than the old 0.5 px minimum step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012G2zCWMtRNnaTYycE9WgpB
@pathanin
pathanin marked this pull request as ready for review October 20, 2026 19:52
@pathanin
pathanin merged commit 2315e42 into master Oct 20, 2026
3 checks passed
@pathanin
pathanin deleted the master-6f5h9b branch October 20, 2026 19:54
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.

2 participants