Repository navigation
feat: drop the last dependency, own the rasteriser and the font - #9
Conversation
golang.org/x/image supplied two things: an anti-aliased rasteriser and a bitmap font. Those are exactly the two that decide what this library's pixels are, and a library whose whole premise is that a version always produces the same mark cannot outsource them. A release that sharpened its anti-aliasing by a fraction of a pixel would change every mark ever generated, and it would arrive as a routine version bump. The same argument was already written into internal/noise and applies with more force here. canvas/fill.go rasterises by signed-area accumulation: each edge deposits, per pixel, the signed area it sweeps in that column, and a running sum along the row gives coverage. It is exact for straight edges, so filling a shape and summing the alpha returns its geometric area — which turns "is the anti-aliasing right" into arithmetic rather than taste, and the tests assert it to within one percent for rectangles, sub-pixel slivers, triangles, annular sectors and circles. Direction folds into a sign, so the non-zero rule the canvas already relied on falls out of the accumulation instead of needing a winding pass. canvas/font.go is a 5x7 bitmap font over printable ASCII, written out as pictures rather than as hex: a wrong bit in a hex table is invisible and a wrong dot here is not. Tests check every glyph's shape, that the set covers printable ASCII, and that no two glyphs collide. The swap exposed a real defect in the measurement. Only the polar renderer's distances moved — its patch minimum fell from 24 to 13 — while the marks were visually identical, differing by at most 9 of 255 on anti-aliased edges alone. The cause was the difference hash: it point-sampled its 9x8 grid, so shifting an edge by a fraction of a pixel moved the sampled pixel across it and flipped bits wholesale. Thresholds calibrated against that were partly measuring the rasteriser. The grid is now box-averaged, and CheckMetricStability renders the corpus at two resolutions and requires the mean distance to agree — the instrument is now checked as well as the renderers. Under it, the mean moves by at most 0.6 bits across a 1.5x resolution change, where before a 9/255 edge wobble moved polar's by six. All thresholds are re-frozen against the corrected measurement. Some rose and some fell, which is what removing a noisy component looks like; the numbers are not nudged to pass but re-measured after fixing the instrument. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KwWthYNYf1egKNFrFS83Dm
The CI matrix failed a single polar pair on macOS that cleared the same threshold on Linux and Windows: 0.0.3 against 0.1.3, measured at 16 bits on amd64 and 12 on arm64. Nothing about the renderer differs — arm64 contracts float expressions that amd64 evaluates separately, which moves an anti-aliased edge by a fraction of a pixel, and on a borderline pair that is worth several bits of measured distance. That makes a bound set just under one machine's observed minimum not a bound at all. Means are stable to well under a bit, as the resolution-independence check already showed; minima are not, and the thresholds are stated in terms of minima. So the margin moves from about fifteen percent to about thirty, the dist subcommand suggests bounds with that margin already applied, and both CONTRIBUTING and the table itself say why the room is there — otherwise the next person to calibrate will tighten it back to whatever their own machine measured and the macOS leg will find out for them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KwWthYNYf1egKNFrFS83Dm
|
Two things worth recording, both surfaced after opening this. Dependabot alerts on The CI matrix earned its keep again. macOS failed on a single polar pair — So a bound set just under one machine's observed minimum is not a bound. Means |
golang.org/x/imagesupplied two things: an anti-aliased rasteriser and abitmap font. Those are exactly the two that decide what this library's pixels
are, and a library whose whole premise is that a version always produces the
same mark cannot outsource them. A release that sharpened its anti-aliasing by
a fraction of a pixel would change every mark ever generated, and it would
arrive as a routine version bump.
go.modnow lists nothing, andTestNoDependencieskeeps it that way.The rasteriser —
canvas/fill.goSigned-area accumulation: each edge deposits, per pixel, the signed area it
sweeps in that column; a running sum along the row gives coverage. Exact for
straight edges, so filling a shape and summing the alpha returns its
geometric area — which turns "is the anti-aliasing right" into arithmetic
rather than taste. The tests assert it to within one percent for rectangles
placed off-grid, sub-pixel slivers, triangles, annular sectors and circles.
Direction folds into a sign, so the non-zero rule the canvas already relied on
falls out of the accumulation rather than needing a winding pass — reversed
inner contours still punch holes, same-wound overlaps still union.
The font —
canvas/font.goA 5×7 bitmap font over printable ASCII, written out as pictures:
A wrong bit in a hex table is invisible; a wrong dot here is not. Tests check
every glyph's shape, that the set covers printable ASCII, and that no two
glyphs collide.
What the swap exposed
Only
polarmoved: its patch minimum fell from 24 to 13. The marks werevisually identical — a pixel diff showed at most 9 of 255, on anti-aliased
edges alone.
The cause was not the new rasteriser. It was the difference hash, which
point-sampled its 9×8 grid: shift an edge by a fraction of a pixel and the
sampled pixel crosses it, flipping bits wholesale. Thresholds calibrated
against that were partly measuring the rasteriser rather than the marks.
The grid is now box-averaged, and
CheckMetricStabilityrenders the corpus attwo resolutions and requires the mean distance to agree — so the instrument is
checked as well as the renderers:
All thresholds are re-frozen against the corrected measurement. Some rose
(truchet's patch minimum 16 → 24), some fell (flowfield 28 → 20), which is what
removing a noisy component looks like. They are re-measured after fixing the
instrument, not nudged until the suite went green.
Also
so a long prerelease string no longer runs into its neighbour.
x/imageignore rule is gone with the dependency; thegomodentry stays so that the day one is added deliberately, it is also watched.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KwWthYNYf1egKNFrFS83Dm