Skip to content

STU-390: Port the authored logo animation into the pipeline presentation - #95

Merged
sgardoll merged 1 commit into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/stu-390-port-the-authored-logo-animation-into-the-pipeline
Sep 22, 2026
Merged

sgardoll merged 1 commit into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/stu-390-port-the-authored-logo-animation-into-the-pipeline

Conversation

@sgardoll

@sgardoll sgardoll commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

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.html into 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-loop markup 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.25 into beat-12 rot → test failed loop 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 while seek() 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-*.png full pipeline view, stu390-mark-*.png close-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 → offsetParent null), and offscreen (translateY(4000px) keeps offsetParent non-null — only the IO viewport check catches it, proving offsetParent alone is not proof of viewport visibility).

CI sequence (all run locally on the pushed commit 01ab497)

  • npm test → 319 passed
  • npm run build → OK
  • browser suite npm run test:ui (CCC_TEST_PORT=3182) → 122 passed
  • npm run test:accessibility → OK (7 modal shells)
  • npm run test:deploy-guard → 9/9 passed
  • npm run test:proxies → 30/30 passed
  • python3 scripts/scan_bundle_for_leaks.py → No leaks

What I could NOT verify (honest gaps)

  • CI on GitHub itself: I ran the full sequence locally on the exact pushed commit; GitHub Actions will run on this PR but I have not observed its result. Not a blocker for the change, but confirmation should come from the Actions run on the PR.
  • Visual fidelity vs the canonical hero is qualitative — this is source+behavior review; reconciling each beat to the hero's pixels needs human-eyeball, not automated here (the hero is a static design file, not served by the app).
  • Full nine-viewport matrix is owned by final acceptance; I validated the required 360×800 and 1440×900 slices only.
  • Reduced-motion stop is enforced in two layers (change-handler 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.

…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.
@sgardoll
sgardoll marked this pull request as ready for review September 22, 2026 18:46

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

Comment thread src/logoMotion.js
Comment on lines +362 to +368
const observer = new IntersectionObserver(
(entries) => {
intersecting = entries[0] ? entries[0].isIntersecting : false;
sync();
},
{ threshold: 0 }
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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 }
);

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/logoMotion.js
document.removeEventListener("visibilitychange", onVisibility);
}

window.addEventListener("pagehide", dispose);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

Fix the hidden-layout measurement before merging because normal pipeline runs lose the authored cross-arena motion.

Fix All in DevinFindings

  1. P1 Hidden initialization prevents logo travel ▶
  2. P2 Split-piece motion gets clipped ▶
  3. P2 Cached-page return freezes the logo ▶
Fix with agent prompt
### Issue 1
src/logoMotion.js:362-365
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.

### Issue 2
src/logoMotion.css:23-28
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.

### Issue 3
src/logoMotion.js:385
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.

Summary

Adds an isolated decorative pipeline-logo controller, responsive arena styles, two-piece SVG markup, pose unit tests, lifecycle/end-to-end tests, and screenshot artifacts.

Greptile automatically discovered a related ticket that helped explain the purpose of this PR: preserve the authored twelve-beat, 120-second animation independently of backend pipeline completion.

  • Pipeline completion remains independent of the animation.
  • Hidden initialization leaves travel dimensions at zero; the arena also clips split-piece motion, and cached-page restoration permanently disables the controller.
  • All six logo-motion unit tests passed. A focused controller check confirmed the stale dimensions after reveal.
  • Screenshot images could not be inspected because image input is unsupported in this review environment; browser visual validation was not performed.

Reviews (1) · Last reviewed commit: "feat(STU-390): port authored logo animat..."

Comment thread src/logoMotion.js
Comment on lines +362 to +365
const observer = new IntersectionObserver(
(entries) => {
intersecting = entries[0] ? entries[0].isIntersecting : false;
sync();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Claude Code Fix in Codex Fix in Devin

Comment thread src/logoMotion.css
Comment on lines +23 to +28
.loop-mark {
left: 0;
top: 50%;
width: 52px;
height: 52px;
margin: -26px 0 0 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Claude Code Fix in Codex Fix in Devin

Comment thread src/logoMotion.js
document.removeEventListener("visibilitychange", onVisibility);
}

window.addEventListener("pagehide", dispose);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Claude Code Fix in Codex Fix in Devin

@sgardoll
sgardoll merged commit 2c83518 into milestone/stu-implement-updated-ui-ux Sep 22, 2026
3 of 4 checks passed
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