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; }