Step 10 · The Correction
The first stranger's hand
A sequence is not finished until a hand that did not grow it has touched it — the trial is a transformation too.
The report from the first beta tester, verbatim in substance: the
brush and eraser paint a few hundred pixels below and to the right of the
cursor. Everything else worked. The offset is the interesting part
— not constant, but growing toward the bottom-right — and it
convicts a whole class of suspect at once: this is the signature of the
canvas displaying at a different scale than the pointer arithmetic
assumes.
So the hunt ran backwards down the sequence. Step 4, the translator at
the door — docPoint, View.toDoc — was
read first and acquitted; the arithmetic is right. Step 6, the view
transform: acquitted; zoom-at-cursor pins its point correctly. The trail
ended at editor-01, in the stylesheet, in one declaration
laid before any pixel existed: #stage{position:absolute;
inset:0}. A <canvas> is a replaced
element, and inset:0 does not stretch a replaced element
— its displayed size stays its intrinsic size, which is the
backing store, which step 2 sizes at clientWidth ×
devicePixelRatio. At a display scaling of exactly 1:1 the two sizes
coincide and the flaw is invisible — which is why nine steps and a
headless test rig (no layout, dpr of 1) lived over it without a tremor. On a
real screen at Windows 125% or 150% scaling, a retina display, or any
browser zoom, the canvas is shown at backing size: everything drawn appears
multiplied by dpr from the top-left corner, while the pointer math aims,
correctly, at the unmultiplied place. The hand misses by (dpr − 1)
× the distance from the corner. A few hundred pixels, below and to the
right, worse as you work down the page. Exactly the report.
The correction is one line: pin the canvas's CSS size to its box, and let
the backing store keep its devicePixelRatio for crispness. It ships as
editor-10; steps 01–09 are left exactly as published,
because they are the record, and the record now includes this. Two morals,
honestly earned. First, the CMS coda's warning returns in new clothing: a
trivial decision made early and implicitly ("the stage fills its
box" — assumed, never stated) is the kind that damages important
decisions later, and it hid in the one step that seemed too simple to be
wrong. Second: every version here was tested headlessly before shipping, and
this bug is invisible to every such test by construction — there is a
class of truth only a real screen, a real pointer, and a hand that did not
write the code can reach. The first outside trial is not after the sequence.
It is step ten.
/* editor-01, css/base - the flaw, dormant for nine steps */
#stage{position:absolute;inset:0} /* a canvas is a replaced
element: inset does not
stretch it */
/* editor-10 - the correction */
#stage{position:absolute;inset:0;
width:100%;height:100%} /* pinned to its box; the
backing store keeps dpr */
One line, laid in step 1, felt in step 4, found by the first stranger.
open editor-10-correction.html →