Skip to content

feat: drop the last dependency, own the rasteriser and the font - #9

Merged
timzifer merged 2 commits into
mainfrom
feat/zero-dependencies
Sep 10, 2026
Merged

timzifer merged 2 commits into
mainfrom
feat/zero-dependencies

Conversation

@timzifer

Copy link
Copy Markdown
Owner

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.

go.mod now lists nothing, and TestNoDependencies keeps it that way.

The rasteriser — canvas/fill.go

Signed-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.go

A 5×7 bitmap font over printable ASCII, written out as pictures:

'A': ".###./#...#/#...#/#####/#...#/#...#/#...#",

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 polar moved: its patch minimum fell from 24 to 13. The marks were
visually 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 CheckMetricStability renders the corpus at
two resolutions and requires the mean distance to agree — so the instrument is
checked as well as the renderers:

before the fix after
polar mean, under a 9/255 edge wobble −6 bits —
any renderer's mean, across a 1.5× resolution change — ≤ 0.6 bits

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

  • Contact-sheet labels pick the largest whole font scale that fits their cell,
    so a long prerelease string no longer runs into its neighbour.
  • Gallery regenerated.
  • Dependabot's x/image ignore rule is gone with the dependency; the gomod
    entry stays so that the day one is added deliberately, it is also watched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KwWthYNYf1egKNFrFS83Dm

timzifer and others added 2 commits September 10, 2026 08:45
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
@timzifer

Copy link
Copy Markdown
Owner Author

Two things worth recording, both surfaced after opening this.

Dependabot alerts on main. Pinning x/image at v0.24.0 to hold the Go
floor at 1.21 also pinned its advisories: one high and one moderate, both
excessive-resource-consumption bugs in the TIFF decoder — a package this
library never imported. Not exploitable here, but a fair illustration of the
cost of holding an old pin for an unrelated reason. This branch removes the
dependency, and the alerts go with it.

The CI matrix earned its keep again. macOS failed on a single polar pair —
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 shifts an anti-aliased edge by a fraction of a
pixel, and on a borderline pair that is worth several bits.

So a bound set just under one machine's observed minimum is not a bound. Means
are stable to well under a bit — the resolution-independence check already
showed that — but the thresholds are stated in terms of minima, and those are
not. The margin moves from about fifteen percent to about thirty, blazon dist
now suggests bounds with that margin already applied, and both CONTRIBUTING and
the table say why the room is there.

@timzifer
timzifer merged commit 4e8fd39 into main Sep 10, 2026
8 checks passed
@timzifer
timzifer deleted the feat/zero-dependencies branch September 10, 2026 07:02
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