From 3fb687a539059cff654949dd7bf05d4b37b5ff3d Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Sun, 17 May 2026 21:49:35 -0700 Subject: [PATCH] perf(wc): pause marquee animation when slide is off-screen The marquee on title and outro slides ran morkdeck-marquee-scroll 48s linear infinite on every render, keeping a compositing layer alive for slides scrolled out of view, slides in overview thumbnails, and off-slot slides in presenter mode. Transform animations are cheap but not free. Tie the run state to slide visibility through the existing IntersectionObserver. fireUpdates now toggles an is-in-view class on each entry's target before the role and overview gating, so the class reflects pure visibility independent of navigation logic. The CSS responds with animation-play-state: paused on morkdeck-slide:not(.is-in-view) .marquee-track. In audience mode, only the currently-snapped slide's marquee runs; in presenter mode the current and next previews both run; in overview every thumbnail's marquee is paused. Verified live: title in view shows running, outro off-screen shows paused, and the reverse after scrolling. Co-Authored-By: Claude Opus 4.7 --- packages/core/templates/partials/slide-styles.eta | 9 +++++++++ packages/wc/components/presentation/wc.ts | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/core/templates/partials/slide-styles.eta b/packages/core/templates/partials/slide-styles.eta index 2d1f973..b12ea78 100644 --- a/packages/core/templates/partials/slide-styles.eta +++ b/packages/core/templates/partials/slide-styles.eta @@ -660,6 +660,15 @@ animation: morkdeck-marquee-scroll 48s linear infinite; } + /* Pause the marquee whenever its slide is not the in-view slide. + The presentation toggles `.is-in-view` from the existing + IntersectionObserver, so overview thumbnails, off-slot slides in + presenter mode, and slides scrolled past in audience mode all + drop their compositing layer until they return to view. */ + morkdeck-slide:not(.is-in-view) .marquee-track { + animation-play-state: paused; + } + morkdeck-slide .marquee-item, morkdeck-slide .marquee-sep { font-family: var(--morkdeck-fontFamily-mono); diff --git a/packages/wc/components/presentation/wc.ts b/packages/wc/components/presentation/wc.ts index 949c696..35e227e 100644 --- a/packages/wc/components/presentation/wc.ts +++ b/packages/wc/components/presentation/wc.ts @@ -965,6 +965,15 @@ export class PresentationWC extends LitElement { } fireUpdates(entries: IntersectionObserverEntry[]) { + // Visibility-class toggle first, before any role/overview gating — + // this is read by the marquee CSS to pause its animation on slides + // that aren't currently in view (overview thumbnails, off-slot + // slides in presenter mode, slides scrolled out of the audience + // viewport). Driving it from the existing observer is essentially + // free; the alternative was a second IO per slide. + for (const entry of entries) { + entry.target.classList.toggle("is-in-view", entry.isIntersecting); + } // IO drives audience-mode scroll navigation only. if (this.role !== "audience") return; // Overview shows many slides at once in a grid; whichever happens -- 2.51.2