Files
open-design/plugins/_official/examples/motion-future/example.html
341641d71a [OPEND-3962] fix(motion): 修复预览声音与模板配乐约束 (#8777)
* fix(motion): avoid audio seek thrashing during preview stalls

* fix(motion): keep in-progress video previews silent

* fix(motion): let user briefs override template demo soundtracks

* fix(motion): keep project grid covers muted

All Projects grid and recent-project hover covers loaded motion sources
without odMotionThumbnail, so the embedded player autoplayed the
soundtrack. Covers now always request thumbnail mode: animation keeps
playing, audio stays muted until the project preview is opened.

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
2026-10-22 04:53:43 +00:00

808 lines
66 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" data-od-preview-ready="false">
<head>
<meta name="od-motion-output" content="video">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Future Is Already Here</title>
<script src="assets/three.min.js"></script>
<script src="assets/gsap.min.js"></script>
<script src="assets/three-addons.js"></script>
<script src="assets/unbounded-black-glyphs.js"></script>
<style>
@font-face{font-family:UB;src:url('assets/unbounded-subset.ttf') format('truetype');font-weight:200 900;font-display:block}
@font-face{font-family:JBM;src:url('assets/jetbrains-mono-subset.ttf') format('truetype');font-weight:100 800;font-display:block}
:root{--bg:#050507;--ink:#FFFFFF;--accent:#1A2BFF}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;background:#000;overflow:hidden}
#viewport{position:fixed;inset:0;overflow:hidden;background:var(--bg)}
#main{position:absolute;left:50%;top:50%;width:1920px;height:1080px;transform-origin:center;background:var(--bg);overflow:hidden}
#main canvas{position:absolute;left:0;top:0;display:block;width:1920px;height:1080px}
#controls{position:fixed;left:50%;bottom:14px;transform:translateX(-50%);z-index:100;display:flex;align-items:center;gap:14px;width:min(900px,92vw);padding:12px 18px;border-radius:14px;background:#101214dd;color:#fff;font:600 13px ui-monospace,Menlo,monospace;opacity:0;transition:opacity .2s}
#controls:hover,#controls:focus-within{opacity:1}
#controls button{font:inherit;letter-spacing:.06em;background:var(--accent);color:#FFFFFF;border:0;border-radius:999px;padding:9px 16px;cursor:pointer;min-width:88px}
#controls button:focus-visible,#scrub:focus-visible{outline:3px solid #fff;outline-offset:2px}
#scrub{flex:1;accent-color:var(--accent)}
#tc{min-width:110px;text-align:right;font-variant-numeric:tabular-nums}
/* The OD export bridge marks frame capture; keep preview controls out of the film. */
html.od-export #controls{display:none!important}
html.od-export #main{left:0!important;top:0!important;transform:none!important}
</style>
</head>
<body>
<div id="viewport">
<div id="main" data-composition-id="main" data-start="0" data-duration="20" data-width="1920" data-height="1080">
<canvas id="gl" width="1920" height="1080"></canvas>
<canvas id="ui" width="1920" height="1080"></canvas>
<audio id="soundtrack" src="assets/soundtrack.mp3" data-start="0" data-duration="20" data-track-index="2" data-volume="1" preload="auto"></audio>
</div>
</div>
<div id="controls">
<button id="playBtn" type="button" aria-label="Play or pause">PAUSE</button>
<input id="scrub" type="range" min="0" max="20" step="0.01" value="0" aria-label="Timeline position">
<span id="tc">0.00 / 20.00</span>
</div>
<script>
/* ============================================================
The Future Is Already Here — a 20-second tech reel at 120 BPM.
Cubes → SIGNAL → retina → word tunnel → THE FUTURE → desktop →
countdown → four glass objects → vortex → chrome hero → end card.
Two stacked canvases: #gl (three.js) under #ui (2D type and HUD).
One clock: render(t) draws every frame from timeline time.
============================================================ */
const CFG = {
duration: 20, w: 1920, h: 1080, bpm: 120, reducedAt: 17.0,
// Copy: change these to retell the film for another brand or message.
// Every 3D word uses assets/unbounded-black-glyphs.js (A–Z, a–z, 0–9, common punctuation).
brand: 'FIH/2026',
headline: 'THE FUTURE IS ALREADY HERE',
hero: 'FUTURE', // chrome 3D word in the closing shot; long words scale to fit
heroLine: 'IS ALREADY HERE.',
date: '2026.09.27',
signal: 'SIGNAL',
wake: 'WAKE UP',
awaken: 'AWAKEN',
tags: ['FUTURE', 'ALREADY', 'HERE'],
objects: ['TOMORROW', 'POWER', 'WORLD', 'MIND'], // type behind the four glass objects
endLine: 'END OF TRANSMISSION'
};
/* ======== core ======== */
// Shared helpers for the replica: palette, easing, 2D type, HUD, three.js setup.
const W = 1920, H = 1080, BPM = 120, BEAT = 60 / BPM;
const C = { blue: '#1A2BFF', blue2: '#3344FF', deep: '#0B1060', ink: '#0A0A0F', paper: '#EDEDE8', white: '#FFFFFF', lav: '#C9C6FF', dim: '#8F97FF' };
const UB = 'UB, "Arial Black", sans-serif';
const CJK = UB; // the English cut uses no CJK glyphs
const MONO = 'JBM, ui-monospace, Menlo, monospace';
const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
const seg = (t, a, b) => clamp((t - a) / (b - a));
const lerp = (a, b, p) => a + (b - a) * p;
const TAU = Math.PI * 2;
function rnd(i) { const s = Math.sin(i * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); }
const EZ = {
o2: p => 1 - (1 - p) ** 2, o3: p => 1 - (1 - p) ** 3, o4: p => 1 - (1 - p) ** 4, i3: p => p ** 3, i2: p => p * p,
io3: p => p < .5 ? 4 * p ** 3 : 1 - (-2 * p + 2) ** 3 / 2, io2: p => p < .5 ? 2 * p * p : 1 - (-2 * p + 2) ** 2 / 2,
expo: p => p >= 1 ? 1 : 1 - 2 ** (-10 * p), back: p => { const c = 1.70158 * 1.3, c3 = c + 1; return 1 + c3 * (p - 1) ** 3 + c * (p - 1) ** 2; },
};
/* ---------- 2D ---------- */
const ui = document.getElementById('ui').getContext('2d');
let CUR = ui; // helpers draw into CUR; target(g) redirects them to another canvas
function target(g) { CUR = g || ui; }
function text(s, x, y, size, color, o = {}) {
const g = o.g || CUR; g.save();
g.font = `${o.w || 900} ${size}px ${o.f || UB}`; g.textAlign = o.align || 'left'; g.textBaseline = o.base || 'alphabetic';
g.globalAlpha = o.a ?? 1; g.translate(x, y); if (o.rot) g.rotate(o.rot); g.scale(o.sx || 1, o.sy || 1); g.letterSpacing = (o.ls ?? 0) + 'px';
if (o.stroke) { g.lineWidth = o.stroke; g.strokeStyle = color; g.lineJoin = 'round'; g.strokeText(s, 0, 0); }
else { g.fillStyle = color; g.fillText(s, 0, 0); }
g.restore();
}
const mono = (s, x, y, size, color, o = {}) => text(s, x, y, size, color, { w: 600, ls: 1.5, ...o, f: o.f || MONO });
function measure(s, size, o = {}) { ui.save(); ui.font = `${o.w || 900} ${size}px ${o.f || UB}`; ui.letterSpacing = (o.ls ?? 0) + 'px'; const m = ui.measureText(s).width; ui.restore(); return m; }
function fill(c) { CUR.fillStyle = c; CUR.fillRect(0, 0, W, H); }
function rect(x, y, w, h, c) { CUR.fillStyle = c; CUR.fillRect(x, y, w, h); }
function line(x1, y1, x2, y2, c, w = 1.5) { CUR.strokeStyle = c; CUR.lineWidth = w; CUR.beginPath(); CUR.moveTo(x1, y1); CUR.lineTo(x2, y2); CUR.stroke(); }
function barcode(x, y, w, h, seed, col, g = CUR) { g.fillStyle = col; let cx = x, k = 0; while (cx < x + w) { const bw = 1 + Math.floor(rnd(seed + k) * 4); if (k % 2 === 0) g.fillRect(cx, y, bw, h); cx += bw; k++; } }
function brackets(x, y, w, h, col, l = 18, lw = 1.5) {
const g = CUR; g.strokeStyle = col; g.lineWidth = lw; g.beginPath();
[[x, y, 1, 1], [x + w, y, -1, 1], [x, y + h, 1, -1], [x + w, y + h, -1, -1]].forEach(([px, py, sx, sy]) => { g.moveTo(px + l * sx, py); g.lineTo(px, py); g.lineTo(px, py + l * sy); });
g.stroke();
}
function sparkle(x, y, r, col = '#fff', rot = 0, g = CUR) {
g.save(); g.translate(x, y); g.rotate(rot); g.fillStyle = col; g.beginPath();
for (let i = 0; i < 4; i++) { const a = i / 4 * TAU; g.quadraticCurveTo(Math.cos(a - TAU / 8) * r * .12, Math.sin(a - TAU / 8) * r * .12, Math.cos(a) * r, Math.sin(a) * r); }
g.quadraticCurveTo(Math.cos(-TAU / 8) * r * .12, Math.sin(-TAU / 8) * r * .12, r, 0); g.closePath(); g.fill(); g.restore();
}
function typed(s, p) { return s.slice(0, Math.floor([...s].length * clamp(p))); }
function scramble(s, p, seed = 1) { const n = Math.floor(s.length * clamp(p)); const pool = '#&%$@01ABXZ'; return s.slice(0, n) + (n < s.length ? pool[Math.floor(rnd(seed + n) * pool.length)] : ''); }
// Tag row under hero type: FUTURE · ALREADY · HERE, one boxed.
function tagRow(cx, y, hi, fg, bg) {
const words = CFG.tags; const ws = words.map(w => measure(w, 26, { w: 900 }) + 16); const gap = 50;
let x = cx - (ws.reduce((a, b) => a + b, 0) + gap * 2) / 2;
words.forEach((w, i) => {
if (i === hi) rect(x - 4, y - 30, ws[i], 40, fg);
text(w, x + 4, y, 26, i === hi ? bg : fg, { w: 900 }); x += ws[i];
if (i < 2) { text('·', x + gap / 2 - 6, y, 26, fg, {}); x += gap; }
});
}
/* ---------- frame HUD that runs through the whole film ---------- */
function hud(t, fg, chapter) {
mono('● ' + CFG.brand + ' — ' + CFG.headline, 56, 50, 15, fg);
mono(`00:${String(Math.floor(t)).padStart(2, '0')}:${String(Math.floor((t % 1) * 30)).padStart(2, '0')}`, W - 56, 50, 15, fg, { align: 'right' });
mono(chapter, 56, H - 34, 15, fg);
mono('120 BPM', W - 120, H - 34, 15, fg, { align: 'right' });
ui.fillStyle = fg; const b = Math.floor(t / BEAT) % 4; for (let i = 0; i < 4; i++) { ui.globalAlpha = b === i ? 1 : .35; ui.fillRect(W - 108 + i * 14, H - 46, 9, 9); }
ui.globalAlpha = 1;
}
/* ---------- three.js ---------- */
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('gl'), antialias: true, preserveDrawingBuffer: true, alpha: false });
renderer.setPixelRatio(1); renderer.setSize(W, H, false);
renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.0;
const pmrem = new THREE.PMREMGenerator(renderer);
const ENV = pmrem.fromScene(new THREEAddons.RoomEnvironment(), 0.03).texture;
const loader = new THREEAddons.FontLoader();
const FONT3D = { lat: loader.parse(window.__FONTS.unbounded) };
const geoCache = new Map();
// Extruded glyph geometry, centered on its own box.
function glyphGeo(s, font, size, depth, bevel = size * 0.03) {
const key = [s, font, size, depth, bevel].join('|'); if (geoCache.has(key)) return geoCache.get(key);
const g = new THREEAddons.TextGeometry(s, { font: FONT3D[font], size, depth, curveSegments: 6, bevelEnabled: bevel > 0, bevelThickness: bevel, bevelSize: bevel * 0.7, bevelSegments: 3 });
g.computeBoundingBox(); const b = g.boundingBox; g.translate(-(b.max.x + b.min.x) / 2, -(b.max.y + b.min.y) / 2, -(b.max.z + b.min.z) / 2);
geoCache.set(key, g); return g;
}
function chromeMat(tint = '#E9E8FF', rough = 0.14) {
return new THREE.MeshStandardMaterial({ color: new THREE.Color(tint), metalness: 1, roughness: rough, envMap: ENV, envMapIntensity: 1.25 });
}
function glassMat(o = {}) {
return new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 0, roughness: 0.04, transmission: 1, thickness: 1.2, ior: 1.45, iridescence: 1, iridescenceIOR: 1.6, iridescenceThicknessRange: [180, 820], envMap: ENV, envMapIntensity: 1.4, transparent: true, ...o });
}
function clear3d(color) { renderer.setClearColor(new THREE.Color(color), 1); renderer.clear(); }
// A word as one group of per-letter meshes laid out by the font's advances,
// so letters can move individually while keeping real spacing.
function wordGroup(str, font, size, depth, mat, o = {}) {
const f = FONT3D[font], k = size / f.data.resolution, track = (o.track ?? 0) * size; const g = new THREE.Group(); let x = 0;
for (const ch of str) { const gl = f.data.glyphs[ch]; const adv = (gl ? gl.ha : f.data.resolution * 0.3) * k;
if (ch !== ' ' && gl) { const m = new THREE.Mesh(glyphGeo(ch, font, size, depth, o.bevel ?? size * 0.03), mat); const cx = ((gl.x_min + gl.x_max) / 2) * k; m.position.x = x + cx; g.add(m); }
x += adv + track; }
g.children.forEach(m => m.position.x -= (x - track) / 2); g.userData.width = x - track; return g;
}
// A 2D canvas used as a three.js scene background, so 3D objects (and glass
// transmission) sit in front of flat graphics.
function bgCanvas(scene) {
const c = document.createElement('canvas'); c.width = W; c.height = H; const g = c.getContext('2d');
const tex = new THREE.CanvasTexture(c); tex.colorSpace = THREE.SRGBColorSpace; scene.background = tex;
return { g, c, update() { tex.needsUpdate = true; } };
}
/* ======== seg1 ======== */
// Segment 1 (0–16 s): signal → retina → awaken → THE FUTURE IS ALREADY HERE → FUTURE.EXE → TOMORROW
/* ---------- A: one cube → four → sixty-four ---------- */
const A = (() => {
const scene = new THREE.Scene(); const cam = new THREE.PerspectiveCamera(28, W / H, 0.1, 200); cam.position.set(0, 0, 26);
scene.add(new THREE.HemisphereLight(0xdfe3ff, 0x0a0c30, 1.1)); const key = new THREE.DirectionalLight(0xffffff, 2.4); key.position.set(-6, 8, 10); scene.add(key);
const rim = new THREE.DirectionalLight(0x8fa0ff, 1.2); rim.position.set(8, -2, -6); scene.add(rim);
const mat = new THREE.MeshStandardMaterial({ color: new THREE.Color(C.blue), roughness: 0.42, metalness: 0.05, envMap: ENV, envMapIntensity: 0.35 });
const box = new THREE.BoxGeometry(1, 1, 1); const inst = new THREE.InstancedMesh(box, mat, 64); scene.add(inst);
const group = new THREE.Group(); group.add(inst); scene.add(group);
const m = new THREE.Matrix4(), q = new THREE.Quaternion(), v = new THREE.Vector3(), s = new THREE.Vector3();
const S = 7.2;
const cell = (n, ix, iy) => { const c = 8 / n; const cx = Math.floor(ix / c), cy = Math.floor(iy / c); const sz = S / n; return [((cx + 0.5) / n - 0.5) * S, ((cy + 0.5) / n - 0.5) * S, sz * (n === 1 ? 1 : n === 2 ? 0.94 : 0.9)]; };
function draw(lt) {
const born = EZ.back(seg(lt, 0.45, 0.8));
const p2 = EZ.io3(seg(lt, 1.05, 1.35)), p8 = EZ.io3(seg(lt, 1.55, 1.85));
for (let i = 0; i < 64; i++) {
const ix = i % 8, iy = Math.floor(i / 8);
const a = cell(1, ix, iy), b = cell(2, ix, iy), c = cell(8, ix, iy);
let x = lerp(lerp(a[0], b[0], p2), c[0], p8), y = lerp(lerp(a[1], b[1], p2), c[1], p8), sz = lerp(lerp(a[2], b[2], p2), c[2], p8);
const pop = p8 > 0 ? 1 + 0.25 * Math.sin(seg(lt, 1.55 + rnd(i) * 0.2, 1.9 + rnd(i) * 0.2) * Math.PI) : 1;
const z = p8 * (rnd(i * 3) - 0.5) * 0.4 * (1 - seg(lt, 1.9, 2.1));
m.compose(v.set(x, y, z), q.identity(), s.set(sz * born, sz * born * pop, sz * 0.9 * born)); inst.setMatrixAt(i, m);
}
inst.instanceMatrix.needsUpdate = true;
group.rotation.set(0.12 + 0.18 * p8, -0.42 + 0.1 * lt, 0.02);
cam.position.z = 26 - 3 * p8; cam.lookAt(0, 0, 0);
renderer.setClearColor(0x050507, 1); renderer.render(scene, cam);
ui.clearRect(0, 0, W, H);
line(0, 540, W, 540, 'rgba(255,255,255,.18)'); line(960, 0, 960, H, 'rgba(255,255,255,.18)');
if (lt < 0.5) { const k = 0.5 + 0.5 * Math.sin(lt * 30); rect(952, 524, 16, 32, `rgba(255,255,255,${k})`); }
const n = lt < 1.2 ? 1 : lt < 1.7 ? 4 : 64, bw = [440, 560, 900][[1, 4, 64].indexOf(n)];
if (lt > 0.45) { brackets(960 - bw / 2, 540 - bw / 2 * 0.95, bw, bw * 0.95, 'rgba(255,255,255,.75)', 18);
mono(`CELLS ${String(n).padStart(4, '0')}`, 960 - bw / 2, 540 - bw / 2 * 0.95 - 14, 14, '#fff');
mono('SIGNAL/01 · SOURCE', 960 + bw / 2, 540 - bw / 2 * 0.95 - 14, 14, '#9aa', { align: 'right' });
mono('KLEIN #1A2BFF', 960 - bw / 2, 540 + bw / 2 * 0.95 + 22, 14, '#9aa');
mono(`${633 + n * 2} × ${677 + n * 2} PX`, 960 + bw / 2, 540 + bw / 2 * 0.95 + 22, 14, '#9aa', { align: 'right' }); }
[-600, -300, 300, 600].forEach(d => mono(String(Math.abs(d) / 3 | 0).padStart(3, '0'), 960 + d, 534, 11, 'rgba(255,255,255,.35)', { align: 'center' }));
}
return { t0: 0, t1: 2.1, chapter: '00 SIGNAL', fg: '#fff', three: true, draw };
})();
/* ---------- B/C/D: the signal wall resolves into SIGNAL ---------- */
const sigCells = (() => { const c = document.createElement('canvas'); c.width = 300; c.height = 70; const g = c.getContext('2d');
g.fillStyle = '#fff'; g.font = `900 64px ${UB}`; g.textBaseline = 'top'; g.letterSpacing = '-2px'; g.fillText(CFG.signal, 0, 0); return () => {
g.clearRect(0, 0, 300, 70); g.font = `900 64px ${UB}`; g.fillText(CFG.signal, 0, 0);
const d = g.getImageData(0, 0, 300, 70).data, out = []; for (let y = 0; y < 70; y += 2) for (let x = 0; x < 300; x += 2) if (d[(y * 300 + x) * 4 + 3] > 110) out.push([x / 2, y / 2]); return out; }; })();
let SIG = null;
function signalFrame(lt, blink = true) {
if (!SIG) SIG = sigCells();
fill('#050507'); ui.fillStyle = 'rgba(26,43,255,.13)'; for (let y = 4; y < H; y += 12) for (let x = 4; x < W; x += 12) ui.fillRect(x, y, 6, 6);
const cols = Math.max(...SIG.map(c => c[0])) + 1, rows = Math.max(...SIG.map(c => c[1])) + 1, cs = 1400 / cols, ox = 960 - cols * cs / 2, oy = 500 - rows * cs / 2;
SIG.forEach(([x, y], i) => { ui.fillStyle = rnd(i * 7 + Math.floor(lt * 8)) > 0.93 ? '#8A95FF' : C.blue; ui.fillRect(ox + x * cs, oy + y * cs, cs - 1.5, cs - 1.5); });
mono('CH.01 · CARRIER LOCKED · 2026.09.27', ox, oy - 26, 15, '#C8CCFF'); mono('SNR 97.2 DB · LAT 0.00 MS', ox + cols * cs, oy - 26, 15, '#C8CCFF', { align: 'right' });
const msg = 'INCOMING TRANSMISSION — STAND BY'; mono(typed(msg, seg(lt, 0, 0.45)) + (blink && Math.floor(lt * 6) % 2 ? '▌' : ''), ox + 10, oy + rows * cs + 52, 26, '#fff', { ls: 4 });
barcode(ox + cols * cs - 190, oy + rows * cs + 30, 190, 26, 5, '#fff');
['+', '+'].forEach((s, i) => mono(s, i ? ox + cols * cs + 20 : ox - 30, oy + rows * cs + 44, 20, '#8A95FF'));
}
const B = {
t0: 2.1, t1: 3.1, chapter: '00 SIGNAL', fg: '#fff', draw(lt) {
const wx = lerp(-80, W + 120, EZ.io2(seg(lt, 0, 1)));
ui.save(); ui.beginPath(); ui.rect(0, 0, Math.max(0, wx), H); ui.clip(); signalFrame(lt + 0.3, false); ui.restore();
ui.save(); ui.beginPath(); ui.rect(wx, 0, W - wx, H); ui.clip(); fill('#060820');
for (let y = 0; y < H; y += 13) for (let x = Math.floor(wx / 13) * 13; x < W; x += 13) { const r = rnd(x * 0.37 + y * 1.31); ui.fillStyle = r > 0.992 ? '#fff' : C.blue; ui.fillRect(x, y, 11, 11); }
ui.restore();
for (let y = 0; y < H; y += 13) { const j = (rnd(y + Math.floor(lt * 20)) - 0.5) * 70; for (let k = 0; k < 6; k++) { const x = wx + j + k * 11 - 30; ui.fillStyle = `rgba(255,255,255,${0.25 + rnd(y * k + 3) * 0.75})`; ui.fillRect(x, y, 11, 11); } }
mono(`RECEIVING ${String(Math.round(lerp(62, 100, seg(lt, 0, 0.9)))).padStart(3, '0')}%`, 56, 96, 15, '#fff'); rect(56, 106, 300 * seg(lt, 0, 0.9), 3, '#fff');
mono('CH.01 · 31,000 CELLS · PITCH 14PX', 56, 128, 12, '#9aa');
} };
const Csig = { t0: 3.1, t1: 3.72, chapter: '00 SIGNAL', fg: '#fff', draw(lt) { signalFrame(lt + 0.45); } };
const Dsig = { t0: 3.72, t1: 4.0, chapter: '00 SIGNAL', fg: '#fff', draw(lt) {
const p = EZ.i3(seg(lt, 0, 0.22)); fill('#050507');
ui.save(); ui.translate(0, 540); ui.scale(1 + p * 0.1, Math.max(0.004, 1 - p)); ui.translate(0, -540); signalFrame(1.2, false); ui.restore();
rect(0, 538, W, 4 * (1 - seg(lt, 0.2, 0.28)), '#fff'); } };
/* ---------- E/F: retinal handshake ---------- */
const sclera = (() => { const c = document.createElement('canvas'); c.width = W; c.height = H; const g = c.getContext('2d');
g.fillStyle = '#fff'; for (let y = 0; y < H; y += 9) for (let x = 0; x < W; x += 9) { const d = Math.hypot((x - 960) / 1.6, y - 540) / 520; const r = 3.6 * clamp(1.15 - d * 0.9); if (r > 0.4) { g.beginPath(); g.arc(x, y, r, 0, TAU); g.fill(); } } return c; })();
function iris(cx, cy, R, lt, g = ui) {
g.save(); g.translate(cx, cy);
g.fillStyle = '#0B1180'; g.beginPath(); g.arc(0, 0, R, 0, TAU); g.fill();
for (let i = 0; i < 360; i++) { const a = i / 360 * TAU + lt * 0.05, r0 = R * (0.3 + rnd(i) * 0.1), r1 = R * (0.75 + rnd(i * 3) * 0.24);
g.strokeStyle = `hsl(${232 + rnd(i * 5) * 10},100%,${45 + rnd(i * 2) * 35}%)`; g.lineWidth = 1 + rnd(i * 9) * 2.2; g.beginPath(); g.moveTo(Math.cos(a) * r0, Math.sin(a) * r0); g.lineTo(Math.cos(a) * r1, Math.sin(a) * r1); g.stroke(); }
g.strokeStyle = '#050520'; g.lineWidth = R * 0.07; g.beginPath(); g.arc(0, 0, R * 0.97, 0, TAU); g.stroke();
const rim = g.createConicGradient(0, 0, 0); ['#ff5ad6', '#7af0ff', '#ffe36a', '#ff5ad6'].forEach((c, i) => rim.addColorStop(i / 3, c));
g.strokeStyle = rim; g.lineWidth = 3; g.beginPath(); g.arc(0, 0, R * 1.005, 0, TAU); g.stroke();
const pr = R * (0.33 + 0.02 * Math.sin(lt * 5)); g.fillStyle = '#000'; g.beginPath(); for (let i = 0; i < 9; i++) { const a = i / 9 * TAU + 0.3; g.lineTo(Math.cos(a) * pr, Math.sin(a) * pr); } g.closePath(); g.fill();
g.strokeStyle = 'rgba(160,170,255,.6)'; g.lineWidth = 2; g.stroke();
sparkle(-R * 0.36, -R * 0.3, R * 0.2, '#fff', 0, g); g.fillStyle = '#fff'; g.beginPath(); g.arc(R * 0.16, R * 0.18, R * 0.03, 0, TAU); g.fill();
g.restore();
}
const Eeye = { t0: 4.0, t1: 5.6, chapter: '00 SIGNAL', fg: '#fff', draw(lt) {
fill('#040406'); const open = EZ.o4(seg(lt, 0, 0.3)), ew = 720, eh = 300 * open, push = 1 + 0.1 * seg(lt, 0, 1.6);
ui.save(); ui.translate(960, 540); ui.scale(push, push); ui.translate(-960, -540);
const almond = () => { ui.beginPath(); ui.moveTo(960 - ew, 540); ui.bezierCurveTo(960 - ew * 0.45, 540 - eh * 1.25, 960 + ew * 0.45, 540 - eh * 1.25, 960 + ew, 540); ui.bezierCurveTo(960 + ew * 0.45, 540 + eh * 1.25, 960 - ew * 0.45, 540 + eh * 1.25, 960 - ew, 540); };
ui.save(); almond(); ui.clip(); fill('#2732ff'); ui.globalAlpha = 0.95; ui.drawImage(sclera, 0, 0); ui.globalAlpha = 1; iris(960, 540, 255, lt); ui.restore();
almond(); ui.strokeStyle = '#000'; ui.lineWidth = 8; ui.stroke();
ui.restore();
const a = EZ.o3(seg(lt, 0.25, 0.55));
ui.globalAlpha = a; ui.strokeStyle = 'rgba(255,255,255,.55)'; ui.lineWidth = 1.5;
[[420, -0.25, 1.1], [470, 1.9, 1.2], [520, 3.5, 0.8]].forEach(([r, s0, l], i) => { ui.beginPath(); ui.arc(960, 540, r, s0 + lt * (i % 2 ? -0.3 : 0.3), s0 + l + lt * (i % 2 ? -0.3 : 0.3)); ui.stroke(); });
brackets(560, 220, 800, 640, 'rgba(255,255,255,.6)', 16);
line(940, 540, 980, 540, '#fff'); line(960, 520, 960, 560, '#fff'); ui.globalAlpha = 1;
mono('RETINAL HANDSHAKE — SCAN ' + (lt < 0.9 ? 'SEARCHING' : 'TRACKING'), 960, 86, 15, '#fff', { align: 'center', a });
mono('01 IRIS', 300, 300, 17, '#fff', { a }); mono('Ø 11.6 MM · FIBERS 4096', 300, 324, 12, '#9aa', { a });
mono('PUPIL 02', 1620, 300, 17, '#fff', { a, align: 'right' }); mono('APERTURE 8 BLADES · Ø 3.1 MM', 1620, 324, 12, '#9aa', { a, align: 'right' });
const pct = (lerp(0, 7.1, seg(lt, 0.5, 1.5))).toFixed(1);
mono('MATCH 03', 1620, 760, 14, '#fff', { a: seg(lt, 0.5, 0.7), align: 'right' }); text(pct + '%', 1620, 830, 64, '#fff', { a: seg(lt, 0.5, 0.7), align: 'right' });
rect(1440, 845, 180, 3, `rgba(255,255,255,${0.3 * seg(lt, 0.5, 0.7)})`); rect(1440, 845, 180 * seg(lt, 0.5, 1.5), 3, '#fff');
} };
const Fpct = { t0: 5.6, t1: 6.0, chapter: '00 SIGNAL', fg: '#fff', draw(lt) {
fill('#040406'); ui.save(); ui.globalAlpha = 1; iris(1500, 560, 980, lt + 1.6); ui.restore();
ui.fillStyle = C.blue; for (let y = 6; y < H; y += 22) for (let x = 1700; x < W; x += 22) { const r = 7 * clamp((x - 1680) / 260); ui.beginPath(); ui.arc(x, y, r, 0, TAU); ui.fill(); }
const p = EZ.expo(seg(lt, 0, 0.2)); text('97.2', 70, 620, 270, '#fff', { a: p }); text('%', 70 + measure('97.2', 270), 470, 130, '#fff', { a: p });
mono('MACRO ×4.2 · IRIS FIBER SCAN · 7 BLADES', 70, 340, 14, '#fff'); mono('03 MATCH ■ LOCKED', 70, 372, 15, '#fff');
for (let i = 0; i < 40; i++) rect(80 + i * 15, 680, 10, 6, i < 36 ? '#fff' : 'rgba(255,255,255,.3)'); mono('SUBJECT = YOU', 700, 688, 12, '#fff');
} };
/* ---------- G: through the pupil — a tunnel of words ---------- */
const TOK = ['WAKE UP', 'AWAKEN();', '<EYE/>', '0x1A2BFF', 'SIGNAL.OK', 'WAKE', 'UP', 'FUTURE IS HERE', 'FIH/2026', '2026.09.27', 'SEE', '{SEE();}', 'MATCH 97.2%', '01100110', '>>>', 'PUPIL', 'NOW==FUTURE', 'AWAKE', 'EYE', 'CONST', 'ENTER', 'TOMORROW'];
const TUN = Array.from({ length: 170 }, (_, i) => ({ a: rnd(i * 1.7) * TAU, r: 0.55 + rnd(i * 2.3) * 0.5, z: rnd(i * 3.9), s: TOK[i % TOK.length], blue: rnd(i * 5.1) > 0.62, big: rnd(i * 6.3) > 0.85 }));
const Gtun = { t0: 6.0, t1: 8.0, chapter: '00 SIGNAL', fg: '#fff', draw(lt) {
fill('#020206');
const irisA = 1 - seg(lt, 0.25, 0.75); if (irisA > 0) { ui.save(); ui.globalAlpha = irisA; const z = 1 + 4 * EZ.i2(seg(lt, 0, 0.8)); ui.translate(960, 540); ui.scale(z, z); ui.translate(-960, -540); iris(960, 540, 520, lt + 2); ui.restore(); }
const speed = 0.55;
const list = TUN.map(o => ({ ...o, zz: ((o.z - lt * speed) % 1 + 1) % 1 })).sort((a, b) => b.zz - a.zz);
list.forEach((o, i) => { const z = 0.08 + o.zz * 1.4, s = 1 / z, a = o.a + lt * 0.25, rr = o.r * 520 * s;
const x = 960 + Math.cos(a) * rr * 1.25, y = 540 + Math.sin(a) * rr; if (x < -400 || x > W + 400 || y < -300 || y > H + 300) return;
const size = (o.big ? 46 : 26) * s; const al = clamp((1.45 - z) * 2) * clamp(z * 6);
const cj = /[\u4e00-\u9fff]/.test(o.s);
text(o.s, x, y, size, o.blue ? '#3B49FF' : '#fff', { f: cj ? CJK : (o.big ? UB : MONO), w: cj ? 900 : (o.big ? 900 : 700), a: al, rot: a + Math.PI / 2, align: 'center', base: 'middle' });
});
const w1 = EZ.back(seg(lt, 0.55, 0.85)) * (1 - seg(lt, 1.05, 1.15));
if (w1 > 0) text(CFG.wake, 960, 560, 230 * w1, '#fff', { align: 'center', base: 'middle', ls: -6 });
const w2 = EZ.expo(seg(lt, 1.12, 1.3)) * (1 - seg(lt, 1.85, 2.0));
if (w2 > 0) text(CFG.awaken, 960, 560, 200 * (0.9 + 0.1 * w2), '#fff', { align: 'center', base: 'middle', a: w2, ls: -4 });
mono('WAKE SEQUENCE', 870, 82, 13, '#fff'); for (let i = 0; i < 8; i++) rect(990 + i * 10, 72, 7, 10, i < lt * 4 ? '#fff' : 'rgba(255,255,255,.3)');
mono(`ENTERING PUPIL · DEPTH ${String(Math.round(2465 + lt * 7000)).padStart(5, '0')}.${String(Math.round(lt * 97) % 100).padStart(2, '0')} μm`, 960, H - 70, 14, '#fff', { align: 'center' });
} };
/* ---------- H/I/J: THE FUTURE ---------- */
const BLUEBG = () => { fill(C.blue); ui.strokeStyle = 'rgba(255,255,255,.1)'; ui.lineWidth = 1; for (let x = 64; x < W; x += 149) { ui.beginPath(); ui.moveTo(x, 0); ui.lineTo(x, H); ui.stroke(); } };
const Hthe = { t0: 8.0, t1: 9.0, chapter: '01 ACCELERATE', fg: '#fff', draw(lt) {
BLUEBG(); const p1 = EZ.expo(seg(lt, 0, 0.18)), sz = 330;
line(0, 225, W, 225, 'rgba(255,255,255,.35)'); line(0, 470, W, 470, 'rgba(255,255,255,.35)'); line(0, 830, W, 830, 'rgba(255,255,255,.35)');
ui.save(); ui.beginPath(); ui.rect(0, 160, W, 680); ui.clip();
text('THE', 50, 470 + (1 - p1) * 300, sz, '#F2F2EE', { ls: -10 });
if (lt < 0.4) text(typed('FUTURE', seg(lt, 0.1, 0.35)), 50, 830, sz, 'rgba(255,255,255,.4)', { stroke: 3, ls: -10 });
else text('FUTURE', 50, 830 + (1 - EZ.expo(seg(lt, 0.4, 0.58))) * 360, sz, '#F2F2EE', { ls: -10 });
ui.restore();
const a = seg(lt, 0.1, 0.3);
mono('CAP-HEIGHT — 0.750 EM / 300 PX', 1150, 250, 13, '#fff', { a }); mono('SPECIMEN', 1150, 300, 15, '#fff', { a }); mono('01 OF 03', 1150, 324, 13, '#fff', { a }); mono('KINETIC TYPE STUDY', 1150, 346, 13, '#fff', { a });
text('01', 1860, 340, 150, '#fff', { w: 300, align: 'right', a });
barcode(1150, 395, 230, 34, 8, `rgba(255,255,255,${a})`);
['UNBOUNDED BLACK 900 / 400 PX', 'TRACK -0.005 EM · LEAD 0.82', 'GRID 12 COL · MARGIN 64'].forEach((s, i) => mono(s, 1400, 402 + i * 18, 12, '#fff', { a }));
mono('BASELINE — 0.000', 1150, 500, 12, '#fff', { a }); mono('FIT → 1792 PX · 344.5 PX / 273 CAP', 1860, 520, 12, '#fff', { a, align: 'right' });
sparkle(1830, 440, 22, '#fff', 0);
'FUTURE'.split('').forEach((ch, i) => mono(`${ch}-0${i + 1} 0.${[791, 889, 849, 889, 876, 812][i]}`, 56 + i * 305, 865, 12, '#fff', { a: seg(lt, 0.5, 0.7) }));
line(56, 905, 1860, 905, 'rgba(255,255,255,.7)'); mono('1810 PX', 960, 912, 12, '#fff', { align: 'center' });
tagRow(960, 985, 0, '#fff', C.blue);
} };
const Ifut = { t0: 9.0, t1: 9.6, chapter: '01 ACCELERATE', fg: '#fff', draw(lt) {
BLUEBG(); const x = -lt * 400;
text('FUTURE', 60 + x * 0.3, 260, 300, '#F2F2EE', { ls: -10 }); sparkle(40 + x * 0.3, 160, 40, '#fff');
text('FUTURE', 120 - x * 0.4, 560, 300, '#F2F2EE', { stroke: 4, ls: -10 });
text('TURE FUTURE', -500 + x, 870, 300, '#F2F2EE', { ls: -10 }); sparkle(-500 + x + measure('TURE ', 300, { ls: -10 }) + 30, 760, 60, '#fff');
mono('STATE: SOLID', 1300, 610, 14, '#fff'); rect(1300, 620, 280, 14, 'rgba(255,255,255,.2)'); rect(1300, 620, 280 * clamp(lt * 2), 14, '#000');
mono('RUN 01 / SOLID / 274 PX', 1220, 210, 12, '#fff'); mono('RUN 02 / OUTLINE / 274 PX', 40, 640, 12, '#fff');
} };
const J = (() => {
const scene = new THREE.Scene(); const cam = new THREE.PerspectiveCamera(30, W / H, 0.1, 400); cam.position.set(0, 0, 60);
scene.add(new THREE.AmbientLight(0xffffff, 0.3)); const d = new THREE.DirectionalLight(0xffffff, 2); d.position.set(5, 10, 20); scene.add(d);
const mat = new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 1, roughness: 0.08, envMap: ENV, envMapIntensity: 1.6, iridescence: 1, iridescenceIOR: 1.8, iridescenceThicknessRange: [200, 900] });
const rows = [0, 1, 2].map(r => { const g = wordGroup('FUTURE', 'lat', 9, 3.2, mat, { bevel: 1.1, track: -0.02 }); scene.add(g); return g; });
return { t0: 9.6, t1: 10.0, chapter: '01 ACCELERATE', fg: '#fff', three: true, draw(lt) {
rows.forEach((g, r) => { g.position.set((r === 1 ? 6 : -10) - lt * 14 * (r === 1 ? -1 : 1), 12 - r * 12, 0);
g.children.forEach((m, i) => { m.rotation.x = 0.25 * Math.sin(lt * 9 + i + r); m.rotation.y = 0.35 * Math.sin(lt * 7 + i * 0.7); m.scale.y = 1 + 0.12 * Math.sin(lt * 11 + i); }); });
renderer.setClearColor(new THREE.Color(C.blue), 1); renderer.render(scene, cam);
ui.clearRect(0, 0, W, H); mono('STATE: LIQUID 98%', 1300, 610, 14, '#fff'); rect(1300, 620, 280, 14, 'rgba(255,255,255,.25)'); rect(1300, 620, 274, 14, '#000');
sparkle(560, 760, 46, '#fff'); sparkle(1440, 330, 30, '#fff');
} };
})();
/* ---------- K/L: IS ALREADY / HERE ---------- */
const Kis = { t0: 10.0, t1: 11.0, chapter: '01 ACCELERATE', fg: C.ink, draw(lt) {
fill(C.paper); const solid = lt > 0.48;
text('IS', 20, 300, 150, C.ink, { ls: -6 });
mono('02/03 — PRESENT TENSE', 470, 205, 15, C.ink); mono('HERE · TO BE · NOT "WILL BE"', 470, 230, 12, C.ink); sparkle(1890, 205, 18, C.ink);
line(0, 330, W, 330, 'rgba(0,0,0,.25)');
if (!solid) { text('AL', 20, 640, 320, C.ink, { stroke: 3, ls: -10 }); text('READY', 20, 960, 320, C.ink, { stroke: 3, ls: -10 }); }
else { const p = EZ.expo(seg(lt, 0.48, 0.62)); text('AL', 20 - (1 - p) * 120, 640, 320, C.ink, { ls: -10 }); text('READ', 20, 960, 320, C.ink, { ls: -10 });
rect(1700 - 1700 * EZ.expo(seg(lt, 0.55, 0.75)) + 1700, 700, 260, 270, C.blue); rect(1700, 690, 260 * EZ.expo(seg(lt, 0.55, 0.75)), 280, C.blue);
rect(560, 455, 1360 * EZ.expo(seg(lt, 0.5, 0.75)), 26, C.ink); barcode(560, 500, 300, 26, 3, C.ink); mono('FIH-2026-0927 / 02', 880, 520, 12, C.ink); }
tagRow(960, 1030, 1, C.ink, C.paper);
} };
const Lhere = { t0: 11.0, t1: 12.1, chapter: '01 ACCELERATE', fg: '#fff', draw(lt) {
BLUEBG(); text('R', 1180, 1050, 1150, 'rgba(255,255,255,.22)', { stroke: 3 }); text('HE', 300, 1050, 1150, 'rgba(255,255,255,.12)', { stroke: 3 });
const p = EZ.expo(seg(lt, 0, 0.2)); text('HERE', 960, 700, 420 * (1.1 - 0.1 * p), '#F2F2EE', { align: 'center', ls: -14, a: p });
if (lt > 0.45) { brackets(40, 230, 1840, 560, '#fff', 22, 2); line(940, 540, 980, 540, '#fff'); line(960, 520, 960, 560, '#fff'); ui.strokeStyle = '#fff'; ui.beginPath(); ui.arc(960, 540, 12, 0, TAU); ui.stroke();
mono('02/03 — LOCATION', 56, 190, 15, '#fff'); mono('SIGNAL LOCKED', 1860, 190, 15, '#fff', { align: 'right' }); mono('HERE = NOW', 170, 290, 15, '#fff');
mono('▲ YOU ARE HERE', 1130, 830, 14, '#fff'); mono('X 0960 · Y 0540 · Z 0.00', 1130, 852, 12, '#fff'); }
tagRow(960, 1030, 2, '#fff', C.blue);
} };
/* ---------- M/N: FUTURE.EXE desktop ---------- */
const dots = (() => { const c = document.createElement('canvas'); c.width = W; c.height = H; const g = c.getContext('2d'); g.fillStyle = '#E9E9E4'; g.fillRect(0, 0, W, H); g.fillStyle = 'rgba(0,0,0,.35)'; for (let y = 14; y < H; y += 28) for (let x = 14; x < W; x += 28) { g.beginPath(); g.arc(x, y, 1.6, 0, TAU); g.fill(); } return c; })();
function win(x, y, w, h, o, body) {
const p = EZ.back(seg(o.lt, o.at, o.at + 0.18)); if (p <= 0) return;
ui.save(); ui.translate(x + w / 2, y + h / 2); ui.rotate(o.rot || 0); ui.scale(p, p); ui.translate(-w / 2, -h / 2);
rect(10, 10, w, h, 'rgba(0,0,0,.18)'); rect(0, 0, w, h, o.bg || '#0B0B0E'); ui.strokeStyle = '#0B0B0E'; ui.lineWidth = 4; ui.strokeRect(0, 0, w, h);
rect(0, 0, w, 34, o.bar || '#F2F2EE'); for (let i = 0; i < 4; i++) { rect(46, 8 + i * 5, w * 0.28, 2, '#0B0B0E'); rect(w * 0.62, 8 + i * 5, w * 0.28, 2, '#0B0B0E'); }
ui.strokeStyle = '#0B0B0E'; ui.lineWidth = 2; ui.strokeRect(10, 8, 18, 18); ui.strokeRect(w - 54, 8, 18, 18); rect(w - 30, 8, 18, 18, '#0B0B0E');
mono(o.title || '', w / 2, 24, 13, '#0B0B0E', { align: 'center' });
ui.translate(0, 34); body(w, h - 34); ui.restore();
}
function desktop(lt, base = 0) {
ui.drawImage(dots, 0, 0); const o = (at, extra = {}) => ({ lt, at: at - base, ...extra });
ui.strokeStyle = C.blue; ui.lineWidth = 2; ui.strokeRect(-20, 300, 230, 330); ui.strokeRect(1560, 690, 300, 210);
win(220, 230, 1000, 560, o(0, { title: 'FUTURE.EXE', rot: -0.07 }), (w, h) => {
mono('C:\\FIH\\2026> RUN FUTURE.EXE /FORCE', 40, 60, 22, '#F2F2EE'); mono('V.2026.09.27', w - 40, 60, 15, '#F2F2EE', { align: 'right' });
text('FUTURE', 40, 300, 190, '#F2F2EE', { ls: -6 }); mono('> MODULES', 40, 400, 16, '#F2F2EE'); for (let i = 0; i < 14; i++) rect(190 + i * 13, 386, 9, 16, i < 10 ? '#F2F2EE' : 'rgba(255,255,255,.25)'); mono('74%', 390, 400, 16, '#F2F2EE');
mono('> PID 0X7EA · MEM 640K · STATUS RUNNING', 40, 430, 14, '#F2F2EE'); });
win(-10, 360, 330, 340, o(0.5, { title: 'NOW.APP', bg: C.blue }), (w, h) => { text('NOW', 24, 200, 130, '#fff', { ls: -4 }); sparkle(w - 40, 40, 20, '#fff'); mono('OPEN ▸', w - 20, h - 20, 13, '#fff', { align: 'right' }); });
win(1400, 120, 420, 200, o(0.5, { title: 'YEAR.SYS' }), (w, h) => { const yr = 2036 - Math.round(10 * EZ.o3(seg(lt + base, 0.5, 1.1))); text(String(yr), 30, 120, 120, '#F2F2EE'); mono(`Δ ${(10.1 - 10.1 * seg(lt + base, 0.5, 1.1)).toFixed(1)} YRS · CALIBRATED`, 30, 150, 13, '#F2F2EE'); });
win(120, 830, 560, 160, o(0.9, { title: 'AUDIO.WAV' }), (w, h) => { ui.fillStyle = C.blue; for (let i = 0; i < 90; i++) { const a = 10 + 40 * Math.abs(Math.sin(i * 0.7 + lt * 9) * Math.sin(i * 0.13)); ui.fillRect(20 + i * 5.8, 70 - a / 2, 3.5, a); } mono('120 BPM · 44.1K', w - 20, 112, 12, '#F2F2EE', { align: 'right' }); });
win(1240, 720, 620, 230, o(0.9, { title: 'INSTALL.EXE', bg: '#F2F2EE' }), (w, h) => { mono('INSTALLING TOMORROW…', 30, 50, 20, C.ink); text(`${Math.round(lerp(40, 64, seg(lt + base, 0.9, 1.9)))}%`, w - 30, 110, 74, C.ink, { align: 'right' });
for (let i = 0; i < 24; i++) ui.strokeRect(30 + i * 23, 140, 18, 30); for (let i = 0; i < 15; i++) rect(32 + i * 23, 142, 14, 26, C.ink); });
win(40, 90, 300, 260, o(1.4, { title: 'MOOD.EXE', bg: C.blue }), (w, h) => { ui.fillStyle = '#fff'; ui.fillRect(90, 60, 22, 40); ui.fillRect(180, 60, 22, 40); ui.strokeStyle = '#fff'; ui.lineWidth = 16; ui.beginPath(); ui.arc(146, 110, 70, 0.35, Math.PI - 0.35); ui.stroke(); });
win(380, 420, 470, 280, o(1.4, { title: 'ASSISTANT.EXE', bg: '#F2F2EE' }), (w, h) => { text('HOW CAN', 24, 100, 62, C.ink, { ls: -2 }); text('I HELP?', 24, 170, 62, C.ink, { ls: -2 }); mono('> SHOW ME TOMORROW▌', 24, 225, 14, C.ink); });
win(870, 380, 560, 300, o(1.45, { title: 'UPDATE.EXE', bg: '#F2F2EE' }), (w, h) => { mono('SOFTWARE UPDATE AVAILABLE', 24, 50, 15, C.ink); text('TOMORROW 1.0', 24, 125, 56, C.ink, { ls: -2 }); ui.strokeStyle = C.ink; ui.strokeRect(24, 170, 150, 40); mono('LATER', 99, 196, 14, C.ink, { align: 'center' }); rect(190, 170, 200, 40, C.ink); mono('INSTALL NOW', 290, 196, 14, '#fff', { align: 'center' }); });
win(1020, 20, 520, 230, o(1.5, { title: 'SYSTEM ALERT', bg: '#F2F2EE' }), (w, h) => { rect(24, 30, 70, 70, C.ink); text('!', 59, 92, 60, '#fff', { align: 'center' }); mono('FATAL ERROR 0X2026', 120, 52, 18, C.ink); mono('THE PAST IS NO LONGER SUPPORTED.', 120, 80, 13, C.ink); rect(w - 210, h - 60, 90, 36, C.ink); mono('IGNORE', w - 165, h - 37, 12, '#fff', { align: 'center' }); ui.strokeRect(w - 110, h - 60, 90, 36); mono('OK', w - 65, h - 37, 12, C.ink, { align: 'center' }); });
win(1600, 330, 260, 300, o(1.55, { title: 'STAR.OBJ' }), (w, h) => { sparkle(w / 2, h / 2, 100, '#C9C6FF', lt * 0.5); sparkle(w / 2 + 70, h - 40, 22, '#fff'); });
}
const Mexe = { t0: 12.1, t1: 14.0, chapter: '01 ACCELERATE', fg: C.ink, draw(lt) { desktop(lt); } };
const Nload = { t0: 14.0, t1: 15.0, chapter: '01 ACCELERATE', fg: C.ink, draw(lt) {
desktop(lt + 1.9); ui.fillStyle = 'rgba(233,233,228,.35)'; ui.fillRect(0, 0, W, H);
win(60, 150, 1480, 780, { lt: lt + 1, at: 0.9, title: 'LOADING_TOMORROW.EXE — 88.DAT', bg: C.blue }, (w, h) => {
text('LOADING TOMORROW', 40, 110, 76, '#fff', { ls: -2 }); mono('FILE 01/01 · 88.DAT 3.4 MB · SECTOR 0X2026', w - 40, 70, 13, '#fff', { align: 'right' });
const n = Math.round(lerp(54, 99, EZ.io2(seg(lt, 0, 0.85)))); text('0', 40, 470, 330, '#fff', { stroke: 4, w: 300 }); text(String(n), 300, 470, 330, '#fff'); text('%', 950, 330, 160, '#fff', { stroke: 4, w: 300 });
mono(n >= 99 ? 'WAIT' : 'LOADING', 1110, 400, 14, '#fff');
for (let i = 0; i < 62; i++) rect(40 + i * 22, 540, 16, 34, i / 62 < n / 100 ? '#fff' : 'rgba(255,255,255,.25)');
text('PLEASE STAND BY', 40, 650, 34, '#fff'); barcode(470, 620, 220, 32, 12, '#fff'); mono('SYS 0.85 · SPEED 1.2 YR/S', w - 40, 640, 12, '#fff', { align: 'right' });
});
} };
/* ---------- O: TOMORROW in 3D over a grid floor ---------- */
const O = (() => {
const scene = new THREE.Scene(); const cam = new THREE.PerspectiveCamera(32, W / H, 0.1, 600);
scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const d = new THREE.DirectionalLight(0xffffff, 2.2); d.position.set(-8, 12, 20); scene.add(d);
const mat = new THREE.MeshStandardMaterial({ color: 0xf2f2ee, roughness: 0.35, metalness: 0.1, envMap: ENV, envMapIntensity: 0.5 });
const word = wordGroup('TOMORROW', 'lat', 6, 2.4, mat, { bevel: 0.4, track: -0.02 }); scene.add(word);
const grid = new THREE.GridHelper(400, 80, 0x1a2bff, 0x1a2bff); grid.material.transparent = true; grid.material.opacity = 0.55; grid.position.y = -8; scene.add(grid);
scene.fog = new THREE.Fog(0x05050a, 60, 220);
return { t0: 15.0, t1: 16.0, chapter: '01 ACCELERATE', fg: '#fff', three: true, draw(lt) {
const p = EZ.io3(seg(lt, 0, 1)); cam.position.set(0, 4 - 2 * p, 92 - 62 * p); cam.lookAt(0, 1, 0);
word.rotation.y = -0.18 * (1 - p); word.position.y = 2;
renderer.setClearColor(0x05050a, 1); renderer.render(scene, cam);
ui.clearRect(0, 0, W, H);
if (lt < 0.55) text('TOMORROW', 960, 360, 150, 'rgba(255,255,255,.55)', { align: 'center', stroke: 2, ls: -4 });
else { for (let i = 0; i < 120; i++) { const a = rnd(i) * TAU, r0 = 200 + rnd(i * 3) * 300, l = 100 + rnd(i * 7) * 500 * seg(lt, 0.55, 1); line(960 + Math.cos(a) * r0, 540 + Math.sin(a) * r0 * 0.7, 960 + Math.cos(a) * (r0 + l), 540 + Math.sin(a) * (r0 + l) * 0.7, 'rgba(255,255,255,.5)', 1); }
ui.strokeStyle = '#fff'; brackets(880, 470, 160, 140, '#fff', 14); mono('TARGET TOMORROW.EXE', 1060, 470, 12, '#fff'); mono('DIST 8.3 M · ETA 0.0S', 1060, 492, 12, '#fff'); }
const g = ui.createRadialGradient(960, 545, 0, 960, 545, 70); g.addColorStop(0, 'rgba(255,255,255,.95)'); g.addColorStop(0.4, 'rgba(90,110,255,.9)'); g.addColorStop(1, 'rgba(26,43,255,0)');
ui.fillStyle = g; ui.beginPath(); ui.arc(960, 545, 70 * (0.6 + 0.4 * p), 0, TAU); ui.fill();
} };
})();
const SEG1 = [A, B, Csig, Dsig, Eeye, Fpct, Gtun, Hthe, Ifut, J, Kis, Lhere, Mexe, Nload, O];
const S1 = { A, B, Csig, Dsig, Eeye, Fpct, Gtun, Hthe, Ifut, J, Kis, Lhere, Mexe, Nload, O };
/* ======== cut ======== */
// Scenes for the 20 s highlight cut that segment 1 does not cover:
// countdown, four specimen objects, ring tunnel, checker grid, chrome hero, end card.
function lights(scene, k = 1) {
scene.add(new THREE.HemisphereLight(0xe8eaff, 0x101030, 0.7 * k));
const d = new THREE.DirectionalLight(0xffffff, 2.2 * k); d.position.set(-8, 10, 14); scene.add(d);
const r = new THREE.DirectionalLight(0xb9b4ff, 1.4 * k); r.position.set(10, -4, -8); scene.add(r);
}
function labelBox(cn, en, x, y, dark = true) {
const w = measure(cn, 40) + measure(en, 26) + 70; rect(x, y - 54, w, 72, dark ? C.ink : '#F2F2EE');
text(cn, x + 20, y, 40, dark ? '#fff' : C.ink, { w: 300 }); text(en, x + 40 + measure(cn, 40), y - 4, 26, dark ? '#fff' : C.ink);
}
function specTable(x, y, title, rows, dark) {
const fg = dark ? '#fff' : C.ink, bg = dark ? C.ink : '#F2F2EE'; rect(x, y, 300, 34 + rows.length * 26, bg); ui.strokeStyle = fg; ui.lineWidth = 1.5; ui.strokeRect(x, y, 300, 34 + rows.length * 26);
mono(title, x + 12, y + 22, 12, fg); rows.forEach(([k, v], i) => { line(x, y + 32 + i * 26, x + 300, y + 32 + i * 26, dark ? 'rgba(255,255,255,.3)' : 'rgba(0,0,0,.25)', 1); mono(k, x + 12, y + 52 + i * 26, 11, fg); mono(v, x + 288, y + 52 + i * 26, 11, fg, { align: 'right' }); });
}
/* ---------- 3 · 2 · 1 ---------- */
const Count = (() => {
const scene = new THREE.Scene(); lights(scene); const cam = new THREE.PerspectiveCamera(30, W / H, 0.1, 300); cam.position.set(0, 0, 70);
const bg = bgCanvas(scene);
const mats = [new THREE.MeshStandardMaterial({ color: 0x08080c, metalness: 0.7, roughness: 0.22, envMap: ENV, envMapIntensity: 1.2 }), chromeMat('#F4F3FF', 0.1), new THREE.MeshStandardMaterial({ color: new THREE.Color(C.blue), metalness: 0.55, roughness: 0.18, envMap: ENV, envMapIntensity: 1.1 })];
const digits = ['3', '2', '1'].map((d, i) => { const m = new THREE.Mesh(glyphGeo(d, 'lat', 24, 7, 1.4), mats[i]); m.visible = false; scene.add(m); return m; });
const L = [['READY', 'T-01.500'], ['SET', 'T-01.000'], ['GO', 'T-00.500']];
return { t0: 0, t1: 1.5, chapter: '02 IGNITION', fg: '#fff', three: true, draw(lt) {
const k = Math.min(2, Math.floor(lt / 0.5)), l = lt - k * 0.5;
target(bg.g);
if (k === 0) { fill(C.blue); rect(0, 0, 1120, H, C.paper); }
else if (k === 1) { fill(C.blue); rect(1260, 0, 660, H, C.ink); }
else { fill(C.ink); bg.g.fillStyle = C.blue; bg.g.beginPath(); bg.g.moveTo(1150, H); bg.g.lineTo(W, 520); bg.g.lineTo(W, H); bg.g.fill(); }
const fg = k === 0 ? C.ink : '#fff';
bg.g.strokeStyle = k === 2 ? '#ff8a3d' : 'rgba(255,255,255,.7)'; bg.g.lineWidth = 2; bg.g.beginPath(); bg.g.ellipse(960, 560, 520, k === 2 ? 130 : 500, 0, 0, TAU); bg.g.stroke();
if (k === 2) { bg.g.strokeStyle = '#4a5bff'; bg.g.beginPath(); bg.g.ellipse(960, 580, 480, 110, 0, 0, TAU); bg.g.stroke(); }
target(); bg.update();
digits.forEach((m, i) => { m.visible = i === k; });
const m = digits[k], p = EZ.back(seg(l, 0, 0.22)); m.scale.setScalar(0.6 + 0.4 * p); m.rotation.set(0.12, (1 - EZ.o3(seg(l, 0, 0.3))) * 1.6 - 0.25 + l * 0.3, 0); m.position.set(0, 0, 0);
renderer.render(scene, cam);
ui.clearRect(0, 0, W, H);
mono(`T-MINUS 0${3 - k}`, 230, 430, 15, fg); text(L[k][0], 230, 505, 70, fg); mono(`SEQ 0${k + 1} · ${k + 1}/3`, 230, 540, 13, fg); mono(L[k][1], 230, 565, 13, fg);
const fg2 = '#fff'; mono('IGNITION SEQUENCE', 1680, 450, 13, fg2, { align: 'right' }); mono(`SNARE ROLL ${k + 1}/3`, 1680, 472, 13, fg2, { align: 'right' });
for (let i = 0; i < 3; i++) { ui.strokeStyle = fg2; ui.strokeRect(1540 + i * 48, 490, 40, 16); if (i <= k) rect(1540 + i * 48, 490, 40, 16, fg2); }
line(560, 420, 1360, 680, k === 0 ? 'rgba(0,0,0,.4)' : 'rgba(255,255,255,.4)', 1);
} };
})();
/* ---------- four specimen objects ---------- */
function typeRows(g, word, color, solidRows, t, size = 300) {
target(g); for (let r = 0; r < 4; r++) { const y = 230 + r * 270, x = -((t * 260 * (r % 2 ? -1 : 1)) % 1400) - 300;
const s = (word + ' ').repeat(5); text(s, x, y, size, color, solidRows.includes(r) ? { ls: -8 } : { stroke: 3, ls: -8 }); } target();
}
function objScene(build) { const scene = new THREE.Scene(); lights(scene); const cam = new THREE.PerspectiveCamera(30, W / H, 0.1, 300); cam.position.set(0, 0, 50); const bg = bgCanvas(scene); const obj = build(scene); return { scene, cam, bg, obj }; }
const bolt = () => { const s = new THREE.Shape(); [[1.5, 6], [-3, -0.6], [0.2, -0.6], [-1.6, -6], [3.2, 1], [0, 1], [1.5, 6]].forEach(([x, y], i) => i ? s.lineTo(x, y) : s.moveTo(x, y));
const g = new THREE.ExtrudeGeometry(s, { depth: 1.4, bevelEnabled: true, bevelThickness: 0.35, bevelSize: 0.3, bevelSegments: 4 }); g.center(); return g; };
const OBJS = [
{ cn: '01', en: 'PRISM', word: CFG.objects[0], col: C.paper, fg: C.ink, rows: [0, 2], dark: false, ...objScene(sc => { const m = new THREE.Mesh(new THREE.BoxGeometry(11, 11, 11, 1, 1, 1), glassMat({ thickness: 6, iridescence: 0.8 })); sc.add(m); return m; }) },
{ cn: '02', en: 'BOLT', word: CFG.objects[1], col: C.blue, fg: '#F2F2EE', rows: [1, 2], dark: true, ...objScene(sc => { const m = new THREE.Mesh(bolt(), new THREE.MeshPhysicalMaterial({ color: 0xf4c6ff, metalness: 1, roughness: 0.12, envMap: ENV, envMapIntensity: 1.5, iridescence: 1, iridescenceIOR: 1.9, iridescenceThicknessRange: [300, 900] })); m.scale.setScalar(1.55); sc.add(m); return m; }) },
{ cn: '03', en: 'PLANET', word: CFG.objects[2], col: C.ink, fg: C.blue, rows: [0, 1, 3], dark: true, ...objScene(sc => { const g = new THREE.Group(); g.add(new THREE.Mesh(new THREE.SphereGeometry(7.5, 64, 48), glassMat({ thickness: 4 })));
const ring = new THREE.Mesh(new THREE.TorusGeometry(12, 0.55, 24, 160), new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 1, roughness: 0.1, envMap: ENV, iridescence: 1, iridescenceThicknessRange: [200, 800] })); ring.scale.z = 0.25; ring.rotation.x = 1.25; g.add(ring); sc.add(g); return g; }) },
{ cn: '04', en: 'MIND', word: CFG.objects[3], col: C.blue, fg: C.ink, rows: [0, 2, 3], dark: true, ...objScene(sc => { const g = new THREE.Group(); const ico = new THREE.IcosahedronGeometry(9, 0);
g.add(new THREE.LineSegments(new THREE.EdgesGeometry(ico), new THREE.LineBasicMaterial({ color: 0xffffff })));
const pos = ico.attributes.position; const seen = new Set(); for (let i = 0; i < pos.count; i++) { const k = [pos.getX(i), pos.getY(i), pos.getZ(i)].map(v => v.toFixed(2)).join(); if (seen.has(k)) continue; seen.add(k); const s = new THREE.Mesh(new THREE.SphereGeometry(0.45, 16, 12), new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0x888888 })); s.position.set(pos.getX(i), pos.getY(i), pos.getZ(i)); g.add(s); }
const cube = new THREE.Mesh(new THREE.BoxGeometry(6, 6, 6), glassMat({ thickness: 3, color: 0xc8fff0 })); g.add(cube); sc.add(g); return g; }) },
];
const Objects = { t0: 0, t1: 2.0, chapter: '03 ARCHIVE', fg: '#fff', three: true, draw(lt) {
const k = Math.min(3, Math.floor(lt / 0.5)), l = lt - k * 0.5, o = OBJS[k];
target(o.bg.g); fill(o.col); target(); typeRows(o.bg.g, o.word, o.fg, o.rows, lt); o.bg.update();
const p = EZ.back(seg(l, 0, 0.2)); o.obj.scale.setScalar((k === 1 ? 1.55 : 1) * (0.5 + 0.5 * p)); o.obj.rotation.set(0.5 + l * 0.8, 0.6 + l * 2.2, 0.2);
renderer.render(o.scene, o.cam);
ui.clearRect(0, 0, W, H);
mono(`OBJ.0${k + 1} / 04`, 56, 110, 15, o.dark ? '#fff' : C.ink); mono('FUTURE OBJECT ARCHIVE', 56, 134, 13, o.dark ? '#fff' : C.ink);
mono(`T+2${4 + k * 0.5}.${String(Math.round(l * 999)).padStart(3, '0')}`, 1860, 110, 15, o.dark ? '#fff' : C.ink, { align: 'right' });
specTable(1450, 150, `SPEC / ${o.en}`, [['MATERIAL', ['GLASS', 'CHROME', 'GLASS', 'WIRE'][k]], ['IOR', '1.450'], ['ROUGH', '0.04'], ['ROT/S', (1.2 + k * 0.3).toFixed(2)]], o.dark);
labelBox(o.cn, o.en, 56, 1010, o.dark); barcode(1560, 960, 260, 40, 31 + k, o.dark ? '#fff' : C.ink);
} };
/* ---------- ring tunnel ---------- */
function ringText(g, r, s, size, rot, col) {
g.save(); g.rotate(rot); g.font = `900 ${size}px ${UB}`; g.fillStyle = col; g.textAlign = 'center'; g.textBaseline = 'middle';
const chars = [...s]; let a = 0, i = 0; while (a < TAU - 0.02) { const ch = chars[i % chars.length]; const w = g.measureText(ch).width + size * 0.12; g.save(); g.rotate(a); g.translate(0, -r); g.fillText(ch, 0, 0); g.restore(); a += w / r; i++; } g.restore();
}
const Rings = { t0: 0, t1: 1.0, chapter: '03 ARCHIVE', fg: '#fff', draw(lt) {
fill('#050507'); const z = 1 + 0.5 * EZ.io2(lt);
ui.save(); ui.translate(960, 540); ui.scale(z, z);
for (let k = 9; k >= 0; k--) {
const r = 46 * Math.pow(1.36, k), cx = -Math.pow(1.36, k) * 6 * Math.sin(lt * 2 + k * 0.5), cy = -Math.pow(1.36, k) * 4;
ui.save(); ui.translate(cx * (9 - k) * 0.1, cy * 0.2);
const white = k % 2 === 0, band = r * 0.16;
ui.fillStyle = white ? '#EDEDE8' : '#060608'; ui.beginPath(); ui.arc(0, 0, r + band, 0, TAU); ui.arc(0, 0, r - band, 0, TAU, true); ui.fill();
const g = ui.createLinearGradient(-r, -r, r, r); g.addColorStop(0, '#fff'); g.addColorStop(0.45, '#8890b8'); g.addColorStop(0.55, '#f4f4ff'); g.addColorStop(1, '#5b6390');
ui.strokeStyle = g; ui.lineWidth = Math.max(2, r * 0.03); ui.beginPath(); ui.arc(0, 0, r + band, 0, TAU); ui.stroke();
ringText(ui, r, white ? 'FUTURE · IS · HERE · ' : 'THE FUTURE IS ALREADY HERE · ', band * 1.2, lt * (k % 2 ? 0.9 : -0.7) + k, white ? C.ink : '#fff');
ui.strokeStyle = C.blue; ui.lineWidth = Math.max(2, r * 0.05); ui.beginPath(); ui.arc(0, 0, r - band - r * 0.05, k + lt * 1.5, k + lt * 1.5 + 1.2); ui.stroke();
ui.restore();
}
ui.restore();
for (let i = 0; i < 40; i++) { const a = rnd(i) * TAU; line(960 + Math.cos(a) * 700, 540 + Math.sin(a) * 700, 960 + Math.cos(a) * 1100, 540 + Math.sin(a) * 1100, 'rgba(255,255,255,.35)', 1); }
mono('VORTEX · DEPTH ' + (lt * 999 | 0), 70, 520, 13, '#fff'); mono('RING 10/10 · RPM 042', 1850, 520, 13, '#fff', { align: 'right' });
} };
/* ---------- checker grid ---------- */
const Checker = { t0: 0, t1: 0.5, chapter: '03 ARCHIVE', fg: '#fff', draw(lt) {
fill(C.ink); const tw = 480, th = 360;
const tiles = [
['k', g => { for (let i = -6; i < 12; i++) { ui.fillStyle = '#EDEDE8'; ui.beginPath(); ui.moveTo(i * 60, 0); ui.lineTo(i * 60 + 30, 0); ui.lineTo(i * 60 + 30 + th, th); ui.lineTo(i * 60 + th, th); ui.fill(); } }],
['w', () => text('THE', 240, 230, 120, C.ink, { align: 'center' })],
['w', () => { ui.strokeStyle = C.ink; ui.lineWidth = 2; for (let i = 0; i < 6; i++) { ui.beginPath(); ui.ellipse(240, 180, Math.abs(120 * Math.cos(i / 6 * Math.PI + lt * 3)) + 0.1, 120, 0, 0, TAU); ui.stroke(); } for (let i = -2; i <= 2; i++) { ui.beginPath(); ui.ellipse(240, 180 + i * 45, Math.sqrt(1 - (i / 2.7) ** 2) * 120, 10, 0, 0, TAU); ui.stroke(); } }],
['k', () => text('FUTURE', 240, 220, 92, '#EDEDE8', { align: 'center' })],
['k', () => sparkle(240, 180, 120, '#EDEDE8', lt)],
['w', () => { ui.strokeStyle = C.ink; ui.lineWidth = 3; ui.beginPath(); for (let x = 30; x < 450; x += 4) { const y = 180 + Math.sin(x * 0.05 + lt * 20) * 60 * Math.sin(x * 0.012); x === 30 ? ui.moveTo(x, y) : ui.lineTo(x, y); } ui.stroke(); }],
['k', () => text('IS', 240, 240, 170, '#EDEDE8', { align: 'center' })],
['w', () => { ui.fillStyle = C.ink; for (let y = 40; y < 340; y += 26) for (let x = 40; x < 450; x += 26) { const r = 10 * clamp(1 - Math.hypot(x - 240, y - 180) / 200); ui.beginPath(); ui.arc(x, y, r, 0, TAU); ui.fill(); } }],
['k', () => text('>>>>', 240, 230, 120, '#EDEDE8', { align: 'center' })],
['w', () => text('ALREADY', 240, 205, 58, C.ink, { align: 'center' })],
['w', () => text('NOW', 240, 230, 130, C.ink, { align: 'center' })],
['k', () => text('HERE', 240, 220, 110, '#EDEDE8', { align: 'center' })],
];
tiles.forEach(([bgc, fn], i) => { const x = (i % 4) * tw, y = Math.floor(i / 4) * th; if (lt < i * 0.025) return;
ui.save(); ui.beginPath(); ui.rect(x, y, tw, th); ui.clip(); ui.translate(x, y); rect(0, 0, tw, th, bgc === 'k' ? C.ink : '#EDEDE8'); fn(); mono(`0${i + 1} / 12`, 16, th - 16, 11, bgc === 'k' ? '#fff' : C.ink); ui.restore(); });
ui.strokeStyle = 'rgba(128,128,128,.6)'; ui.lineWidth = 1; for (let i = 1; i < 4; i++) line(i * tw, 0, i * tw, H, 'rgba(128,128,128,.6)', 1); for (let i = 1; i < 3; i++) line(0, i * th, W, i * th, 'rgba(128,128,128,.6)', 1);
} };
/* ---------- chrome hero: FUTURE ---------- */
const Hero = (() => {
const scene = new THREE.Scene(); lights(scene, 1.1); const cam = new THREE.PerspectiveCamera(28, W / H, 0.1, 400);
const bg = bgCanvas(scene); const mat = chromeMat('#E6E3FF', 0.12);
// The hero word scales down to fit when it is longer than FUTURE.
const word = wordGroup(CFG.hero, 'lat', 7.6, 3.0, mat, { bevel: 0.4, track: 0.05 }); const fitK = Math.min(1, wordGroup('FUTURE', 'lat', 7.6, 3.0, mat, { bevel: 0.4, track: 0.05 }).userData.width * 1.08 / word.userData.width);
const chars = word.children.slice(); const HX = chars.map(m => m.position.x * fitK); chars.forEach(m => { m.scale.setScalar(fitK); scene.add(m); });
const halo = (() => { const c = document.createElement('canvas'); c.width = W; c.height = H; const g = c.getContext('2d'); g.fillStyle = 'rgba(255,255,255,.55)';
for (let y = 6; y < H; y += 13) for (let x = 6; x < W; x += 13) { const d = Math.hypot((x - 960) / 1.7, (y - 470) / 1.05) / 360; const r = 4.2 * clamp(1 - Math.abs(d - 1) * 1.6); if (r > 0.5) { g.beginPath(); g.arc(x, y, r, 0, TAU); g.fill(); } } return c; })();
return { t0: 0, t1: 4.0, chapter: '04 ARRIVAL', fg: '#fff', three: true, draw(lt) {
target(bg.g); fill(C.blue); bg.g.globalAlpha = EZ.o3(seg(lt, 0, 0.6)); bg.g.drawImage(halo, 0, 0); bg.g.globalAlpha = 1;
text('READY HERE THE FUTURE IS AL', -200 - lt * 60, 860, 210, 'rgba(10,20,140,.35)', { ls: -6 });
bg.g.strokeStyle = 'rgba(255,255,255,.5)'; bg.g.lineWidth = 2; bg.g.beginPath(); bg.g.ellipse(960, 470, 860, 330, -0.08, 0.2 + lt * 0.2, 2.6 + lt * 0.2); bg.g.stroke();
target(); bg.update();
chars.forEach((m, i) => { const s = 0.15 + i * 0.09, p = EZ.o4(seg(lt, s, s + 0.65)), x = HX[i];
m.position.set(lerp(x + (rnd(i) - 0.5) * 60, x, p), lerp(1.5 + (rnd(i + 4) - 0.5) * 40, 1.5, p), lerp(-80, 0, p));
m.rotation.set(lerp((rnd(i + 2) - 0.5) * 6, 0, p) + 0.06 * Math.sin(lt * 1.3 + i), lerp((rnd(i + 3) - 0.5) * 7, 0, p) + (i === 2 ? TAU * EZ.io3(seg(lt, 2.0, 2.6)) : 0) + (i === 4 ? TAU * EZ.io3(seg(lt, 3.0, 3.5)) : 0), lerp((rnd(i + 5) - 0.5) * 2, 0, p)); });
cam.position.set(Math.sin(lt * 0.25) * 6, -1 + Math.cos(lt * 0.3) * 1.5, 62 - 6 * EZ.o3(seg(lt, 0, 1.2))); cam.lookAt(0, 1, 0);
renderer.render(scene, cam);
ui.clearRect(0, 0, W, H);
const a1 = seg(lt, 0.9, 1.4); text(typed(CFG.heroLine, seg(lt, 0.9, 1.3)), 960, 790, 80, '#fff', { align: 'center', ls: -1 });
if (lt > 1.6) { const a2 = EZ.o3(seg(lt, 1.6, 2.0)); { const ws = CFG.headline.split(' '), sz = 56, widths = ws.map(w => measure(w, sz, { ls: 6 })), gap = (W - 200 - widths.reduce((a, b) => a + b, 0)) / (ws.length - 1); let x = 100; ws.forEach((w, i) => { text(w, x, 150, sz, '#fff', { a: a2, ls: 6 }); x += widths[i] + gap; }); }
const letters = [...CFG.hero.replace(/\s/g, '')], span = Math.min(230, 1380 / letters.length); letters.forEach((c, i) => { const x = 960 + (i - (letters.length - 1) / 2) * span - 40; mono(`${c} ${String(i + 1).padStart(2, '0')}`, x, 690, 13, '#fff', { a: a2 }); mono(['FORM', 'UNIT', 'TIME', 'USE', 'RUN', 'EDGE', 'FLOW', 'NEXT'][i % 8], x, 708, 11, '#C8CCFF', { a: a2 }); });
text(CFG.date, 1860, 960, 70, '#fff', { align: 'right', a: a2 }); mono('ARRIVAL DATE', 1860, 880, 13, '#fff', { align: 'right', a: a2 });
barcode(1120, 905, 300, 54, 77, `rgba(255,255,255,${a2})`); mono('NO. 0927-FIH · EDITION 01', 1120, 980, 11, '#fff', { a: a2 });
['THE FUTURE IS NOT TOMORROW.', 'IT IS A SIGNAL YOU ARE', 'ALREADY RECEIVING.', 'TRANSMISSION 23/24 — END'].forEach((s, i) => mono(s, 90, 890 + i * 20, 12, '#fff', { a: a2 }));
brackets(40, 70, W - 80, H - 140, `rgba(255,255,255,${0.7 * a2})`, 24, 1.5);
[[40, 70], [W - 40, 70], [40, H - 70], [W - 40, H - 70]].forEach(([x, y]) => { line(x - 12, y, x + 12, y, '#fff'); line(x, y - 12, x, y + 12, '#fff'); });
sparkle(1530, 260, 26 * EZ.back(seg(lt, 2.0, 2.3)), '#fff', lt); sparkle(400, 600, 18 * EZ.back(seg(lt, 2.2, 2.5)), '#fff', -lt); }
const sw = seg(lt, 3.1, 3.6); if (sw > 0 && sw < 1) { const x = lerp(-400, W + 400, sw); const g = ui.createLinearGradient(x - 200, 0, x + 200, 0); g.addColorStop(0, 'rgba(255,190,255,0)'); g.addColorStop(0.5, 'rgba(255,200,255,.35)'); g.addColorStop(1, 'rgba(255,190,255,0)'); ui.fillStyle = g; ui.save(); ui.transform(1, 0, -0.35, 1, 0, 0); ui.fillRect(x - 200 + 190, 0, 400, H); ui.restore(); }
if (lt < 0.08) { ui.fillStyle = `rgba(255,255,255,${1 - lt / 0.08})`; ui.fillRect(0, 0, W, H); }
} };
})();
/* ---------- end card ---------- */
const End = { t0: 0, t1: 1.5, chapter: '04 ARRIVAL', fg: '#fff', three: false, draw(lt, t, heroCanvas) {
if (lt < 0.45) { // RGB tear over the last hero frame
fill(C.blue); const p = lt / 0.45;
for (let i = 0; i < 26; i++) { const y = Math.floor(rnd(i * 3 + Math.floor(lt * 30)) * H), h = 10 + rnd(i * 7) * 70, dx = (rnd(i * 5 + Math.floor(lt * 40)) - 0.5) * 400 * p;
ui.drawImage(heroCanvas, 0, y, W, h, dx, y, W, h); }
ui.globalCompositeOperation = 'screen'; ui.globalAlpha = 0.6; ui.drawImage(heroCanvas, 18 * p, 0); ui.globalCompositeOperation = 'source-over'; ui.globalAlpha = 1;
rect(lerp(300, 900, p), 0, 70, H, '#000'); rect(0, H * 0.4, W, 6, `rgba(255,0,80,${p})`);
return; }
fill('#050507'); const l = lt - 0.45;
if (l < 0.85) { const s = l < 0.25 ? scramble(CFG.endLine, seg(l, 0, 0.25) * 0.6, 3) + '#&' : CFG.endLine; mono(s.slice(0, 22) + '█', 960, 560, 46, '#fff', { align: 'center', ls: 8 });
if (l > 0.3) { line(700, 590, 1220, 590, 'rgba(255,255,255,.6)'); mono(`${CFG.headline} — ${CFG.brand} — EDITION 01 — ${CFG.date}`, 960, 625, 15, '#fff', { align: 'center' }); } }
else if (Math.floor(l * 6) % 2) rect(955, 540, 10, 18, '#fff');
} };
/* ======== main ======== */
// 20 s highlight cut. Each entry plays a scene from `from` at `speed` (scene-local time).
const EDIT = [
[0.0, 1.5, S1.A, 0.3, 1.2], [1.5, 2.1, S1.B, 0.4, 1], [2.1, 2.5, S1.Csig, 0, 1],
[2.5, 3.6, S1.Eeye, 0, 1.45], [3.6, 4.0, S1.Fpct, 0, 1], [4.0, 5.0, S1.Gtun, 0.5, 1.4],
[5.0, 6.0, S1.Hthe, 0, 1], [6.0, 6.5, S1.J, 0, 0.8], [6.5, 7.0, S1.Kis, 0.45, 1], [7.0, 7.5, S1.Lhere, 0.3, 1],
[7.5, 9.0, S1.Mexe, 0, 1.27], [9.0, 9.5, S1.Nload, 0.4, 1.2],
[9.5, 11.0, Count, 0, 1], [11.0, 13.0, Objects, 0, 1], [13.0, 14.0, Rings, 0, 1], [14.0, 14.5, Checker, 0, 1],
[14.5, 18.5, Hero, 0, 1], [18.5, 20.0, End, 0, 1],
];
const glcv = document.getElementById('gl'), uicv = document.getElementById('ui');
const snap = document.createElement('canvas'); snap.width = W; snap.height = H; const sg = snap.getContext('2d');
function render(t) {
const e = EDIT.find(x => t >= x[0] && t < x[1]) || EDIT[EDIT.length - 1];
const [t0, , sc, from, speed] = e; const cut = t - t0, lt = from + cut * speed;
ui.setTransform(1, 0, 0, 1, 0, 0); ui.globalAlpha = 1; ui.globalCompositeOperation = 'source-over';
if (sc === End) {
Hero.draw(3.95); sg.drawImage(glcv, 0, 0); sg.drawImage(uicv, 0, 0); ui.clearRect(0, 0, W, H);
glcv.style.visibility = 'hidden'; End.draw(cut, t, snap);
} else {
glcv.style.visibility = sc.three ? 'visible' : 'hidden';
if (!sc.three) { ui.clearRect(0, 0, W, H); renderer.setClearColor(0x000000, 1); renderer.clear(); }
sc.draw(lt, t);
}
ui.setTransform(1, 0, 0, 1, 0, 0); ui.globalAlpha = 1;
if (cut < 0.07 && t0 > 0 && sc !== End) {
if (sc.three) { sg.clearRect(0, 0, W, H); sg.drawImage(glcv, 0, 0); sg.drawImage(uicv, 0, 0); }
const src = sc.three ? snap : uicv;
for (let i = 0; i < 6; i++) { const y = Math.floor(rnd(t0 * 13 + i) * H), h = 16 + rnd(t0 * 7 + i) * 70, dx = (rnd(t0 * 5 + i) - 0.5) * 120 * (1 - cut / 0.07); ui.drawImage(src, 0, y, W, h, dx, y, W, h); }
}
hud(t, sc.fg, sc.chapter);
}
/* ---------- master timeline: the only clock ---------- */
gsap.ticker.lagSmoothing(0);
const tl = gsap.timeline({ paused: true });
const clock = { t: 0 };
tl.to(clock, { t: CFG.duration, duration: CFG.duration, ease: 'none' }, 0);
tl.eventCallback('onUpdate', () => render(tl.time()));
window.__timelines = { main: tl };
render(0);
// SIGNAL is sampled from the display font, so resample it once the font is in.
const refresh = () => { SIG = null; render(tl.time()); };
Promise.all(['900 100px UB', '600 20px JBM'].map(f => document.fonts.load(f))).then(refresh, refresh);
/* ---------- preview shell (outside the composition) ---------- */
const main = document.getElementById('main');
const playBtn = document.getElementById('playBtn'), scrub = document.getElementById('scrub'), tcEl = document.getElementById('tc');
function fit() {
const s = Math.min(window.innerWidth / CFG.w, window.innerHeight / CFG.h);
main.style.transform = 'translate(-50%,-50%) scale(' + s + ')';
}
// Inside an Open Design project the motion source player wraps the stage,
// fits it and adds its own controls; leave both to it there.
const hostPlayer = () => window.__odMotionSourcePlayer || main.parentElement.id !== 'viewport';
const ownFit = fit;
fit = function () { if (hostPlayer()) { main.style.transform = 'translate(-50%,-50%)'; return; } ownFit(); };
window.addEventListener('resize', fit); fit();
document.addEventListener('DOMContentLoaded', () => setTimeout(() => {
fit(); if (hostPlayer()) document.getElementById('controls').style.display = 'none';
}, 0));
function sync() {
const t = tl.time();
scrub.value = t; tcEl.textContent = t.toFixed(2) + ' / ' + CFG.duration.toFixed(2);
playBtn.textContent = tl.isActive() ? 'PAUSE' : (t >= CFG.duration - 0.01 ? 'REPLAY' : 'PLAY');
}
gsap.ticker.add(sync);
playBtn.addEventListener('click', () => {
if (tl.time() >= CFG.duration - 0.01) tl.restart(); else if (tl.paused()) tl.play(); else tl.pause();
sync();
});
scrub.addEventListener('input', () => { tl.pause(); tl.time(parseFloat(scrub.value)); sync(); });
window.addEventListener('keydown', e => { if (e.code === 'Space' && e.target === document.body) { e.preventDefault(); playBtn.click(); } });
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) { tl.pause(); tl.time(CFG.reducedAt); }
else document.fonts.ready.then(() => tl.play(0));
sync();
</script>
<script>
// Video export seeks with timeline callbacks suppressed, so the onUpdate
// drawing also runs from a per-frame loop whenever the timeline time moves.
(function install() {
var tls = window.__timelines;
var tl = tls && tls[Object.keys(tls)[0]];
if (!tl) { setTimeout(install, 50); return; }
var draw = tl.eventCallback('onUpdate');
if (!draw) return;
var drawnAt = null;
tl.eventCallback('onUpdate', function () { drawnAt = tl.time(); return draw.apply(this, arguments); });
(function loop() {
if (window.__odFrameRenderer && !document.documentElement.classList.contains('od-export')) {
document.documentElement.classList.add('od-export');
}
requestAnimationFrame(loop);
var t = tl.time();
if (t !== drawnAt) { drawnAt = t; draw(); }
})();
})();
</script>
<script>
// Preview bake: mark ready once the timeline reaches the poster frame.
(function () {
var POSTER_AT = 5.55;
function check() {
var tls = window.__timelines;
var tl = tls && tls[Object.keys(tls)[0]];
if (tl && tl.time() >= POSTER_AT) {
document.documentElement.setAttribute('data-od-preview-ready', 'true');
return;
}
requestAnimationFrame(check);
}
requestAnimationFrame(check);
})();
</script>
<script>
// Sound starts with the film. If the browser blocks unmuted autoplay, the first
// gesture on the page turns it on; the master timeline owns play, pause and seek.
(function() {
// BEGIN shared motion audio sync
// A render stall is not a request to seek the soundtrack. Only correct drift
// after fresh, stable samples; explicit transport actions bypass that filter.
function createMotionAudioSync(audio) {
var sampledAt = null, driftSince = null, previousDrift = 0, correctedAt = -Infinity, align = false;
return function (target, moving, force) {
var now = performance.now(), gap = sampledAt === null ? Infinity : now - sampledAt;
sampledAt = now;
if (force) { align = true; driftSince = null; }
if (align) {
if (audio.readyState > 0) {
if (Math.abs(audio.currentTime - target) > .01) audio.currentTime = target;
align = false; correctedAt = now;
}
return;
}
var drift = target - audio.currentTime;
if (!moving || audio.paused || audio.seeking || audio.readyState < 3 || gap > 200 || Math.abs(drift) <= .3 || now - correctedAt < 1500) {
driftSince = null; previousDrift = drift; return;
}
if (driftSince === null || drift * previousDrift <= 0 || Math.abs(drift - previousDrift) > .1) driftSince = now;
previousDrift = drift;
if (now - driftSince >= 750) {
audio.currentTime = target; correctedAt = now; driftSince = null;
}
};
}
// END shared motion audio sync
const audio=document.getElementById('soundtrack');
if (!audio || typeof audio.play !== 'function' || window.__odFrameRenderer) return;
const controls=document.getElementById('controls');
const button=document.createElement('button');button.type='button';controls.appendChild(button);
// Gallery thumbnails load the page in an opaque sandbox; those stay silent until asked.
let enabled=window.origin!=='null',pending=false,chosen=false;
const timeline=()=>window.__timelines&&window.__timelines.main;
const syncAudio=createMotionAudioSync(audio);
let wasPlaying=false,lastTime=0;
function label() { button.textContent=enabled?'Sound off':'Sound on';button.setAttribute('aria-label',enabled?'Mute sound':'Enable sound');button.setAttribute('aria-pressed',String(enabled)); }
function play() { if(pending||!audio.paused)return;pending=true;audio.play().catch(()=>{enabled=false;label();if(!chosen)waitForGesture();}).finally(()=>{pending=false;}); }
function waitForGesture() {
const start=event=>{
if(event.target===button)return;
document.removeEventListener('pointerdown',start,true);document.removeEventListener('keydown',start,true);
if(chosen||enabled)return;
const tl=timeline();enabled=true;label();if(tl)syncAudio(tl.time(),true,true);play();
};
document.addEventListener('pointerdown',start,true);document.addEventListener('keydown',start,true);
}
label();
button.onclick=()=>{chosen=true;enabled=!enabled;label();const tl=timeline();if(enabled&&tl){if(tl.progress()>=1)tl.restart();else tl.play();syncAudio(tl.time(),true,true);play();}else audio.pause();};
document.getElementById('scrub').addEventListener('input',event=>{if(enabled)syncAudio(Number(event.target.value),false,true);});
function sync() {
if (window.__odMotionSourcePlayer) return;
const tl=timeline();
if(tl&&enabled&&!window.__odFrameRenderer){
const time=tl.time(),moving=!tl.paused()&&tl.progress()<1;
syncAudio(time,moving,(moving&&!wasPlaying)||time<lastTime-.1);
wasPlaying=moving;lastTime=time;
if(moving)play();else audio.pause();
}else { audio.pause();wasPlaying=false; }
requestAnimationFrame(sync);
}sync();
})();
</script>
</body>
</html>