Stage four. Strokes are kept per page in that page's own coordinate
space — PDF points at scale 1 — not in the fixed 1200x1600 space a note's
drawing uses. A PDF cannot use one space: every page has its own size,
and a stroke belongs to the page it was drawn on.
That is what keeps a mark on the word it was drawn over at any zoom and
on any screen, and it is the only form that can be stamped back into the
file in stage seven, where page points are the only coordinates that mean
anything. Measured rather than assumed: zooming moves a stroke by 0.001
of the page.
Pen sizes are scaled by the page width. The sizes are calibrated for the
1200-unit note page, so on a 595-point A4 page the same number would have
drawn twice as thick.
Reading and drawing are separate modes. While reading the ink layer takes
no pointer events, so scrolling and selecting text reach the page beneath
it; while drawing, a finger still scrolls and only a pen or mouse marks,
because a touch-drag on a phone means scroll far more often than it means
draw. The drawing can be hidden without being erased, for reading the
original clean.
The server accepted only the single-page shape; it now takes either and
counts strokes across pages for its limit.
Carried over from notes: the save token guard, so the six-second poller
adopting a response issued before the local save cannot undo what was
just drawn. The suite waits nine seconds and checks the ink is still
there — that bug has cost strokes twice before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage three: the viewer. Drawing on pages comes next.
pdf.js stays out of the bundle everyone downloads — it is larger than the
rest of the application put together, so it is behind a dynamic import and
a session that never opens a PDF never fetches it. Main bundle grew by
8 kB for the viewer shell; the 162 kB library arrives only when needed.
The worker is bundled rather than pulled from a CDN, because a viewer
that stops working during someone else's outage is not one you can rely
on offline.
Two things that only showed up once it ran in a browser:
- pdf.js 6 calls Uint8Array.prototype.toHex, which only very recent
browsers have. Anything older failed with "n.toHex is not a function",
which says nothing about the cause. Now on the legacy build, which
compiles those away — a little larger, and the difference between
working everywhere and working on this year's browsers.
- nginx has no MIME type for .mjs and served the worker as
octet-stream, which a browser refuses to run as a module.
Pages are laid out at their true proportions before anything is drawn, so
the scrollbar is honest immediately and a document mixing portrait and
landscape keeps both — the first version scaled every page to the widest
one, which the suite caught by checking the two aspect ratios differ.
Bytes come from the authenticated attachment route and are cached in
IndexedDB. Content-addressed storage means a cached copy can never be
stale, so a document opened once needs no connection again. Verified with
the network switched off.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>