From d25743a70f47dcbb4220fe9b98692ae1bd2eac28 Mon Sep 17 00:00:00 2001 From: Pat Robinson Date: Sat, 25 Jul 2026 12:26:12 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20desktop=20scenes=20can=20render=20black?= =?UTF-8?q?=20=E2=80=94=20reveal=20clip=20only=20after=20a=20painted=20fra?= =?UTF-8?q?me,=20prime=20on=20all=20platforms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On desktop the engine swapped the still poster for the video on the first 'seeked' event and never primed playback. A muted video that has never played does not reliably paint a frame on seek (the same quirk the mobile path already guards against), so desktop visitors could get a black stage while the scrub logic ran on happily underneath. - reveal .has-clip via requestVideoFrameCallback (an actually painted frame) when available, falling back to 'seeked' where rVFC is missing - primeVideo() no longer bails on desktop; loadeddata primes immediately on fine-pointer devices (muted autoplay is permitted there), keeping the existing first-gesture priming on touch --- skills/scroll-world/references/scrub-engine.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skills/scroll-world/references/scrub-engine.js b/skills/scroll-world/references/scrub-engine.js index d7d4c3b..88f6661 100644 --- a/skills/scroll-world/references/scrub-engine.js +++ b/skills/scroll-world/references/scrub-engine.js @@ -213,8 +213,9 @@ function mountScrollWorld(container, config) { // Reveal the video (hide the still poster) only once a real frame has // painted — on iOS a seeked-but-never-played muted video stays blank, so // hiding the still on metadata alone would flash an empty scene. - v.addEventListener('seeked', () => { s.el.classList.add('has-clip'); }, { once: true }); - v.addEventListener('loadeddata', () => { try { v.pause(); } catch (e) {} if (userReady) primeVideo(v); }); + if (v.requestVideoFrameCallback) { v.requestVideoFrameCallback(() => { s.el.classList.add('has-clip'); }); } + else { v.addEventListener('seeked', () => { s.el.classList.add('has-clip'); }, { once: true }); } + v.addEventListener('loadeddata', () => { try { v.pause(); } catch (e) {} if (userReady || !isMobile()) primeVideo(v); }); s.el.appendChild(v); s.video = v; s.hasClip = true; }).catch(() => { s.loading = false; }); } @@ -294,7 +295,7 @@ function mountScrollWorld(container, config) { // clips prime themselves (see loadClip). let userReady = false; function primeVideo(v) { - if (!isMobile() || !v) return; + if (!v) return; try { const p = v.play(); if (p && p.then) p.then(() => { try { v.pause(); } catch (e) {} }).catch(() => {}); } catch (e) {} }