From 6f6753960feea27c9db1e5bfd8cbead39c5cd4a7 Mon Sep 17 00:00:00 2001 From: Christian Dassy <18606875+inxbit@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:49:53 -0500 Subject: [PATCH] Keep the hero prism when the footage paints nothing, and draw its light crisp The hero hid the still prism plate on the video's 'playing' event. A browser can run the clock and present no frames: the owner's Safari 27 did that for every clip (121 of 121 frames dropped, requestVideoFrameCallback never fired), so the prism slot went empty while the beam and dust still came on. The plate now steps aside only once requestVideoFrameCallback reports a frame and the footage is unhidden, whichever comes last, and settle() drops footage that never produced a frame in favor of the plate. A frame can be reported for the hidden element (Firefox on load, any engine while paused off screen), hence the hidden check. Browsers without the API keep the old swap on 'playing'. The light layer is drawn crisp: the flow band hugs the beam line (2u core, tilted 1.29deg to its fall) instead of a 12u soft strip, and dust motes are whole-device-pixel points instead of sub-pixel discs. --- DESIGN.md | 2 +- docs/site.css | 14 +++++++++----- docs/site.js | 34 +++++++++++++++++++++++++++------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 61a289d..ad65544 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -265,7 +265,7 @@ Logo variant C (the prism mark with a terminal inside) at 60u plus the PrismTTY Three facts on the hero's bottom edge, each a 44u stroke icon (2px, currentColor, custom SVG) beside a title and body, divided by vertical hairlines; stacked with horizontal hairlines on phones. ### The Seam (signature) -Raw output left, the prism in the middle, the same output through PrismTTY right, captioned "Real PrismTTY output, cisco profile." The still prism plate is the default. With motion allowed, the approved Higgsfield image-to-video take (take 1 of the approved prism) plays once, eases per frame to a stop on the wide spectrum, and the light layer fades in over 1.6s (cubic-bezier(0.16, 1, 0.3, 1)): a steady white beam flowing left to right (1.5s linear loop, screen-blended) and canvas dust that only shows inside the beam and the spectrum fan. On load each raw line flashes as it passes into the prism and its highlighted twin wipes in 0.32s later, 0.18s apart. Reduced motion, data saver, a video error or a stall keep the still plate. The light layer is hidden below 1280 px. The hero light plate and crystals drift slowly (26s and 34s, alternate) when motion is allowed. +Raw output left, the prism in the middle, the same output through PrismTTY right, captioned "Real PrismTTY output, cisco profile." The still prism plate is the default. With motion allowed, the approved Higgsfield image-to-video take (take 1 of the approved prism) plays once, eases per frame to a stop on the wide spectrum, and the light layer fades in over 1.6s (cubic-bezier(0.16, 1, 0.3, 1)): a steady white beam flowing left to right (1.5s linear loop, screen-blended) in a 4u band tilted 1.29° onto the beam line, and canvas dust drawn as whole-device-pixel points that only shows inside the beam and the spectrum fan. On load each raw line flashes as it passes into the prism and its highlighted twin wipes in 0.32s later, 0.18s apart. Reduced motion, data saver, a video error, a stall or footage that never produces a frame keep the still plate: where the browser has requestVideoFrameCallback, the plate steps aside only once it reports a frame and the footage is unhidden (without that API, on playing). The light layer is hidden below 1280 px. The hero light plate and crystals drift slowly (26s and 34s, alternate) when motion is allowed. ## Do's and Don'ts diff --git a/docs/site.css b/docs/site.css index 0558a01..3646ea4 100644 --- a/docs/site.css +++ b/docs/site.css @@ -458,24 +458,28 @@ a { opacity: 1; } +/* The flow band hugs the beam line of the plate and the footage: a 2u core + with 1u edges, tilted 1.29deg to follow the beam's fall. */ .beam { position: absolute; left: calc(500 * var(--u)); - top: calc(504 * var(--u)); + top: calc(506 * var(--u)); width: calc(246 * var(--u)); - height: calc(12 * var(--u)); + height: calc(4 * var(--u)); background: repeating-linear-gradient( 90deg, transparent 0, transparent calc(34 * var(--u)), - rgb(255 255 255 / 0.5) calc(62 * var(--u)), + rgb(255 255 255 / 0.75) calc(62 * var(--u)), transparent calc(90 * var(--u)) ); background-size: calc(90 * var(--u)) 100%; - -webkit-mask-image: linear-gradient(180deg, transparent, #000 42%, #000 58%, transparent), + transform-origin: 0 50%; + transform: rotate(1.29deg); + -webkit-mask-image: linear-gradient(180deg, transparent, #000 25%, #000 75%, transparent), linear-gradient(90deg, transparent, #000 22%, #000 88%, transparent); -webkit-mask-composite: source-in; - mask-image: linear-gradient(180deg, transparent, #000 42%, #000 58%, transparent), + mask-image: linear-gradient(180deg, transparent, #000 25%, #000 75%, transparent), linear-gradient(90deg, transparent, #000 22%, #000 88%, transparent); mask-composite: intersect; mix-blend-mode: screen; diff --git a/docs/site.js b/docs/site.js index 22955da..d2240e8 100644 --- a/docs/site.js +++ b/docs/site.js @@ -89,8 +89,14 @@ let settled = Boolean(navigator.connection?.saveData); // nothing left to play let easing = 0; // animation frame of the ease let stall = 0; // timer that gives up on footage that stopped arriving + // Whether the footage has produced a frame. A browser can run the clock + // and drop every frame (Safari 27 did, for any clip), so the plate stays + // until requestVideoFrameCallback reports one. Without that API, 'playing' + // is taken at its word. + let shown = !(video && 'requestVideoFrameCallback' in video); // The intro is over: hold the frame it stopped on and bring in the light. + // Footage that never produced a frame gives way to the still plate. function settle() { settled = true; clearTimeout(stall); @@ -98,13 +104,16 @@ easing = 0; video.pause(); root.classList.add('prism-live'); + if (!shown) { + video.hidden = true; + root.classList.remove('prism-playing'); + } } // The footage broke: drop it and fall back to the still plate. function giveUp() { + shown = false; settle(); - video.hidden = true; - root.classList.remove('prism-playing'); } // One step per frame near the end. playbackRate = sqrt(left / EASE) is a @@ -145,8 +154,19 @@ } clearTimeout(stall); video.hidden = false; - root.classList.add('prism-playing'); + if (shown) root.classList.add('prism-playing'); }); + // The footage sits above the plate and paints nothing until it has a + // frame. The plate steps aside once a frame is reported and the footage + // is unhidden, whichever comes last: a frame can be reported for the + // hidden element (Firefox on load, any engine while paused off screen). + if (!shown) { + video.requestVideoFrameCallback(() => { + if (settled) return; + shown = true; + if (!video.hidden) root.classList.add('prism-playing'); + }); + } // timeupdate (about 4 Hz) only opens the window; the frames do the ease. video.addEventListener('timeupdate', () => { const left = video.duration - video.currentTime; @@ -179,7 +199,7 @@ motes = Array.from({ length: 34 }, (_, i) => ({ x: ((i * 0.6180339887) % 1) * canvas.width, y: (0.08 + 0.84 * ((i * 0.3819660113) % 1)) * canvas.height, - r: (0.5 + ((i * 7) % 5) * 0.22) * scale, + s: Math.max(1, Math.round((0.5 + ((i * 7) % 5) * 0.22) * scale)), // side, whole device px v: (0.1 + ((i * 3) % 7) * 0.025) * scale, })); } @@ -207,9 +227,9 @@ const fade = Math.min(1, Math.min(m.x, w - m.x) / (w * 0.08)); const lit = 0.06 + 0.8 * light(m.x / w, y / h); ctx.globalAlpha = lit * fade; - ctx.beginPath(); - ctx.arc(m.x, y, m.r, 0, Math.PI * 2); - ctx.fill(); + // Snapped to whole device pixels, each mote is a crisp point rather + // than a sub-pixel smear. + ctx.fillRect(Math.round(m.x - m.s / 2), Math.round(y - m.s / 2), m.s, m.s); } frame = visible && drawable ? requestAnimationFrame(draw) : 0; }