Skip to content

renderText: smooth outlines along the antialiased glyph edge - #78

Merged
matmen merged 1 commit into
masterfrom
smooth-stroke
Oct 16, 2026
Merged

matmen merged 1 commit into
masterfrom
smooth-stroke

Conversation

@matmen

@matmen matmen commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

The outline from renderText (strokeWidth) now follows the glyphs' antialiased edges instead of their pixel staircase. Wide strokes were visibly bumpy before.

Cause

The outline came from the distance to the nearest pixel that is at least half covered. That treats the glyph as a binary mask, so the outer edge of the stroke traced the mask's stairs. The effect grew with the stroke width.

Fix

  • Nearest covered pixel: the distance transform now also returns, for each pixel, the nearest covered pixel.
  • Edge points: for each edge pixel the edge point is estimated once, from the pixel center along the coverage gradient (Sobel) to where a straight edge would cover that pixel by its coverage. This is the idea behind Gustavson's antialiased distance transform.
  • Band near the outline's edge: only pixels in the band where the stroke fades out use the closest edge point among the 3×3 pixels around the nearest covered pixel. Every other pixel is decided by the plain distance as before.
  • Speed:
    • Distances beyond the outline (more than radius + 2) are left out of the row pass, so the extra work costs almost nothing.
    • The final max with the glyph coverage happens in the same loop.

Measurements

  • Accuracy: compared with text rendered at 8× size and scaled down, the mean error of the outline's coverage fell from about 0.3 to 0.07–0.1. This holds for Source Sans and Open Sans Bold at 24 and 64 px with stroke widths 1–24.
    • A 5×5 window was only measurably better for Carbon, a decorative font with sharp concave corners.
  • Time: 64 px text with a 3 px stroke takes 7.9 ms instead of 6.6 ms; 128 px with an 8 px stroke takes 25.1 ms instead of 24.4 ms.
    • A 4× supersampled distance field looked just as smooth but took 16 times as long.
  • Width: the outline now extends exactly strokeWidth beyond the edge. Before, it was about half a pixel narrower.

The effects target is regenerated, because its outlined text changes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ

The outline was the distance to the nearest pixel at least half covered,
so its edge followed that pixel staircase (bumpy at larger widths). Near
the outline's edge, the distance now goes to the edge points of the
pixels around the nearest covered one (3x3): from the pixel center along
the coverage gradient to where a straight edge would cover it as much,
like Gustavson's antialiased distance transform. The distance transform
also returns the nearest sample for that.

Against text rendered at 8x and scaled down, the mean error of the
outline's coverage drops from about 0.3 to 0.07-0.1 (Source Sans, Open
Sans, stroke widths 1-24). Distances beyond the outline are left out of
the row pass, so it costs about as much as before (64px text, 3px
outline: 6.6 -> 7.9 ms). effects target regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
@matmen
matmen merged commit 3d28ae4 into master Oct 16, 2026
13 checks passed
@matmen
matmen deleted the smooth-stroke branch October 16, 2026 20:36
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