// s06 Programs, not video. The statement holds mid-stage, then rises. A three-step pipeline then // builds left to right on its cues: "Code" (`window.seek(t)` types on, 1 frame a letter), "Browser" // (a real frame of the walkthrough clip), and on "freezes" a strip of 5 real frames, 50 ms apart. // "Video tool" arrives, and the hero move: on "joins" the 5 frames slide together (spring ζ 0.82) // into one clip with a play bar, and the video tool plays it. Then three benefits, one row per cue, // each with a small proof: repeatable (the same frame twice, "="), cheap edits (roughness 0.35 → // 0.30, and the frame's highlight tightens), self-checking (a magnifier over a frame). export default async function build(root, k) { const { make, place, show, cueIn, spring01, easeOut, easeInOut, p01, swap, style, frames, check, sfx, box, overlap, clamp, lerp, C } = k; const { cutIn, cutOut } = k.win; const FPS = 60; const tCode = cueIn(k.id, "code"); const tBrowser = cueIn(k.id, "browser"); const tFreeze = cueIn(k.id, "freezes"); const tVideo = cueIn(k.id, "video tool"); const tJoin = cueIn(k.id, "joins"); const tRep = cueIn(k.id, "every frame"); const tEdit = cueIn(k.id, "edits"); const tCheap = cueIn(k.id, "cheap"); const tCheck = cueIn(k.id, "can check"); const CLIP = "clips/walkthrough"; const clipInfo = await (await fetch(`${CLIP}/clip.json`)).json(); const src = (n) => `${CLIP}/${String(n).padStart(6, "0")}.jpg`; const imgs = []; const img = (parent, n, css = {}) => { const el = make("img", "", "", parent, { position: "absolute", left: "0px", top: "0px", width: "100%", height: "100%", objectFit: "cover", ...css }); el.src = src(n); imgs.push(el); return el; }; // ---- the statement (the opening picture) ---- // It opens mid-stage, then rises out of the pipeline's way (ease-in-out, 0.8 s) just before "Code". const HEAD_Y0 = 400, HEAD_Y1 = 26, RISE = 0.8, tRise = tCode - 0.1 - RISE; const head = place(make("div", "h1", "", root, { whiteSpace: "nowrap" }), 80, HEAD_Y0); head.innerHTML = `Programs, not video`; // ---- the pipeline: three cards, left to right ---- const pipe = make("div", "", "", root, { position: "absolute", left: "0px", top: "0px", width: "1080px", height: "992px" }); const COLS = [80, 400, 720], CW = 280, CY = 128, CH = 232; const BX = 24, BY = 80, BW = 232, BH = 130.5; // each card's body, 16:9 const card = (i, name) => { const c = place(make("div", "card", "", pipe), COLS[i], CY, CW, CH); make("span", "mono", String(i + 1), c, { position: "absolute", left: "24px", top: "15px", fontSize: "28px", color: C.muted }); make("span", "", name, c, { position: "absolute", left: "58px", top: "12px", fontSize: "32px", fontWeight: "650", letterSpacing: "-0.01em", whiteSpace: "nowrap" }); make("div", "", "", c, { position: "absolute", left: "0px", right: "0px", top: "62px", height: "2px", background: C.line }); return c; }; const body = (c, css = {}) => make("div", "", "", c, { position: "absolute", left: `${BX}px`, top: `${BY}px`, width: `${BW}px`, height: `${BH}px`, borderRadius: "10px", overflow: "hidden", ...css }); // 1 Code: `window.seek(t)`, every letter laid out in advance and shown 1 frame apart. const codeCard = card(0, "Code"); const codeLine = make("div", "mono", "", codeCard, { position: "absolute", left: `${BX}px`, top: "96px", fontSize: "28px", fontWeight: "520", letterSpacing: "-0.02em", whiteSpace: "pre" }); const CODE = [["window.", C.muted], ["seek", C.accent], ["(t)", C.ink]]; const chars = []; for (const [text, color] of CODE) for (const ch of text) chars.push(make("span", "", ch, codeLine, { color, visibility: "hidden" })); const codeSub = make("div", "", "draws frame t", codeCard, { position: "absolute", left: `${BX}px`, top: "152px", fontSize: "28px", color: C.muted }); const typedAt = (i) => tCode + 0.3 + i / FPS; // 2 Browser: a real frame of the walkthrough clip in the body. const browserCard = card(1, "Browser"); const STRIP = [1, 37, 73, 109, 145]; // evenly spaced through the 6 s clip img(body(browserCard, { background: C.line }), STRIP[2]); // 3 Video tool: a dark player with a play mark; after the join it shows the clip. const videoCard = card(2, "Video tool"); const player = body(videoCard, { background: C.ink }); const playMark = make("div", "", ``, player, { position: "absolute", left: `${BW / 2 - 22}px`, top: `${BH / 2 - 22}px` }); const playerImg = make("img", "", "", player, { position: "absolute", left: "0px", top: "0px", width: "100%", height: "100%", objectFit: "cover", visibility: "hidden" }); const clip = frames(playerImg, { dir: CLIP, ...clipInfo }); const arrows = [0, 1].map((i) => make("div", "", ``, pipe, { position: "absolute", left: `${COLS[i] + CW + 6}px`, top: `${CY + CH / 2 - 12}px` })); // ---- the strip: 5 frozen frames, then one clip with a play bar ---- const FW = 150, FH = 84.375, SY = 412; const spreadX = (i) => 80 + i * 192.5; // 920 px wide with 42.5 px gaps const joinedX = (i) => 165 + i * FW; // 750 px, centred, no gaps const outline = place(make("div", "", "", pipe, { border: `4px solid ${C.ink}`, borderRadius: "12px" }), joinedX(0) - 6, SY - 6, 5 * FW + 12, FH + 12); const stills = STRIP.map((n, i) => { const f = place(make("div", "", "", pipe, { overflow: "hidden", background: C.line }), spreadX(i), SY, FW, FH); img(f, n); f.dataset.track = `s06-still-${i}`; return f; }); const STAGGER = 0.05; const stillAt = (i) => tFreeze + 0.1 + i * STAGGER; const JOIN = { duration: 1.2, damping: 0.82 }; const join = (t) => spring01(t, tJoin, JOIN); // The frame where the join has settled: every still within 0.5 px of its mark from then on. let joinSettle = tJoin; for (let n = 1; n < 120; n++) if (Math.abs(1 - join(tJoin + n / FPS)) * 85 > 0.5) joinSettle = tJoin + (n + 1) / FPS; let touch = tJoin; // the frames first meet (spring crosses 1) for (let n = 1; n < 120; n++) if (join(tJoin + n / FPS) >= 1) { touch = tJoin + n / FPS; break; } // The play bar: an accent playhead over the clip; the video tool plays in step with it. const playhead = make("div", "", "", pipe, { position: "absolute", top: `${SY - 22}px`, width: "4px", height: `${FH + 34}px`, marginLeft: "-2px", background: C.accent, borderRadius: "2px" }); make("div", "", "", playhead, { position: "absolute", left: "-7px", top: "-8px", width: "18px", height: "18px", borderRadius: "9px", background: C.accent }); // Once the join has stopped moving (speed under 0.5% of its range, f45 on the speed graph), the // playhead appears and the clip plays at 1x; it pauses as the pipeline steps back for the benefits. const tStill = tJoin + 0.75, tPlay = tStill + 0.15, tStop = tRep + 0.4; const clipT = (t) => clamp(t - tPlay, 0, tStop - tPlay); // ---- three benefits, one row each ---- const ROWS = [552, 684, 816], RH = 120, PX = 360, TW = 168, TH = 94.5; const chip = (txt, y) => { const c = place(make("div", "chip accent", txt, root, { fontSize: "30px", padding: "10px 22px" }), 80, y + 32); return c; }; const thumb = (n, x, y) => { const f = place(make("div", "", "", root, { overflow: "hidden", borderRadius: "8px", background: C.line, boxShadow: "0 0 0 2px #d8d6d0, 0 8px 22px rgba(28,28,26,0.12)" }), x, y, TW, TH); if (n) img(f, n); return f; }; // Repeatable: the same frame rendered twice. const repChip = chip("repeatable", ROWS[0]); const repA = thumb(109, PX, ROWS[0] + 12); const repEq = place(make("div", "mono", "=", root, { fontSize: "52px", fontWeight: "500", width: "72px", textAlign: "center", lineHeight: "1" }), PX + TW, ROWS[0] + 34); const repB = thumb(109, PX + TW + 72, ROWS[0] + 12); // Cheap edits: one number changes and the frame re-renders; lower roughness, tighter highlight. const editChip = chip("cheap edits", ROWS[1]); const editFrame = thumb(0, PX, ROWS[1] + 12); editFrame.style.background = "#f6f5f2"; const disc = make("div", "", "", editFrame, { position: "absolute", left: `${TW / 2 - 34}px`, top: `${TH / 2 - 34}px`, width: "68px", height: "68px", borderRadius: "34px", background: "#e2ded6", boxShadow: "inset -10px -12px 18px rgba(28,28,26,0.16), 0 10px 14px -8px rgba(28,28,26,0.35)", overflow: "hidden" }); const glint = make("div", "", "", disc, { position: "absolute", borderRadius: "50%", background: "#ffffff" }); const editText = place(make("div", "", "", root, { width: "300px" }), PX + TW + 40, ROWS[1] + 8); make("div", "", "roughness", editText, { fontSize: "28px", color: C.muted }); const valBox = make("div", "mono", "", editText, { position: "relative", height: "56px", fontSize: "46px", fontWeight: "560" }); const valOld = make("span", "", "0.35", valBox, { position: "absolute", left: "0px", top: "0px" }); const valNew = make("span", "", "0.30", valBox, { position: "absolute", left: "0px", top: "0px", color: C.accent }); // Self-checking: a magnifier slides over a frame and reads the field the pointer just typed in. const checkChip = chip("self-checking", ROWS[2]); const checkFrame = thumb(145, PX, ROWS[2] + 12); const R = 52, Z = 2.4; const lens = make("div", "", "", root, { position: "absolute", width: `${2 * R}px`, height: `${2 * R}px` }); const lensView = make("div", "", "", lens, { position: "absolute", inset: "0px", borderRadius: "50%", overflow: "hidden", background: C.canvas, boxShadow: "0 10px 26px rgba(28,28,26,0.18)" }); const lensImg = make("img", "", "", lensView, { position: "absolute", width: `${TW * Z}px`, height: `${TH * Z}px` }); lensImg.src = src(145); imgs.push(lensImg); make("div", "", ``, lens, { position: "absolute", left: "0px", top: "0px" }); lens.dataset.track = "s06-lens"; // The email field in frame 145 sits at about (85, 28) of the 168 x 94.5 thumbnail. const LENS_FROM = [PX + TW + 150, ROWS[2] + 40], LENS_TO = [PX + 85, ROWS[2] + 12 + 28]; const tLens = tCheck + 0.25; await Promise.all(imgs.map((i) => i.decode().catch(() => {}))); sfx(touch + 1 / FPS, "tap"); // the frames meet sfx(tCheap + 0.14 + 0.04 + 1 / FPS, "toggle"); // 0.30 arrives // ---- checks ---- const inside = (el) => { const b = box(el); return b.x >= 0 && b.x + b.w <= 1080 && b.y >= 128 && b.y + b.h <= 1120; }; check("s06 the 5 frames freeze 50 ms apart, left to right", [tFreeze, tFreeze + 0.5], (t) => { const bad = stills.findIndex((f, i) => (Number(f.style.opacity) > 0) !== (t > stillAt(i))); return bad < 0 || `still ${bad} is ${Number(stills[bad].style.opacity) > 0 ? "early" : "late"}`; }); check("s06 the join settles on its marks by f" + Math.round((joinSettle - tJoin) * FPS), [joinSettle, cutOut - 1 / FPS], () => { const bad = stills.findIndex((f, i) => Math.abs(box(f).x - joinedX(i)) > 0.5); return bad < 0 || `still ${bad} at x ${box(stills[bad]).x.toFixed(2)} (want ${joinedX(bad)})`; }); check("s06 the join is the only move: the stills hold still before it", [stillAt(4) + 0.5, tJoin], () => stills.every((f, i) => Math.abs(box(f).x - spreadX(i)) < 0.01) || "a still moved early"); check("s06 the playhead marks the clip time shown in the video tool", [tPlay, tStop], (t) => { const want = joinedX(0) + (5 * FW * clipT(t)) / (clipInfo.count / clipInfo.fps); const x = box(playhead).cx; return Math.abs(x - want) < 1 || `playhead ${x.toFixed(1)} want ${want.toFixed(1)}`; }); check("s06 the statement has risen before the code card arrives", tCode, () => Math.abs(box(head).y - 128 - HEAD_Y1) < 0.5 || `head at ${(box(head).y - 128).toFixed(1)}`); check("s06 the code line fits its card", tCode + 1, () => { const a = box(codeLine), c = box(codeCard); return a.x + a.w <= c.x + c.w - 16 || `line ends ${(a.x + a.w).toFixed(1)}, card ${(c.x + c.w).toFixed(1)}`; }); check("s06 the edit lands: 0.30 replaces 0.35", tCheap + 0.6, () => (Number(valNew.style.opacity) > 0.99 && Number(valOld.style.opacity) < 0.01) || "value"); check("s06 the last benefit holds ≥ 1.83 s before the cut", cutOut - 1 / FPS, () => cutOut - tCheck >= 1.83 || `hold ${(cutOut - tCheck).toFixed(2)} s`); check("s06 rows, chips and the pipeline never overlap", [cutIn, cutOut - 1 / FPS], () => { const els = [head, codeCard, browserCard, videoCard, outline, repChip, repA, repB, editChip, editFrame, editText, checkChip, checkFrame]; for (let i = 0; i < els.length; i++) for (let j = i + 1; j < els.length; j++) { const o = overlap(els[i], els[j]); if (o > 0) return `${i} x ${j}: ${o | 0} px²`; } return true; }); check("s06 everything stays inside the stage", [cutIn, cutOut - 1 / FPS], () => [head, outline, ...stills, repB, editText, checkFrame, lens].every(inside) || "outside"); return (t) => { show(head, t, cutIn - 1); head.style.top = `${lerp(HEAD_Y0, HEAD_Y1, easeInOut(p01(t, tRise, RISE))).toFixed(2)}px`; // The pipeline steps back to 50% when the benefits begin. pipe.style.opacity = (1 - 0.5 * easeOut(p01(t, tRep, 0.5))).toFixed(4); show(codeCard, t, tCode, Infinity, { dur: 0.5, dy: 18 }); chars.forEach((s, i) => { s.style.visibility = t >= typedAt(i) ? "visible" : "hidden"; }); show(codeSub, t, typedAt(chars.length) + 0.1, Infinity, { dur: 0.4, dy: 0 }); show(arrows[0], t, tBrowser - 0.1, Infinity, { dur: 0.4, dy: 0, dx: -10 }); show(browserCard, t, tBrowser, Infinity, { dur: 0.5, dy: 18 }); show(arrows[1], t, tVideo - 0.1, Infinity, { dur: 0.4, dy: 0, dx: -10 }); show(videoCard, t, tVideo, Infinity, { dur: 0.5, dy: 18 }); // Freeze: the stills drop in, 50 ms apart. Join: they slide together on one spring. const j = join(t); stills.forEach((f, i) => { show(f, t, stillAt(i), Infinity, { dur: 0.4, dy: -16 }); f.style.left = `${lerp(spreadX(i), joinedX(i), j).toFixed(2)}px`; const r = 8 * (1 - clamp(j)); f.style.borderRadius = `${r.toFixed(2)}px`; // A thin ring keeps each still apart from the canvas (the clip's own background matches it). const u = (1 - clamp(j)).toFixed(3); f.style.boxShadow = `0 0 0 2px rgba(168,166,160,${u}), 0 8px 22px rgba(28,28,26,${(0.12 * (1 - clamp(j))).toFixed(3)})`; }); show(outline, t, touch, Infinity, { dur: 0.35, dy: 0 }); show(playhead, t, tStill, Infinity, { dur: 0.3, dy: 0 }); const ct = clipT(t); playhead.style.left = `${(joinedX(0) + (5 * FW * ct) / (clipInfo.count / clipInfo.fps)).toFixed(2)}px`; const playing = t >= tPlay; playerImg.style.visibility = playing ? "visible" : "hidden"; playMark.style.visibility = playing ? "hidden" : "visible"; // Benefits, one row per cue: the chip, then its proof 150 ms later. show(repChip, t, tRep, Infinity, { dur: 0.45, dx: -16, dy: 0 }); show(repA, t, tRep + 0.15, Infinity, { dur: 0.45, dy: 10 }); show(repEq, t, tRep + 0.25, Infinity, { dur: 0.4, dy: 0 }); show(repB, t, tRep + 0.31, Infinity, { dur: 0.45, dy: 10 }); show(editChip, t, tEdit, Infinity, { dur: 0.45, dx: -16, dy: 0 }); show(editFrame, t, tEdit + 0.15, Infinity, { dur: 0.45, dy: 10 }); show(editText, t, tEdit + 0.21, Infinity, { dur: 0.45, dy: 10 }); const sw = swap(t, tCheap); style(valOld, sw.out); style(valNew, sw.in); // The highlight re-renders with the new number: smaller and sharper at roughness 0.30. const q = easeOut(p01(t, tCheap + 0.18, 0.25)); const g = lerp(30, 17, q), gb = lerp(7, 2.2, q); Object.assign(glint.style, { left: `${(20 - g / 2).toFixed(2)}px`, top: `${(19 - g / 2).toFixed(2)}px`, width: `${g.toFixed(2)}px`, height: `${g.toFixed(2)}px`, filter: `blur(${gb.toFixed(2)}px)` }); show(checkChip, t, tCheck, Infinity, { dur: 0.45, dx: -16, dy: 0 }); show(checkFrame, t, tCheck + 0.15, Infinity, { dur: 0.45, dy: 10 }); // The lens glides in on an arc (x leads, y follows on a slower spring) and settles on the field. const lx = lerp(LENS_FROM[0], LENS_TO[0], spring01(t, tLens, { duration: 0.8, damping: 0.82 })); const ly = lerp(LENS_FROM[1], LENS_TO[1], spring01(t, tLens, { duration: 0.95, damping: 0.9 })); lens.style.left = `${(lx - R).toFixed(2)}px`; lens.style.top = `${(ly - R).toFixed(2)}px`; show(lens, t, tLens - 0.05, Infinity, { dur: 0.35, dy: 0 }); lensImg.style.left = `${(R - (lx - PX) * Z).toFixed(2)}px`; lensImg.style.top = `${(R - (ly - ROWS[2] - 12) * Z).toFixed(2)}px`; return clip(ct); }; }