From ae072f35ac89268440f6e21eab352bb4cde5a870 Mon Sep 17 00:00:00 2001 From: srivtx Date: Thu, 22 Oct 2026 18:02:06 +0530 Subject: [PATCH] Website: keep the deck at 60 fps while every card plays Chrome drops the whole page to 30 fps while two or more videos play on screen, and the fan played six, so scrolling and hovering felt laggy whenever the deck was in view. Each scene now plays in a video that never goes on the page, and the cards paint its frames onto canvases in one frame loop. Every card still plays; the deck measures 61 fps instead of 31. Bump the asset versions so browsers fetch the new deck.js and styles.css. Co-Authored-By: Claude Opus 5.5 --- website/deck.js | 70 ++++++++++++++++++++++++++++++++++++++------- website/index.html | 4 +-- website/main.js | 6 ++-- website/styles.css | 6 ++-- website/timeline.js | 4 +-- 5 files changed, 70 insertions(+), 20 deletions(-) diff --git a/website/deck.js b/website/deck.js index c7067ae..f02a047 100644 --- a/website/deck.js +++ b/website/deck.js @@ -1,7 +1,7 @@ // The poster deck: one collectible card per new animated poster, fanned out. -// Cards tilt toward the pointer under a foil, play their loop on hover (one at -// a time), and open in an inspector where they turn over to show their timeline. -import { DECK, SHOWCASES } from './showcases.js?v=3'; +// Cards tilt toward the pointer under a foil, play their loops while on screen, +// and open in an inspector where they turn over to show their timeline. +import { DECK, SHOWCASES } from './showcases.js?v=4'; const CARDS = DECK.map((id) => SHOWCASES.find((s) => s.id === id)); @@ -62,7 +62,7 @@ function front(s, i) { return `
${esc(s.title)}No.${String(i + 1).padStart(2, '0')}/${String(CARDS.length).padStart(2, '0')}
-
+
${esc(s.material)}${s.duration.toFixed(1)} s ยท ${s.layers} layers
    ${stats}

${esc(s.flavor)}

@@ -165,15 +165,65 @@ function tilter(el, range) { }; } +/* ---------- Card screens ---------- */ + +// Chrome drops the whole page to 30 fps while two or more videos play on +// screen, and the fan plays six. So each scene plays in a video that never goes +// on the page, and its cards paint the frames onto canvases in one frame loop. +const feeds = new Map(); +let painting = 0; + +function feed(id) { + let f = feeds.get(id); + if (!f) { + const video = document.createElement('video'); + video.muted = true; + video.loop = true; + video.playsInline = true; + video.src = `/media/${id}.mp4`; + f = { video, screens: new Set(), at: -1 }; + feeds.set(id, f); + } + return f; +} + +function paint() { + painting = 0; + let live = false; + for (const f of feeds.values()) { + if (!f.screens.size) continue; + live = true; + // Only when the video has moved on to a new frame. + if (f.video.readyState < 2 || f.video.currentTime === f.at) continue; + f.at = f.video.currentTime; + for (const c of f.screens) { + c.getContext('2d').drawImage(f.video, 0, 0, c.width, c.height); + c.classList.add('playing'); + } + } + if (live) painting = requestAnimationFrame(paint); +} + function playIn(card, on) { - const v = $('video', card); - if (!v) return; + const c = $('canvas[data-feed]', card); + if (!c) return; if (on && !reduced) { - if (!v.src) v.src = v.dataset.src; - v.onplaying = () => v.classList.add('playing'); - v.play().catch(() => {}); + if (!c.dataset.sized) { + c.dataset.sized = '1'; + const w = Math.min(1080, Math.round((c.clientWidth || 360) * Math.min(devicePixelRatio || 1, 2))); + c.width = w; + c.height = Math.round(w / 1.5); + } + const f = feed(c.dataset.feed); + f.screens.add(c); + f.at = -1; + f.video.play().catch(() => {}); + painting ||= requestAnimationFrame(paint); } else { - v.pause(); + const f = feeds.get(c.dataset.feed); + if (!f) return; + f.screens.delete(c); + if (!f.screens.size) f.video.pause(); } } diff --git a/website/index.html b/website/index.html index e7ef057..bdd382a 100644 --- a/website/index.html +++ b/website/index.html @@ -15,8 +15,8 @@ - - + +