Skip to content

fix(clock-face): trace canvases were photographed at the wrong resolution - #354

Merged
Yona-Appletree merged 12 commits into
mainfrom
claude/musing-brattain-8fb80e
Aug 6, 2026
Merged

fix(clock-face): trace canvases were photographed at the wrong resolution#354
Yona-Appletree merged 12 commits into
mainfrom
claude/musing-brattain-8fb80e

Conversation

@Yona-Appletree

Copy link
Copy Markdown
Collaborator

Fixes the open defect docs/defects/2026-08-05-clock-face-baselines-oscillate.md: clock-face story baselines alternated between two renderings, so every studio-touching PR collected a validate-stories bot commit and no branch reached a stable head.

Not a settling race

The starting hypothesis was that the face's per-paint self-freeze admits two settling points. The pixel diff of the two committed variants of clock-face__crowd__lg (7 506 px, max Δ243, confined to the eight trace canvases) says otherwise:

  • The waveforms are at the same phase in both — peaks, troughs and square transitions on the same output columns. Nothing about time is in play.
  • Every device-pixel-absolute constant shrinks together: the 3px pad reads ≈0.5, the 1.25px stroke reads ≈0.24, and the 1px 14%-alpha midline is gone entirely while the vertical risers survive at varying intensity. That asymmetry is the fingerprint of a bitmap being scaled down; the curve, being normalized, comes through untouched.

Root cause

paint_card sizes the canvas backing store from getBoundingClientRect(), and on a frozen story page the driver stops the rAF loop after its first frame — so that one measurement is permanent. Studio's stylesheet is injected by the wasm bundle after boot (index.html says so), so a paint that beats it measures a box the card does not end up with. Both outcomes are stable terminals; the stable-pair capture passes on either.

Reproduced live by serving the story build with the tailwind stylesheet delayed 1500 ms: the canvases painted at the pre-stylesheet box (backing 2160×84 for a box that settles at 126.66×42) and stayed there.

Fix

  1. A ResizeObserver per driver repaints on box change — it runs even though the rAF loop has stopped, which is the property the frozen page needs.
  2. The canvas box moves to an inline style: it applies on the first layout, and it keeps the box independent of the width/height attributes the paint writes (otherwise an unstyled canvas at dpr > 1 takes its box from those attributes and the repaint feeds itself).
  3. The capture's ready gate refuses to shoot while any canvas.ux-box-sized-canvas has a backing store that disagrees with its box — a repeat becomes a story-ready timeout instead of a silent bot commit.

Verification

  • Live repro + fix confirmed in the browser (canvases converge on box × dpr once a frame runs).
  • A local capture of the clock-face stories matches CI's first variant inside the trace-canvas band to a max channel delta of 1 (against 243 for the other) — so d7712f79 was the correct render and main currently holds the degraded bitmap.
  • just check, just test-studio-host, cargo fmt --check all clean.

Expect one baseline refresh. main's clock-face baselines hold the degraded render, so the first validate-stories run here will legitimately auto-commit them. The acceptance check for this defect is that the run after that reports no drift.

Note: PR #349 carries the same defect file with status: open; whichever lands second will need a trivial conflict resolution in favour of this one.

🤖 Generated with Claude Code

Yona-Appletree and others added 6 commits August 5, 2026 09:22
The trace driver sizes each canvas's backing store from
getBoundingClientRect, and on a frozen story page it stops the rAF loop
after the first frame — so that one measurement is permanent. Studio's
stylesheet is injected by the wasm bundle after boot, so a paint that
beats it measures a box the card does not end up with and bakes a bitmap
the browser then squeezes into the real one. Both outcomes are stable,
which is why clock-face baselines alternated between two renderings and
every studio-touching PR collected a bot commit.

A ResizeObserver per driver repaints on box change, so the styled size
wins no matter which paint got there first. The canvas box moves to an
inline style at the same time: it applies on the first layout, and it
keeps the box independent of the width/height attributes the paint
writes — otherwise an unstyled canvas at dpr > 1 would take its box from
those attributes and the new repaint would feed itself.

Verified live by delaying the stylesheet 1500ms: the canvases painted at
the pre-stylesheet box and stayed there; with the fix they converge on
the styled size as soon as a frame runs. A local capture now matches the
correct CI variant to a max channel delta of 1 inside the trace band.
The clock-face oscillation was invisible to the capture: a canvas painted
for the wrong box is a perfectly stable render, so the stable pair passed
on it and the auto-commit shipped whichever one the run reached. The ready
gate now asserts the invariant directly — no `canvas.ux-box-sized-canvas`
may be photographed while its backing store disagrees with its CSS box —
in the same idiom as the data-preview-painted and font gates. A repeat of
this class becomes a story-ready timeout instead of a bot commit.
…aded

