Repository navigation
STU-390: Port the authored logo animation into the pipeline presentation - #95
Conversation
…ation Add an isolated logo-motion module driving the hero's two-piece mark and twelve-beat, seamless 120s loop as a decorative element in the pipeline arena. Decorative only: pipeline completion depends solely on real backend events. Static rest frame for reduced motion; pause clock/rendering when the tab is hidden, the pipeline is CSS-hidden, or the arena is offscreen; single guarded rAF loop with cleanup-safe dispose. Includes unit tests for the pose(0)==pose(120) loop boundary and brand geometry, plus Playwright lifecycle and progress-independence coverage and per-beat review screenshots.
| const observer = new IntersectionObserver( | ||
| (entries) => { | ||
| intersecting = entries[0] ? entries[0].isIntersecting : false; | ||
| sync(); | ||
| }, | ||
| { threshold: 0 } | ||
| ); |
There was a problem hiding this comment.
🟡 Pipeline logo stays at left edge
On first display, measure() retains the hidden arena's zero width. The intersection callback starts frames without remeasuring, so every full-width beat stays left until resize.
| const observer = new IntersectionObserver( | |
| (entries) => { | |
| intersecting = entries[0] ? entries[0].isIntersecting : false; | |
| sync(); | |
| }, | |
| { threshold: 0 } | |
| ); | |
| const observer = new IntersectionObserver( | |
| (entries) => { | |
| intersecting = entries[0] ? entries[0].isIntersecting : false; | |
| if (intersecting) { | |
| measure(); | |
| paint(pose(elapsed)); | |
| } | |
| sync(); | |
| }, | |
| { threshold: 0 } | |
| ); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| document.removeEventListener("visibilitychange", onVisibility); | ||
| } | ||
|
|
||
| window.addEventListener("pagehide", dispose); |
There was a problem hiding this comment.
🟡 Cached navigation freezes logo loop
On back-forward-cache navigation, pagehide permanently disposes the logo controller. Cached restoration skips DOMContentLoaded, so the loop remains frozen after returning.
Learn more
A pagehide event can represent either permanent unload or entry into the browser's back-forward cache. A cached document retains this controller, but dispose() marks it unusable and removes every listener. The later pageshow restores the same document without rerunning initialization, so no new controller replaces it.
Example: A user starts a pipeline, follows a same-tab navigation, then presses Back. If the browser restores the page from its back-forward cache, the pipeline logo remains at its last frame even when the pipeline is visible.
Recommended fix: Inspect PageTransitionEvent.persisted. Stop the loop without disposing it when pagehide enters the back-forward cache, then call sync() on persisted pageshow; reserve dispose() for permanent unload and remove all added lifecycle listeners during disposal.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
| const observer = new IntersectionObserver( | ||
| (entries) => { | ||
| intersecting = entries[0] ? entries[0].isIntersecting : false; | ||
| sync(); |
There was a problem hiding this comment.
Hidden initialization prevents logo travel
The controller is created during DOMContentLoaded while the pipeline is display:none, so measure() caches MARK=34 and TRAVEL=YMAX=0. Revealing the pipeline only calls sync()/start(), leaving those values unchanged until a window resize. The glide and chase beats therefore never travel across the arena, and piece offsets use the wrong mark size.
Re-measure when the arena becomes visible, including under reduced motion, and add a hidden-initialization/reveal test that asserts displacement rather than frame counts. Greptile automatically discovered a related ticket stating that the authored twelve-beat motion must be preserved, which informed this comment.
Source Used: Linear — Port the authored logo animation into the pipeline presentation
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/logoMotion.js
Line: 362-365
Comment:
**Hidden initialization prevents logo travel**
The controller is created during DOMContentLoaded while the pipeline is `display:none`, so `measure()` caches `MARK=34` and `TRAVEL=YMAX=0`. Revealing the pipeline only calls `sync()`/`start()`, leaving those values unchanged until a window resize. The glide and chase beats therefore never travel across the arena, and piece offsets use the wrong mark size.
Re-measure when the arena becomes visible, including under reduced motion, and add a hidden-initialization/reveal test that asserts displacement rather than frame counts. Greptile automatically discovered a related ticket stating that the authored twelve-beat motion must be preserved, which informed this comment.
**Source Used:** Linear — [Port the authored logo animation into the pipeline presentation](https://linear.app/stuart-gardoll/issue/STU-390/port-the-authored-logo-animation-into-the-pipeline-presentation)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| .loop-mark { | ||
| left: 0; | ||
| top: 50%; | ||
| width: 52px; | ||
| height: 52px; | ||
| margin: -26px 0 0 0; |
There was a problem hiding this comment.
Split-piece motion gets clipped
The mark sits at left:0 inside an overflow:hidden strip, but the magnet beat moves the fully opaque second C left by 0.72 * MARK while the parent remains at x=0. Visible portions of the C are cut off at the arena boundary. Correcting the initial measurement does not resolve this separate visual issue.
Inset the resting mark and adjust the travel range to accommodate the pieces’ transformed bounds, or otherwise allow their intended movement without clipping.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/logoMotion.css
Line: 23-28
Comment:
**Split-piece motion gets clipped**
The mark sits at `left:0` inside an `overflow:hidden` strip, but the magnet beat moves the fully opaque second C left by `0.72 * MARK` while the parent remains at `x=0`. Visible portions of the C are cut off at the arena boundary. Correcting the initial measurement does not resolve this separate visual issue.
Inset the resting mark and adjust the travel range to accommodate the pieces’ transformed bounds, or otherwise allow their intended movement without clipping.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| document.removeEventListener("visibilitychange", onVisibility); | ||
| } | ||
|
|
||
| window.addEventListener("pagehide", dispose); |
There was a problem hiding this comment.
Cached-page return freezes the logo
If the browser restores this page from its back/forward cache, the logo remains permanently stopped. pagehide fires when the page enters that cache, but this handler permanently disposes the controller and removes its observer and listeners. Returning with Back restores the disposed instance without rerunning DOMContentLoaded, so subsequent pipeline runs cannot restart the animation.
Pause rather than dispose for persisted pagehide events and resume on pageshow, or explicitly recreate the controller on restoration.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/logoMotion.js
Line: 385
Comment:
**Cached-page return freezes the logo**
If the browser restores this page from its back/forward cache, the logo remains permanently stopped. `pagehide` fires when the page enters that cache, but this handler permanently disposes the controller and removes its observer and listeners. Returning with Back restores the disposed instance without rerunning DOMContentLoaded, so subsequent pipeline runs cannot restart the animation.
Pause rather than dispose for persisted `pagehide` events and resume on `pageshow`, or explicitly recreate the controller on restoration.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.2c83518
into
milestone/stu-implement-updated-ui-ux
STU-390 — Port the authored logo animation into the pipeline presentation
Linear: https://linear.app/stuart-gardoll/issue/STU-390 (In Progress — not Done/In Review)
What this does
Ports the hero's authored two-piece mark + 12-beat, seamless 120-second loop from
custom-code-connect-hero.htmlinto the pipeline arena as an isolated, purely decorative module:src/logoMotion.js— pose()/track()/easing ported verbatim;createPipelineLogoLoop()DOM controller with hidden/offscreen/reduced-motion pausing, idempotent start (one rAF loop), cleanup-safe dispose.src/logoMotion.css— pipeline-arena styles for the mark, its two C pieces, halo and shadow.index.html— the.pipeline-glyph.logo-loopmarkup in#pipeline-progress(aria-hidden), plus the stylesheet link.app.js— initializes the loop (window.__pipelineLogo) alongside the existing hero field.Completion depends only on real backend events; the loop never gates progress. No historical outro pages or video-export tooling were added.
Acceptance criteria — how each was verified
pose(0) == pose(120) loop boundary + brand geometry
node --test src/logoMotion.test.js→ 6/6 pass. "pose(0) and pose(120) are identical on every animated key" and "loop boundary is the authored rest pose" (two interlocked Cs at identity).Falsified: injected
+0.25into beat-12rot→ test failedloop boundary mismatch on "rot": expected 0.25 ±1e-9, got 0; restored → pass.Progress completes/fails regardless of animation position
e2e (
a run that is mid-loop still completes,a mid-loop run still fails honestly): driven with hermetic per-stage fixtures whileseek()parked the loop at t=90 / t=47; releasing all stages lands Results, and a Generator quota rejection lands the failure panel. Verified behavior; decorative independence is structural (loop never touches pipeline state).Reduced-motion toggles live; no multiple rAF loops on leave/re-enter
e2e
live reduced-motion toggle …and single-loop frame-rate assertions (framesReenter < framesOn*1.7) across hidden-tab / CSS-hidden / viewport-offscreen.Falsified: removed the per-frame visibility stop + change-handler stop → reduced-motion test failed (
no logo frames under reduced motion: Expected false Received true); restored → pass. Falsified: removed the IntersectionObserver viewport gate → offscreen test failed; restored → pass. Falsify note: the offsetParent guard removal alone was NOT falsifiable — display:none also reports not-intersecting to the IO, so the CSS-hidden case is caught by the viewport gate too; the offsetParent check is defense-in-depth, verified present.Visual review compares representative beats; hidden/offscreen measured
Screenshots at beats 0/rest, 47/peek-a-boo, 90/orbit, 120/rest-boundary (mobile 360×800 + desktop 1440×900): committed under
screenshots/stu-390/(stu390-page-*.pngfull pipeline view,stu390-mark-*.pngclose-up) and attached to the run. Hidden/offscreen "no render loop continues" is measured with a module-local frame counter (frameCount()), isolating the loop from the independent hero field.Hidden tab, CSS-hidden, viewport-offscreen tested SEPARATELY
Three distinct e2e tests: hidden tab (
document.hidden+visibilitychange), CSS-hidden (display:none→offsetParentnull), and offscreen (translateY(4000px)keepsoffsetParentnon-null — only the IO viewport check catches it, provingoffsetParentalone is not proof of viewport visibility).CI sequence (all run locally on the pushed commit 01ab497)
npm test→ 319 passednpm run build→ OKnpm run test:ui(CCC_TEST_PORT=3182) → 122 passednpm run test:accessibility→ OK (7 modal shells)npm run test:deploy-guard→ 9/9 passednpm run test:proxies→ 30/30 passedpython3 scripts/scan_bundle_for_leaks.py→ No leaksWhat I could NOT verify (honest gaps)
stop()+ per-frame gate) — removing only one layer does not fail the test (defense in depth by design). The load-bearing reduced-motion path is verified by falsifying both together.Files
src/logoMotion.js,src/logoMotion.css,src/logoMotion.test.js(new)e2e/stu390-logo-loop.spec.js(new)app.js,index.html(minimal wiring)screenshots/stu-390/(review artifacts)Work in progress → now ready for review.