Why the edges aren't perfectly sharp — and why that's more accurate
If you zoom right in on a card's stripes you will find the boundary pixels are grey, not pure black or pure white — and that faint greyness is the only reason the stripes are all the same width and evenly spaced, which is the thing the card is actually for.
Combines established science with practitioner guidance and our design opinion.
In one line
If you zoom right in on a card’s stripes you will find the boundary pixels are grey, not pure black or pure white — and that faint greyness is the only reason the stripes are all the same width and evenly spaced, which is the thing the card is actually for.
Key points
- A card is a measurement, not a picture. Everything the generator does comes down to putting a feature of a specified size in front of a child at a specified distance (How big should it be? A plain-language guide to sizing a contrast card). A stripe pattern’s whole meaning is its spacing.
- Pixels are a grid; the sizes we need are not. A 3.4 mm bar printed at 300 dpi is 40.16 pixels wide. There is no such thing as 0.16 of a pixel — so something has to give.
- Option A: round it off. Make the bar 40 pixels, or 41, whichever is nearer. Now the bar is wrong by up to 1.2 %, and — much worse — different bars round differently.
- That is where stripes fall apart. Take a grating whose period lands on 7.3 pixels. Rounded off, you get a run of 7-pixel stripes with an 8-pixel one every so often. Neighbouring stripes then differ in width by 14 %, and the pattern’s spacing wanders by half a pixel across the sheet. The wander is periodic, so it shows up as a large, slow ripple that nobody designed — the same effect as a moiré pattern [established science — sampling theory].
- Option B: let the boundary pixel be grey. Give each pixel the fraction of itself that the shape actually covers. A bar covering 16 % of a pixel makes it 16 % dark. Now the 40.16-pixel bar really deposits 40.16 pixels’ worth of ink, wherever on the grid it happens to land.
- We measured both, in the finished file. Same six gratings, both ways, measured after the PNG had been written and read back: with grey boundary pixels every period landed within 0.006–0.011 pixels of every other one. Rounded off, the same gratings varied by 0.5 to 1.0 whole pixels — between 45 and 170 times worse — and their stripe widths by up to 23 percentage points against our 0.24 [our design opinion, measured].
- Soft edges are not soft contrast. The black is still black and the white still white. Only the single boundary pixel is in between, and it is grey because it is genuinely half-covered. At 300 dpi that pixel is 0.08 mm across; at any distance a card is used from, no eye resolves it.
- Which grey it is matters as much as that it is grey. A half-covered pixel should emit half the light — which is stored as 188, not the 128 most software would write. That is its own small story, and it is told below.
- We had this backwards at first. An early planning note argued that a maximum-contrast card should be rendered hard-edged, with no grey anywhere. That was wrong, and reversing it is what this whole piece of the build was about.
The evidence
Rounding a pattern to whole pixels invents a second pattern
This is the classical aliasing / moiré result: when a periodic signal is point-sampled at a rate that is not a multiple of its period, the sampled version carries a new, much lower frequency that was never in the original. Rounding stripe edges to whole pixels is exactly point sampling. [established science — Nyquist–Shannon sampling theory]
Area coverage is the standard fix, and a long-settled one: computing how much of each pixel a shape covers — rather than asking whether the pixel’s centre happens to be inside it — is what every serious text renderer does, because letters are precisely the case where a fraction of a pixel changes what you see. [established practice — spatial anti-aliasing; e.g. FreeType’s and font-rs’s scanline coverage rasterisers]
You can see it rather than take our word for it. Running npm run raster:qa in this
project’s repository draws a reference sheet with six gratings on it, at 6× magnification, and
then draws the same six gratings with anti-aliasing turned off underneath. In the first the
bars are visibly identical, each with a thin even grey line down either side. In the second the
bars are visibly not identical: some are a pixel wider than their neighbours, and the pattern
of which ones drifts across the sheet. That drift is the thing this whole article is about.
Half covered should mean half the light, not half the number
There is a second decision hiding inside “make the boundary pixel grey”, and getting it wrong undoes most of the benefit.
Image files do not store light; they store a code for light. The sRGB standard the whole web and most printers use is deliberately non-linear, because human vision is: the code 128, exactly halfway between 0 and 255, is only about 21 % of the light of white [established science — sRGB transfer function]. So a renderer that averages the codes and writes 128 into a half-covered pixel has produced a pixel that emits a fifth of the light it should, and therefore a bar that measures wider than the bar it was asked to draw.
We blend in linear light instead — convert to actual light, mix by area, convert back — so a half-covered black-on-white pixel comes out as 188. Every measurement in this article was taken the same way: ink is measured in light, never in byte codes.
The numbers, end to end, from the file a person downloads
Every figure below was measured after the PNG was written and read back with an independent decoder — not inside the renderer, where it would be easier to look good.
- Bar width. Seven widths from 0.04 mm to 8 mm, at 150, 300 and 600 dpi, each at six different sub-pixel positions on the grid: worst error 0.0069 pixels, worst spread across positions 0.0093 pixels. The tolerance we set ourselves before starting was 0.02.
- The 0.47-pixel feature. A 0.04 mm bar at 300 dpi is under half a pixel wide — the case a rounded-off renderer must draw as either nothing at all or a whole pixel, i.e. wrong by 100 %. It came out within 0.0069 pixels of its true width at every position tried.
- A 100 mm ruler. At 72, 150, 300 and 600 dpi: worst error 0.0046 pixels, about 0.0004 mm.
- Gratings. Six combinations of resolution and period, all deliberately non-integer (7.3 px, 5.7 px, 11.35 px, 9.13 px, 4.27 px), 14–18 bars each: every period within 0.011 pixels of every other, duty cycle within 0.24 % of 50 %, and no cumulative drift along the row (0.005 pixels over the full run).
- Against a completely different renderer. The same design was also rasterised from its PDF by Mozilla PDF.js — a well-known, independently-written renderer — and compared pixel by pixel. Of the 98 727 pixels in the interiors of the shapes, zero disagreed. The disagreements were confined entirely to boundary pixels, where two renderers are allowed to round differently, and the worst edge was displaced by 0.4 pixels.
[our design opinion for the rendering decisions; the measurements are what they are]
Why this matters for a child rather than for a screenshot
What a young infant can resolve is described in cycles per degree — a spacing, not a shape (See what your baby sees: simulating infant acuity in the preview, research §02). A card whose stripe spacing wanders is a card whose spatial frequency wanders, so parts of it may sit on the wrong side of what that child can currently see, and you would have no way of telling which parts. Keeping the spacing exact is the point of the exercise. It is not a claim that any particular card does anything for development. [established science for the vision side; our design opinion for the rendering]
Practical takeaways
- Do not “sharpen” the file. Some image tools offer to remove grey edge pixels, and some printer drivers offer a “crisp text / no anti-aliasing” mode. Both undo the thing that makes the sizes correct. Print the file as it comes.
- Do not re-save, re-compress or resize the download. Every one of those steps resamples the image onto a new grid and reintroduces exactly the rounding we removed — and it damages precisely the boundary pixels that carry the geometry. (It is also why a curved card downloads several times larger than a striped one: almost the whole file is those boundary pixels. A bigger file is not a better card, and a smaller one is not a worse one.)
- Higher dpi does not make the grey go away, and does not need to. At 600 dpi the boundary pixel is 0.04 mm — real, invisible, and the sizes get more exact, not less.
- A photocopy is not a copy. A copier resamples onto its own grid. Generate a fresh card from the same settings instead — it will be identical — and print at 100 % / actual size (Print at 100%: why "Fit to Page" quietly ruins a correctly-sized card).
- A quick check, if you are curious. Open a card’s PNG and zoom to 1600 %. A thin, even, unchanging grey line along a stripe edge means the geometry survived. A ragged edge that alternates between two positions means something along the way rounded it off.
Caveats & disclaimer
Not a medical device and not a diagnostic tool. Nothing here says a card with exact stripe spacing does anything for a child’s vision — it says that if a card claims a spacing, the spacing should be true. What high-contrast cards can and cannot do is covered in Cards supplement, never replace, face-to-face time. For a premature infant, or for CVI or any low-vision context, the people to ask are the child’s clinicians.
One honest limit, stated because it is the kind of thing this article would otherwise imply
away: exact anti-aliasing preserves a size that is already correct. It cannot rescue a card
printed at “fit to page”, and it cannot make a pattern finer than the printer can lay down. Both
of those are checked once, by hand, at /check-your-printer/.
Related
- Research:
../02-research/02-patterns-and-perception.md,../02-research/04-viewing-distance-and-sizing.md - Spec:
../../07-rasteriser-spec.md§1 (why anti-aliasing is correctness here), §5 (how the coverage is computed), §6 (linear light) and §8 (the nine gates and their measured values) - Settings: pattern, viewing distance, boldness, output resolution
- Other notes: How big should it be? A plain-language guide to sizing a contrast card, See what your baby sees: simulating infant acuity in the preview, Why the same contrast card won't work on the ceiling and the changing table, Print at 100%: why "Fit to Page" quietly ruins a correctly-sized card, Tiling explained: a poster-sized contrast print from an ordinary home printer, Cards supplement, never replace, face-to-face time
- Still in the notes pipeline, and closely related: Grey is not half-way (why 128 is only 21 % of the light), Shrinking a picture can invent a pattern that was never in it, Your card file knows how big it is, A poster too big for the browser to draw, and Print it again next year and get the same card.