main carries the wrong-resolution render for clock-face crowd__md and
default__lg — the single faint row with no midline this branch's fix
diagnoses. Every other clock-face baseline on main is already correct;
run 31024986361 captured this branch and reproduced them byte-for-byte,
naming exactly these two as stale.

The bytes restored here are that branch's own copies, which the same run
confirmed identical to a fresh capture in the pinned CI environment — not
a local capture. Without this the merge would take main's side (this
branch had not touched the files) and reintroduce the degraded pair.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

CI refreshed the story baselines on this branch

The validate-stories job detected drift and committed the fresh set: 20 changed
in 200fe38.

Review every PNG in the PR's Files changed view (swipe / onion-skin).
Your local branch is now behind — git pull before pushing again.

Story Before After
studio__node__clock-face__crowd__md.png studio__node__clock-face__crowd__md.png@11f82f1a0e studio__node__clock-face__crowd__md.png@200fe38199
studio__node__clock-face__crowd__sm.png studio__node__clock-face__crowd__sm.png@11f82f1a0e studio__node__clock-face__crowd__sm.png@200fe38199
studio__node__clock-face__default__lg.png studio__node__clock-face__default__lg.png@11f82f1a0e studio__node__clock-face__default__lg.png@200fe38199
studio__node__clock-face__fast__sm.png studio__node__clock-face__fast__sm.png@11f82f1a0e studio__node__clock-face__fast__sm.png@200fe38199
studio__node__clock-face__long-runtime__lg.png studio__node__clock-face__long-runtime__lg.png@11f82f1a0e studio__node__clock-face__long-runtime__lg.png@200fe38199
studio__node__clock-face__long-runtime__md.png studio__node__clock-face__long-runtime__md.png@11f82f1a0e studio__node__clock-face__long-runtime__md.png@200fe38199
studio__node__clock-face__no-phasors__lg.png studio__node__clock-face__no-phasors__lg.png@11f82f1a0e studio__node__clock-face__no-phasors__lg.png@200fe38199
studio__node__clock-face__overridden__lg.png studio__node__clock-face__overridden__lg.png@11f82f1a0e studio__node__clock-face__overridden__lg.png@200fe38199
12 more file(s)
  • studio__node__clock-face__overridden__md.png (changed)
  • studio__node__clock-face__paused__lg.png (changed)
  • studio__node__clock-face__paused__sm.png (changed)
  • studio__node__clock-face__scrubbed__lg.png (changed)
  • studio__node__clock-face__scrubbed__md.png (changed)
  • studio__node__clock-face__scrubbed__sm.png (changed)
  • studio__node__clock-face__shared__lg.png (changed)
  • studio__node__clock-face__shared__md.png (changed)
  • studio__node__clock-face__shared__sm.png (changed)
  • studio__node__clock-face__unknown__lg.png (changed)
  • studio__node__clock-face__unknown__md.png (changed)
  • studio__node__clock-face__unread__sm.png (changed)

Yona-Appletree and others added 6 commits August 5, 2026 10:36
Two CI captures of the same tree agree byte-for-byte on the whole
clock-face family — the comparison that can falsify an oscillation, and
the one nothing in the pipeline does on its own. Also records that main
held the degraded render for only two of the stories: an oscillating
baseline leaves the set mixed, so a branch that has not touched those
files silently takes main's side on merge.

Flags exploration/node-ui/status-indicators sm as a fresh member of the
bistable set: byte-identical in the first capture, 304 significant px in
the second, in a story family with no canvas — a different mechanism.
Conflicts: the two clock-face baselines this branch had pinned. main's
side wins both — PR #345 landed the clock transport hero, so those images
are new CONTENT (525px and 355px tall against the pinned 507px), not the
degraded re-render the pin was defending against. The pinned bytes are the
old layout and no longer mean anything.

That leaves those two baselines whatever main's pre-fix capture produced.
With the resize repaint in this branch they converge on one render instead
of alternating, so at worst the next capture refreshes them once.
Conflicts: five clock-face baselines that main and this branch both
rewrote. Ours win — they are the post-fix capture (run 31058006664),
while main's are pre-fix renders its own captures keep rewriting: the
last three commits touching these files on main are all auto-refresh bot
commits, and the dimensions match, so this is the oscillation churning,
not a content change.
@Yona-Appletree
Yona-Appletree merged commit 55579af into main Aug 6, 2026
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