Skip to content

DEF-29: a video export bakes its beacons whatever the author's reduced motion - #42

Open
djDAOjones wants to merge 2 commits into
mainfrom
post-w2/def-29-export-motion
Open

djDAOjones wants to merge 2 commits into
mainfrom
post-w2/def-29-export-motion

Conversation

@djDAOjones

Copy link
Copy Markdown
Owner

Post-W2 queue · DEF-29 (plan §12.1 DEF-29, P3; the policy was decided at §20 Q7b and accepted on 2026-09-22). This changes behaviour for an author whose operating system asks for reduced motion. Their video exports now show pulse, ripple and glow beacons as authored. The editor and the exported HTML player still hold those beacons still for them, including right after an export.

The defect

  • A video export honoured the author's own reduced-motion setting. BeaconRenderer.update holds pulse, ripple and glow beacons still under prefers-reduced-motion. That is right for the editor and the live player. But it held them still in a baked video export too, so an author with the setting on exported videos without them.
  • A video is watched elsewhere, by people whose setting it can't know. §20 Q7b therefore decided: ignore reduced motion when exporting video, and keep honouring it in the live editor and player.

Stated new behaviour

  • A video export draws pulse, ripple and glow as authored, whatever the author's setting: in every frame, with the setting on from page load, off, or changed during the export.
  • The editor (edit and preview) and the exported HTML player still hold them still under reduced motion, as on main.
  • After an export, a reduced-motion editor draws exactly what it drew before, whether the export finished, was cancelled or failed.
  • Switching reduced motion on while a ripple or glow is mid-effect now stops it, rather than leaving it frozen on screen. The review found that older freeze; the same one-line change fixes it.
  • Only the three looping beacons are affected. Pop and grow were never held.
  • On its own, this PR changes nothing for the pulse in an export, because before DEF-08 (DEF-08: pop, grow and pulse scale their marker in export, scrubbing and pause #37) a paused or exported frame never drew a pulse's scale at all. Together with DEF-08, a reduced-motion author's export draws the pulse's marker scale while the editor and the player hold it still. The review merged the two and checked this.

The change

  • src/main.js: the render state carries exportingVideo: this._isExportMode. Only the video export sets that flag (_enterExportMode), and it clears it in its finally.
  • src/services/RenderingService.js: the beacons render step passes { ignoreReducedMotion: state.exportingVideo === true } through renderBeacons to the beacons.
  • src/services/BeaconRenderer.js: update skips the reduced-motion hold when told to. A held beacon is now reset rather than only rescaled, so it is drawn as one that was never synced. That second change came from the review: export frames now sync every beacon, and the hold used to leave whatever they had drawn frozen on screen.

Evidence

Tests first. In tests/goldenDrawLogs.test.js, DEF-29: a video export ignores the author's reduced-motion setting, on Open day. Each style gets its own fixture, so a check can only pass on that style's own drawing. At each beacon's arrival plus 250 ms, the frame drawn with reduced motion is compared with the frame drawn without it. A pulse shows only through its marker's scale, which before DEF-08 only a playing frame draws, so the pulse's frames are drawn playing.

Test Fails on
the export canvas draws a ripple / a pulse / a glow as authored (3 tests) main: the reduced frame differs (140 lines for the ripple)
the editor and the exported player still hold a ripple / a pulse / a glow still (3 tests) nothing: they pin the hold that must stay
after a finished / cancelled / failed export, a reduced-motion editor draws what it drew before (3 tests) this branch before its review round: a frozen glow (146 lines) or ripple ring (140 lines)

The post-export tests run the app's real exportVideo(), with only the encoder replaced, from preview under reduced motion. They compare frames at four instants before and after the export.

Mutations, each against the whole suite (parallel, JSON report, watchdog):

Mutation Caught by
H1 the beacons ignore the flag, so reduced motion holds them in an export too the three export tests
H2 every host ignores reduced motion the three hold tests
H3 the app never says it is exporting a video the three export tests
H4 the app always says it is exporting a video the three hold tests
H5 the exported player says it is exporting a video the three hold tests
H6 renderBeacons drops the option on its way to the beacons the three export tests
H7 a render state without the flag counts as a video export the three hold tests and the three post-export tests
H8 the hold only rescales a held beacon again (the review round's fix reverted) the three post-export tests
H9 the export still holds the pulse (the review's M10) the pulse's export test
H10 the editor and the player never hold the pulse (the review's M11) the pulse's hold test
C1 control: main.js and both services as on main exactly the three export tests; 1,196 others pass
C0 control: no mutation nothing; 1,199 pass

Goldens. None changed.

In a real browser (headless Chromium 141, in this session's cloud container). I built main and this branch's head (509fa43) in throwaway clones and served each locally; nothing touched Joe's machine or the live site. The review's own Playwright scripts drove the real exportVideo() with the real WebM encoder (WebCodecs VP9). Chromium's media emulation stood in for the operating-system setting and fired the app's real reduced-motion listener.

main this branch
Open day exported with reduced motion on from load: frames differing from an export with it off (of 143; 28 carry ripple rings, 29 a glow) 57 0
the same, with the setting flipped three times during the export 66, and the last frame keeps a frozen glow 0
a reduced-motion editor after that export, and after one cancelled with Escape at frame 47 (a ripple mid-effect): rings or glows drawn none none
"Site walk" with a glow on Finish, exported to the end: a glow drawn in the editor afterwards none none (r = 57.26 before the review round)

Review

Codex isn't available in this cloud container. Instead, an independent Claude agent attacked five numbered claims. It had a fresh context, its own no-remote clone and the falsification brief Codex would get. It used jsdom probes of the real exportVideo(), Chromium 141 with the real WebM encoder, and a merge with DEF-08.

It confirmed:

  • Claim 1, for ripple and glow: every exported frame, including the first and last, is identical whatever the setting. That covers the setting on from load, off, and flipped during the export. It held over 371 frames in jsdom, 143 frames with the real encoder in Chromium, and 847 frames of the authored-extras fixture. On main, 252 of the 371 frames differ with the setting on.
  • Claim 3, for readers of the flag: exportingVideo has one writer and one reader, the player never sets it, and nothing else renders in export mode.
  • Claim 4: with DEF-08 merged, the export draws the pulse's marker at 81 distinct sizes, identical in both settings, while the editor and the player hold it at its base size. The source files merge cleanly.

It found real holes, and I reproduced each before acting:

  1. Adopted: a regression in the editor after every export (claim 2 broken). The hold skipped syncing a beacon but kept its old state. Once export frames synced every beacon, a reduced-motion author saw the last ripple ring or glow frozen in the editor after any finished, cancelled or failed export. In Chromium, a real Escape cancel left a ring of r = 31.07, and a finished export left a glow of r = 57.26 round the last waypoint. main shows neither.

    The tests missed it because they reset the beacons before every reduced frame. The hold now resets the beacon. The reviewer verified this fix, and the three post-export tests pin it.

  2. Adopted: the pulse cases were vacuous (claim 5). In the shared fixture, the entrance ripple was still animating at the pulse's instant, so its lines passed the pulse's checks. Each style now has its own fixture, and the pulse is drawn playing. H9 and H10 above are the review's M10 and M11.

  3. Recorded for Joe: a second export started during the first corrupts it. _isExportMode is a boolean. A second exportVideo() throws "Export already in progress", but its finally still leaves export mode under the running export. In jsdom the remaining frames lose the flag and the canvas drops from 1920 to 1173 px wide. The likely route is a double click on Export MP4, which couldn't be tried here: headless Chromium has no H.264 encoder. It is older than this PR; the close-out PR proposes it to Joe as DEF-46.

If Joe wants Codex's view before merging, the same falsification brief is ready to run against this branch.

Design review gate (UI-STANDARDS.md)

No control, layout or style changes. The reduced-motion contract is unchanged where people are watching live: the editor and the player hold the looping beacons still, and now also right after an export and when the setting is switched on mid-effect. A baked video drops the author's personal setting, as §20 Q7b decided.

Still manual

  • An MP4 export with a real H.264 encoder. Chromium here has only WebM (VP9), which the review used.
  • A real OS reduced-motion switch. Chromium's media emulation stood in for it and fires the app's real matchMedia listener.
  • Firefox and Safari.

Merging. This PR conflicts, in text only, with two others; whichever merges second needs main merged in, and I'll do that. With #37 (DEF-08), the golden-draw-logs test's imports (both add BeaconRenderer) and its file-map row conflict; I'll combine them. Once both are in, the pulse checks can use paused frames too. With #40 (DEF-30), the plan's DEF-29 and DEF-30 rows are adjacent, so their dated notes conflict.

file-map.md: the golden-draw-logs test row describes the new coverage. The plan: a dated note on DEF-29's row.

Verify

npm run check: 85 test files · 1,199 tests · 2 todo · shell 0 · build:check 0. main: 85 · 1,190 · 2 todo.

Rollback

Revert this PR. Either way, the change reaches users only through a release.

🤖 Generated with Claude Code

https://claude.ai/code/session_013dV8F9uaxT1v6tu3JFqTkf


Generated by Claude Code

…d motion

Under prefers-reduced-motion the pulse, ripple and glow beacons were held still in every host, so an author with the setting on exported videos without them; a video export now draws them as authored, while the editor and the live player keep honouring the setting (§20 Q7b).

Verify: 85 test files · 1,192 tests · shell 0 · build:check 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dV8F9uaxT1v6tu3JFqTkf
The review found export frames, now syncing every beacon, left their last ring or glow drawn and frozen in a reduced-motion editor once the export ended; the hold now resets a held beacon rather than only rescaling it, which also ends an older freeze when the setting is switched on mid-effect. The tests give each style its own fixture, draw the pulse while playing, and check the editor after a finished, cancelled and failed export.

Verify: 85 test files · 1,199 tests · shell 0 · build:check 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dV8F9uaxT1v6tu3JFqTkf

This branch has not been deployed

No deployments
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.

2 participants