Created
May 9, 2026 23:02
-
-
Save av/43b698f40e3a30f1970af88e697a3605 to your computer and use it in GitHub Desktop.
mi/specter — HyperFrames composition (80s promo)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=1920, height=1080" /> | |
| <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script> | |
| <style> | |
| @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=JetBrains+Mono:wght@100;200;300;400;500;600;700;800&display=swap"); | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| width: 1920px; | |
| height: 1080px; | |
| overflow: hidden; | |
| background: #0e1608; | |
| color: #f0e4d8; | |
| } | |
| #root { | |
| position: relative; | |
| width: 1920px; | |
| height: 1080px; | |
| overflow: hidden; | |
| } | |
| .scene { | |
| position: absolute; | |
| inset: 0; | |
| opacity: 0; | |
| pointer-events: none; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| #grain { | |
| position: absolute; | |
| inset: 0; | |
| z-index: 100; | |
| pointer-events: none; | |
| } | |
| #close-grad { | |
| position: absolute; | |
| inset: 0; | |
| width: 1920px; | |
| height: 1080px; | |
| image-rendering: pixelated; | |
| z-index: 0; | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| #close-dither { | |
| position: absolute; | |
| inset: 0; | |
| width: 1920px; | |
| height: 1080px; | |
| image-rendering: pixelated; | |
| z-index: 1; | |
| pointer-events: none; | |
| } | |
| #vignette { | |
| position: absolute; | |
| inset: 0; | |
| z-index: 99; | |
| pointer-events: none; | |
| background: radial-gradient( | |
| ellipse 75% 70% at 50% 50%, | |
| transparent 50%, | |
| rgba(0, 0, 0, 0.55) 100% | |
| ); | |
| } | |
| .terminal { | |
| position: absolute; | |
| top: 108px; | |
| left: 192px; | |
| right: 192px; | |
| bottom: 108px; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 22px; | |
| line-height: 1.6; | |
| color: #f0e4d8; | |
| overflow: hidden; | |
| } | |
| .terminal-line { | |
| opacity: 0; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| } | |
| .tool-call { | |
| color: #6b7a5a; | |
| } | |
| .tool-result { | |
| color: #6b7a5a; | |
| } | |
| .banner-icon { | |
| color: #e87850; | |
| } | |
| .banner-name { | |
| color: #e87850; | |
| font-family: "Inter", sans-serif; | |
| font-weight: 700; | |
| letter-spacing: -0.6px; | |
| } | |
| .separator { | |
| color: #4a5a3a; | |
| } | |
| .prompt-caret { | |
| color: #f0e4d8; | |
| } | |
| #code-flash-1, | |
| #code-flash-1b, | |
| #code-flash-2 { | |
| position: absolute; | |
| inset: 0; | |
| background: #0e1608; | |
| z-index: 40; | |
| opacity: 0; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 32px; | |
| line-height: 2; | |
| color: #f0e4d8; | |
| } | |
| .code-flash-text { | |
| white-space: pre; | |
| opacity: 0.7; | |
| } | |
| .code-flash-label { | |
| font-size: 20px; | |
| color: #e87850; | |
| margin-bottom: 16px; | |
| letter-spacing: 1px; | |
| } | |
| #camera { | |
| position: absolute; | |
| inset: 0; | |
| z-index: 2; | |
| will-change: transform; | |
| } | |
| #dither-canvas { | |
| position: absolute; | |
| inset: 0; | |
| width: 1920px; | |
| height: 1080px; | |
| image-rendering: pixelated; | |
| z-index: 1; | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .spectrogram-line { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 20px; | |
| line-height: 1.3; | |
| color: #f0e4d8; | |
| white-space: pre; | |
| opacity: 0; | |
| } | |
| #source-camera { | |
| position: absolute; | |
| inset: 0; | |
| will-change: transform; | |
| transform-origin: 0 0; | |
| } | |
| #source-pre { | |
| position: absolute; | |
| left: 80px; | |
| top: 80px; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 15px; | |
| line-height: 1.7; | |
| color: #f0e4d8; | |
| white-space: pre; | |
| overflow: visible; | |
| max-width: none; | |
| } | |
| #source-pre .w1 { font-weight: 100; opacity: 0.25; } | |
| #source-pre .w2 { font-weight: 200; opacity: 0.35; } | |
| #source-pre .w3 { font-weight: 300; opacity: 0.45; } | |
| #source-pre .w4 { font-weight: 400; opacity: 0.58; } | |
| #source-pre .w5 { font-weight: 500; opacity: 0.70; } | |
| #source-pre .w6 { font-weight: 600; opacity: 0.80; } | |
| #source-pre .w7 { font-weight: 700; opacity: 0.90; } | |
| #source-pre .w8 { font-weight: 800; opacity: 1.0; } | |
| #source-pre .kw { color: #e87850; } | |
| #source-pre .st { color: #a0b878; } | |
| #source-pre .nm { color: #d4a07a; } | |
| #source-label { | |
| position: absolute; | |
| bottom: 80px; | |
| right: 160px; | |
| font-family: "Inter", sans-serif; | |
| font-size: 48px; | |
| font-weight: 700; | |
| color: #f0e4d8; | |
| text-align: right; | |
| line-height: 1.2; | |
| opacity: 0; | |
| z-index: 3; | |
| } | |
| #source-label span { | |
| display: block; | |
| font-size: 32px; | |
| font-weight: 400; | |
| color: #a0b090; | |
| } | |
| .close-tagline { | |
| position: relative; | |
| z-index: 1; | |
| font-family: "Inter", sans-serif; | |
| font-weight: 700; | |
| font-size: 80px; | |
| letter-spacing: -3px; | |
| line-height: 1; | |
| color: #f0e4d8; | |
| text-align: center; | |
| opacity: 0; | |
| } | |
| .close-url { | |
| position: relative; | |
| z-index: 1; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 36px; | |
| font-weight: 400; | |
| line-height: 1; | |
| color: #f0e4d8; | |
| text-align: center; | |
| opacity: 0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <svg width="0" height="0" style="position:absolute"> | |
| <filter id="chromab"> | |
| <feColorMatrix type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" in="SourceGraphic" result="r"/> | |
| <feColorMatrix type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" in="SourceGraphic" result="g"/> | |
| <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" in="SourceGraphic" result="b"/> | |
| <feOffset in="r" dx="-1.5" dy="0" result="rs"/> | |
| <feOffset in="b" dx="1.5" dy="0" result="bs"/> | |
| <feBlend mode="screen" in="rs" in2="g" result="rg"/> | |
| <feBlend mode="screen" in="rg" in2="bs"/> | |
| </filter> | |
| </svg> | |
| <div | |
| id="root" | |
| style="filter: url(#chromab)" | |
| data-composition-id="main" | |
| data-start="0" | |
| data-duration="80" | |
| data-width="1920" | |
| data-height="1080" | |
| data-fps="30" | |
| > | |
| <div | |
| id="grain" | |
| class="clip" | |
| data-start="0" | |
| data-duration="80" | |
| data-track-index="10" | |
| ></div> | |
| <div id="vignette"></div> | |
| <audio | |
| id="bgm" | |
| class="clip" | |
| data-start="0" | |
| data-duration="80" | |
| data-track-index="11" | |
| data-media-start="9" | |
| src="mi-theme-2.mp3" | |
| ></audio> | |
| <!-- MAIN SCENE (0–51s): title + terminal in one camera --> | |
| <div | |
| id="main-scene" | |
| class="scene clip" | |
| data-start="0" | |
| data-duration="51" | |
| data-track-index="1" | |
| style=" | |
| background: #0e1608; | |
| align-items: flex-start; | |
| justify-content: flex-start; | |
| " | |
| > | |
| <canvas id="dither-canvas" width="640" height="360"></canvas> | |
| <div id="camera"> | |
| <div class="terminal"> | |
| <!-- Phase 1: Pre-scan content --> | |
| <div id="phase1"> | |
| <div class="terminal-line" id="t-banner" style="opacity: 1"> | |
| <span class="banner-icon" id="t-icon" style="opacity: 0" | |
| >◰ </span | |
| ><span class="banner-name" id="t-name" style="opacity: 0" | |
| >mi</span | |
| > | |
| </div> | |
| <div style="height: 35px"> </div> | |
| <div class="terminal-line" id="t-prompt"> | |
| <span class="prompt-caret">> </span | |
| ><span id="t-prompt-text"></span><span id="t-block-cursor" style="opacity:0">▌</span> | |
| </div> | |
| <div class="terminal-line separator" id="t-sep"> | |
| ───── | |
| </div> | |
| <div class="terminal-line" id="t-a1"></div> | |
| <div class="terminal-line" id="t-a2"></div> | |
| <div class="terminal-line tool-call" id="t-tc1"></div> | |
| <div class="terminal-line tool-result" id="t-tr1"></div> | |
| <div class="terminal-line" id="t-a3"></div> | |
| <div class="terminal-line tool-call" id="t-tc2"></div> | |
| <div class="terminal-line tool-result" id="t-tr2"></div> | |
| <div class="terminal-line" id="t-a4"></div> | |
| <div class="terminal-line" id="t-a5" style="color: #e87850"></div> | |
| <div class="terminal-line" id="t-a6"></div> | |
| <div class="terminal-line tool-call" id="t-tc3"></div> | |
| <div class="terminal-line tool-result" id="t-tr3"></div> | |
| <div class="terminal-line" id="t-a7"></div> | |
| </div> | |
| <!-- Phase 2: Scan + spectrogram reveal --> | |
| <div | |
| id="phase2" | |
| style="opacity: 0; position: absolute; top: 0; left: 0; right: 0" | |
| > | |
| <div class="terminal-line tool-call" id="t-sc1"></div> | |
| <div class="terminal-line tool-call" id="t-sc2"></div> | |
| <div class="terminal-line tool-call" id="t-sc3"></div> | |
| <div class="terminal-line tool-call" id="t-sc4"></div> | |
| <div class="terminal-line tool-call" id="t-sc5"></div> | |
| <div class="terminal-line tool-call" id="t-sc6"></div> | |
| <div class="terminal-line" id="t-a8"></div> | |
| <div style="height: 12px"></div> | |
| <div class="spectrogram-line" id="t-img-0"></div> | |
| <div class="spectrogram-line" id="t-img-1"></div> | |
| <div class="spectrogram-line" id="t-img-2"></div> | |
| <div class="spectrogram-line" id="t-img-3"></div> | |
| <div class="spectrogram-line" id="t-img-4"></div> | |
| <div class="spectrogram-line" id="t-img-5"></div> | |
| <div class="spectrogram-line" id="t-img-6"></div> | |
| <div class="spectrogram-line" id="t-img-7"></div> | |
| <div class="spectrogram-line" id="t-img-8"></div> | |
| <div class="spectrogram-line" id="t-img-9"></div> | |
| <div class="spectrogram-line" id="t-img-10"></div> | |
| <div style="height: 12px"></div> | |
| <div class="terminal-line" id="t-a9"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Code flash overlays (screen-space, outside #camera) --> | |
| <div id="code-flash-1"> | |
| <div> | |
| <div class="code-flash-label">writes: read_wav.mjs</div> | |
| <pre class="code-flash-text"> | |
| const header = readFileSync(path).slice(0, 44); | |
| const channels = header.readUInt16LE(22); | |
| const sampleRate = header.readUInt32LE(24); | |
| const bitDepth = header.readUInt16LE(34);</pre | |
| > | |
| </div> | |
| </div> | |
| <div id="code-flash-1b"> | |
| <div> | |
| <div class="code-flash-label">writes: read_samples.mjs</div> | |
| <pre class="code-flash-text"> | |
| const buf = readFileSync(path); | |
| const start = 44 + offset * 2; | |
| const samples = new Float64Array(count); | |
| for (let i = 0; i < count; i++) | |
| samples[i] = buf.readInt16LE(start + i * 2) / 32768;</pre | |
| > | |
| </div> | |
| </div> | |
| <div id="code-flash-2"> | |
| <div> | |
| <div class="code-flash-label">writes: spectrum.mjs</div> | |
| <pre class="code-flash-text"> | |
| for (let n = 0; n < samples.length; n++) { | |
| const angle = 2 * Math.PI * freq * n / sampleRate; | |
| real += samples[n] * Math.cos(angle); | |
| imag -= samples[n] * Math.sin(angle); | |
| } | |
| return Math.sqrt(real * real + imag * imag);</pre | |
| > | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SOURCE CODE SCENE (51–55s) --> | |
| <div | |
| id="source-scene" | |
| class="scene clip" | |
| data-start="51" | |
| data-duration="4" | |
| data-track-index="1" | |
| style="background: #0e1608" | |
| > | |
| <div id="source-camera"> | |
| <pre id="source-pre"></pre> | |
| </div> | |
| <div id="source-label">30 lines<span>no dependencies</span></div> | |
| </div> | |
| <!-- CLOSE SCENE (55–75s) --> | |
| <div | |
| id="close-scene" | |
| class="scene clip" | |
| data-start="55" | |
| data-duration="25" | |
| data-track-index="1" | |
| style="background: #0e1608; flex-direction: column; gap: 24px" | |
| > | |
| <canvas id="close-grad" width="640" height="360"></canvas> | |
| <canvas id="close-dither" width="640" height="360"></canvas> | |
| <div class="close-tagline" id="close-tagline">a loop, two tools, and an llm</div> | |
| <div class="close-url" id="close-url">github.com/av/mi</div> | |
| </div> | |
| </div> | |
| <script> | |
| // ═══════════════════════════════════════════ | |
| // BAYER 8×8 MATRIX | |
| // ═══════════════════════════════════════════ | |
| const BAYER = [ | |
| [0, 32, 8, 40, 2, 34, 10, 42], | |
| [48, 16, 56, 24, 50, 18, 58, 26], | |
| [12, 44, 4, 36, 14, 46, 6, 38], | |
| [60, 28, 52, 20, 62, 30, 54, 22], | |
| [3, 35, 11, 43, 1, 33, 9, 41], | |
| [51, 19, 59, 27, 49, 17, 57, 25], | |
| [15, 47, 7, 39, 13, 45, 5, 37], | |
| [63, 31, 55, 23, 61, 29, 53, 21], | |
| ]; | |
| // ═══════════════════════════════════════════ | |
| // SEEDED PRNG | |
| // ═══════════════════════════════════════════ | |
| function mulberry32(a) { | |
| return function () { | |
| a |= 0; | |
| a = (a + 0x6d2b79f5) | 0; | |
| let t = Math.imul(a ^ (a >>> 15), 1 | a); | |
| t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; | |
| return ((t ^ (t >>> 14)) >>> 0) / 4294967296; | |
| }; | |
| } | |
| const rand = mulberry32(42); | |
| // ═══════════════════════════════════════════ | |
| // FILM GRAIN | |
| // ═══════════════════════════════════════════ | |
| (function generateGrain() { | |
| const c = document.createElement("canvas"); | |
| c.width = 100; | |
| c.height = 100; | |
| const ctx = c.getContext("2d"); | |
| const img = ctx.createImageData(100, 100); | |
| for (let i = 0; i < img.data.length; i += 4) { | |
| const v = rand() * 255; | |
| img.data[i] = v; | |
| img.data[i + 1] = v; | |
| img.data[i + 2] = v; | |
| img.data[i + 3] = Math.round(0.06 * 255); | |
| } | |
| ctx.putImageData(img, 0, 0); | |
| document.getElementById("grain").style.backgroundImage = | |
| `url(${c.toDataURL()})`; | |
| })(); | |
| // ═══════════════════════════════════════════ | |
| // DITHER REVEAL (BEAT 1A) | |
| // ═══════════════════════════════════════════ | |
| const ditherCanvas = document.getElementById("dither-canvas"); | |
| const ditherCtx = ditherCanvas.getContext("2d"); | |
| const offscreen = document.createElement("canvas"); | |
| offscreen.width = 640; | |
| offscreen.height = 360; | |
| const offCtx = offscreen.getContext("2d"); | |
| function captureText() { | |
| offCtx.clearRect(0, 0, 640, 360); | |
| offCtx.letterSpacing = "-2.67px"; | |
| offCtx.font = "bold 93.33px Inter, sans-serif"; | |
| offCtx.fillStyle = "#f0e4d8"; | |
| offCtx.textAlign = "center"; | |
| offCtx.textBaseline = "middle"; | |
| offCtx.fillText("mi", 320, 180); | |
| textPixels = offCtx.getImageData(0, 0, 640, 360); | |
| } | |
| let textPixels; | |
| captureText(); | |
| document.fonts.ready.then(captureText); | |
| const BG = [14, 22, 8]; | |
| const FG = [240, 228, 216]; | |
| function renderDither(threshold) { | |
| const out = ditherCtx.createImageData(640, 360); | |
| const t = Math.floor(threshold); | |
| for (let y = 0; y < 360; y++) { | |
| for (let x = 0; x < 640; x++) { | |
| const i = (y * 640 + x) * 4; | |
| const isText = textPixels.data[i + 3] > 30; | |
| const show = isText && BAYER[y % 8][x % 8] <= t; | |
| out.data[i] = show ? FG[0] : BG[0]; | |
| out.data[i + 1] = show ? FG[1] : BG[1]; | |
| out.data[i + 2] = show ? FG[2] : BG[2]; | |
| out.data[i + 3] = 255; | |
| } | |
| } | |
| ditherCtx.putImageData(out, 0, 0); | |
| } | |
| renderDither(-1); | |
| // ═══════════════════════════════════════════ | |
| // DITHER GRADIENT RENDERERS (two-point) | |
| // ═══════════════════════════════════════════ | |
| const bgCtx = ditherCtx; | |
| const GW = 640, | |
| GH = 360; | |
| const COL = { | |
| olive: [14, 22, 8], | |
| cream: [240, 228, 216], | |
| coral: [232, 120, 80], | |
| }; | |
| function drawLinear2P(x1, y1, x2, y2, cA, cB) { | |
| const img = bgCtx.createImageData(GW, GH); | |
| const dx = x2 - x1, | |
| dy = y2 - y1; | |
| const len2 = dx * dx + dy * dy || 0.001; | |
| for (let y = 0; y < GH; y++) { | |
| for (let x = 0; x < GW; x++) { | |
| const i = (y * GW + x) * 4; | |
| const t = Math.max( | |
| 0, | |
| Math.min( | |
| 1, | |
| ((x / (GW - 1) - x1) * dx + (y / (GH - 1) - y1) * dy) / len2, | |
| ), | |
| ); | |
| const pick = BAYER[y % 8][x % 8] <= t * 63 ? cB : cA; | |
| img.data[i] = pick[0]; | |
| img.data[i + 1] = pick[1]; | |
| img.data[i + 2] = pick[2]; | |
| img.data[i + 3] = 255; | |
| } | |
| } | |
| bgCtx.putImageData(img, 0, 0); | |
| } | |
| function drawRadial2P(cx, cy, ex, ey, cCenter, cEdge) { | |
| const img = bgCtx.createImageData(GW, GH); | |
| const maxD = Math.sqrt((ex - cx) ** 2 + (ey - cy) ** 2) || 0.001; | |
| for (let y = 0; y < GH; y++) { | |
| for (let x = 0; x < GW; x++) { | |
| const i = (y * GW + x) * 4; | |
| const t = Math.min( | |
| 1, | |
| Math.sqrt((x / (GW - 1) - cx) ** 2 + (y / (GH - 1) - cy) ** 2) / | |
| maxD, | |
| ); | |
| const pick = BAYER[y % 8][x % 8] <= t * 63 ? cEdge : cCenter; | |
| img.data[i] = pick[0]; | |
| img.data[i + 1] = pick[1]; | |
| img.data[i + 2] = pick[2]; | |
| img.data[i + 3] = 255; | |
| } | |
| } | |
| bgCtx.putImageData(img, 0, 0); | |
| } | |
| function drawRainbow2P(x1, y1, x2, y2, colors) { | |
| const img = bgCtx.createImageData(GW, GH); | |
| const dx = x2 - x1, | |
| dy = y2 - y1; | |
| const len2 = dx * dx + dy * dy || 0.001; | |
| const bands = colors.length - 1; | |
| for (let y = 0; y < GH; y++) { | |
| for (let x = 0; x < GW; x++) { | |
| const i = (y * GW + x) * 4; | |
| const t = Math.max( | |
| 0, | |
| Math.min( | |
| 0.999, | |
| ((x / (GW - 1) - x1) * dx + (y / (GH - 1) - y1) * dy) / len2, | |
| ), | |
| ); | |
| const bf = t * bands, | |
| bi = Math.floor(bf), | |
| bt = bf - bi; | |
| const pick = | |
| BAYER[y % 8][x % 8] <= bt * 63 ? colors[bi + 1] : colors[bi]; | |
| img.data[i] = pick[0]; | |
| img.data[i + 1] = pick[1]; | |
| img.data[i + 2] = pick[2]; | |
| img.data[i + 3] = 255; | |
| } | |
| } | |
| bgCtx.putImageData(img, 0, 0); | |
| } | |
| function drawLinearStops(x1, y1, x2, y2, stops) { | |
| const img = bgCtx.createImageData(GW, GH); | |
| const dx = x2 - x1, dy = y2 - y1; | |
| const len2 = dx * dx + dy * dy || 0.001; | |
| for (let y = 0; y < GH; y++) { | |
| for (let x = 0; x < GW; x++) { | |
| const i = (y * GW + x) * 4; | |
| const t = Math.max(0, Math.min(1, ((x / (GW - 1) - x1) * dx + (y / (GH - 1) - y1) * dy) / len2)); | |
| let lo = 0; | |
| for (let s = 1; s < stops.length; s++) { if (stops[s].p <= t) lo = s; } | |
| const hi = Math.min(lo + 1, stops.length - 1); | |
| const span = stops[hi].p - stops[lo].p; | |
| const pick = span < 0.001 ? stops[hi].c | |
| : BAYER[y % 8][x % 8] <= ((t - stops[lo].p) / span) * 63 ? stops[hi].c : stops[lo].c; | |
| img.data[i] = pick[0]; img.data[i+1] = pick[1]; img.data[i+2] = pick[2]; img.data[i+3] = 255; | |
| } | |
| } | |
| bgCtx.putImageData(img, 0, 0); | |
| } | |
| function drawRadialMulti(cx, cy, ex, ey, colors) { | |
| const img = bgCtx.createImageData(GW, GH); | |
| const maxD = Math.sqrt((ex - cx) ** 2 + (ey - cy) ** 2) || 0.001; | |
| const bands = colors.length - 1; | |
| for (let y = 0; y < GH; y++) { | |
| for (let x = 0; x < GW; x++) { | |
| const i = (y * GW + x) * 4; | |
| const t = Math.min( | |
| 0.999, | |
| Math.sqrt((x / (GW - 1) - cx) ** 2 + (y / (GH - 1) - cy) ** 2) / | |
| maxD, | |
| ); | |
| const bf = t * bands, | |
| bi = Math.floor(bf), | |
| bt = bf - bi; | |
| const pick = | |
| BAYER[y % 8][x % 8] <= bt * 63 ? colors[bi + 1] : colors[bi]; | |
| img.data[i] = pick[0]; | |
| img.data[i + 1] = pick[1]; | |
| img.data[i + 2] = pick[2]; | |
| img.data[i + 3] = 255; | |
| } | |
| } | |
| bgCtx.putImageData(img, 0, 0); | |
| } | |
| const edgeBlobs = { | |
| ax: 0, | |
| ay: 200, | |
| ar: 30, | |
| bx: 640, | |
| by: 290, | |
| br: 28, | |
| cx: 450, | |
| cy: 360, | |
| cr: 25, | |
| dx: 200, | |
| dy: 0, | |
| dr: 22, | |
| ex: 640, | |
| ey: 80, | |
| er: 20, | |
| tlAngle: 0.7, | |
| tlReach: 0.35, | |
| brAngle: 0.7, | |
| brReach: 0.35, | |
| f1x: 130, | |
| f1y: 170, | |
| f1r: 110, | |
| f2x: 430, | |
| f2y: 60, | |
| f2r: 90, | |
| f3x: 520, | |
| f3y: 270, | |
| f3r: 120, | |
| f4x: 280, | |
| f4y: 320, | |
| f4r: 85, | |
| f5x: 70, | |
| f5y: 50, | |
| f5r: 80, | |
| f6x: 500, | |
| f6y: 160, | |
| f6r: 95, | |
| f7x: 320, | |
| f7y: 180, | |
| f7r: 140, | |
| }; | |
| const BEIGE = [75, 82, 55]; | |
| const BG_PARALLAX = 0.6; | |
| function bgParallax() { | |
| const s = gsap.getProperty("#camera", "scale") || 1; | |
| const cx = gsap.getProperty("#camera", "x") || 0; | |
| const cy = gsap.getProperty("#camera", "y") || 0; | |
| return { | |
| ox: cx * BG_PARALLAX / 1920 * 640, | |
| oy: cy * BG_PARALLAX / 1080 * 360, | |
| z: 1 + (s - 1) * BG_PARALLAX, | |
| }; | |
| } | |
| function renderEdgeDither() { | |
| const { ox, oy, z } = bgParallax(); | |
| const xformBlob = (x, y, r) => ({ | |
| x: (x - 320) * z + 320 + ox, | |
| y: (y - 180) * z + 180 + oy, | |
| r: r * z, | |
| }); | |
| const defs = [ | |
| { ...xformBlob(edgeBlobs.ax, edgeBlobs.ay, edgeBlobs.ar), col: COL.coral }, | |
| { ...xformBlob(edgeBlobs.bx, edgeBlobs.by, edgeBlobs.br), col: COL.coral }, | |
| { ...xformBlob(edgeBlobs.cx, edgeBlobs.cy, edgeBlobs.cr), col: COL.cream }, | |
| { ...xformBlob(edgeBlobs.dx, edgeBlobs.dy, edgeBlobs.dr), col: COL.cream }, | |
| { ...xformBlob(edgeBlobs.ex, edgeBlobs.ey, edgeBlobs.er), col: COL.coral }, | |
| ]; | |
| const img = ditherCtx.createImageData(640, 360); | |
| // Angled dither washes from two corners (drift via edgeBlobs params) | |
| const { tlAngle, tlReach, brAngle, brReach } = edgeBlobs; | |
| const nox = ox / 639, noy = oy / 359; | |
| for (let py = 0; py < 360; py++) { | |
| for (let px = 0; px < 640; px++) { | |
| const nx = px / 639 - nox, | |
| ny = py / 359 - noy; | |
| const tTL = Math.max(0, 1 - (nx * tlAngle + ny) / (tlReach * z)); | |
| const tBR = Math.max( | |
| 0, | |
| 1 - ((1 - nx) * brAngle + (1 - ny)) / (brReach * z), | |
| ); | |
| const maxT = Math.max(tTL, tBR); | |
| if (maxT > 0 && BAYER[py % 8][px % 8] <= maxT * 32) { | |
| const i = (py * 640 + px) * 4; | |
| img.data[i] = BEIGE[0]; | |
| img.data[i + 1] = BEIGE[1]; | |
| img.data[i + 2] = BEIGE[2]; | |
| img.data[i + 3] = 255; | |
| } | |
| } | |
| } | |
| // Large faint ambient blobs | |
| const ambient = [ | |
| xformBlob(edgeBlobs.f1x, edgeBlobs.f1y, edgeBlobs.f1r), | |
| xformBlob(edgeBlobs.f2x, edgeBlobs.f2y, edgeBlobs.f2r), | |
| xformBlob(edgeBlobs.f3x, edgeBlobs.f3y, edgeBlobs.f3r), | |
| xformBlob(edgeBlobs.f4x, edgeBlobs.f4y, edgeBlobs.f4r), | |
| xformBlob(edgeBlobs.f5x, edgeBlobs.f5y, edgeBlobs.f5r), | |
| xformBlob(edgeBlobs.f6x, edgeBlobs.f6y, edgeBlobs.f6r), | |
| xformBlob(edgeBlobs.f7x, edgeBlobs.f7y, edgeBlobs.f7r), | |
| ]; | |
| for (const b of ambient) { | |
| if (b.r < 1) continue; | |
| const x0 = Math.max(0, Math.floor(b.x - b.r)); | |
| const x1 = Math.min(639, Math.ceil(b.x + b.r)); | |
| const y0 = Math.max(0, Math.floor(b.y - b.r)); | |
| const y1 = Math.min(359, Math.ceil(b.y + b.r)); | |
| for (let py = y0; py <= y1; py++) { | |
| for (let px = x0; px <= x1; px++) { | |
| const d = Math.sqrt((px - b.x) ** 2 + (py - b.y) ** 2); | |
| if (d >= b.r) continue; | |
| const t = 1 - d / b.r; | |
| if (BAYER[py % 8][px % 8] <= t * 14) { | |
| const i = (py * 640 + px) * 4; | |
| img.data[i] = BEIGE[0]; | |
| img.data[i + 1] = BEIGE[1]; | |
| img.data[i + 2] = BEIGE[2]; | |
| img.data[i + 3] = 255; | |
| } | |
| } | |
| } | |
| } | |
| // Radial blobs on top | |
| for (const b of defs) { | |
| if (b.r < 1) continue; | |
| const x0 = Math.max(0, Math.floor(b.x - b.r)); | |
| const x1 = Math.min(639, Math.ceil(b.x + b.r)); | |
| const y0 = Math.max(0, Math.floor(b.y - b.r)); | |
| const y1 = Math.min(359, Math.ceil(b.y + b.r)); | |
| for (let py = y0; py <= y1; py++) { | |
| for (let px = x0; px <= x1; px++) { | |
| const d = Math.sqrt((px - b.x) ** 2 + (py - b.y) ** 2); | |
| if (d >= b.r) continue; | |
| const t = 1 - d / b.r; | |
| if (BAYER[py % 8][px % 8] <= t * 63) { | |
| const i = (py * 640 + px) * 4; | |
| img.data[i] = b.col[0]; | |
| img.data[i + 1] = b.col[1]; | |
| img.data[i + 2] = b.col[2]; | |
| img.data[i + 3] = 255; | |
| } | |
| } | |
| } | |
| } | |
| ditherCtx.putImageData(img, 0, 0); | |
| } | |
| // ═══════════════════════════════════════════ | |
| // CLOSE SCENE DITHER | |
| // ═══════════════════════════════════════════ | |
| const closeCanvas = document.getElementById("close-dither"); | |
| const closeCtx = closeCanvas.getContext("2d"); | |
| const closeGradCanvas = document.getElementById("close-grad"); | |
| const closeGradCtx = closeGradCanvas.getContext("2d"); | |
| function drawCloseRadial(cx, cy, ex, ey, colors) { | |
| const img = closeGradCtx.createImageData(GW, GH); | |
| const maxD = Math.sqrt((ex - cx) ** 2 + (ey - cy) ** 2) || 0.001; | |
| const bands = colors.length - 1; | |
| for (let y = 0; y < GH; y++) { | |
| for (let x = 0; x < GW; x++) { | |
| const i = (y * GW + x) * 4; | |
| const t = Math.min(0.999, Math.sqrt((x / (GW - 1) - cx) ** 2 + (y / (GH - 1) - cy) ** 2) / maxD); | |
| const bf = t * bands, bi = Math.floor(bf), bt = bf - bi; | |
| const pick = BAYER[y % 8][x % 8] <= bt * 63 ? colors[bi + 1] : colors[bi]; | |
| img.data[i] = pick[0]; img.data[i + 1] = pick[1]; img.data[i + 2] = pick[2]; img.data[i + 3] = 255; | |
| } | |
| } | |
| closeGradCtx.putImageData(img, 0, 0); | |
| } | |
| const metaT = { v: 0 }; | |
| const metaDefs = [ | |
| { cx: 320, cy: 180, rx: 200, ry: 120, fx: 1.0, fy: 1.3, px: 0.0, py: 0.0, r: 55 }, | |
| { cx: 320, cy: 180, rx: 180, ry: 140, fx: 0.7, fy: 1.0, px: 2.1, py: 0.8, r: 50 }, | |
| { cx: 320, cy: 180, rx: 150, ry: 100, fx: 1.3, fy: 0.8, px: 1.0, py: 3.5, r: 48 }, | |
| { cx: 320, cy: 180, rx: 220, ry: 90, fx: 0.5, fy: 1.1, px: 4.2, py: 1.7, r: 42 }, | |
| { cx: 320, cy: 180, rx: 160, ry: 130, fx: 0.9, fy: 0.6, px: 3.0, py: 5.0, r: 45 }, | |
| { cx: 320, cy: 180, rx: 130, ry: 110, fx: 1.1, fy: 1.4, px: 5.5, py: 2.3, r: 40 }, | |
| { cx: 320, cy: 180, rx: 190, ry: 80, fx: 0.6, fy: 0.9, px: 1.5, py: 4.0, r: 38 }, | |
| ]; | |
| function renderMetaballs() { | |
| const t = metaT.v * Math.PI * 2; | |
| const img = closeCtx.createImageData(640, 360); | |
| const balls = metaDefs.map(m => ({ | |
| x: m.cx + Math.sin(t * m.fx + m.px) * m.rx, | |
| y: m.cy + Math.sin(t * m.fy + m.py) * m.ry, | |
| r2: m.r * m.r, | |
| })); | |
| const n = balls.length; | |
| for (let py = 0; py < 360; py++) { | |
| for (let px = 0; px < 640; px++) { | |
| let field = 0; | |
| for (let i = 0; i < n; i++) { | |
| const dx = px - balls[i].x, dy = py - balls[i].y; | |
| field += balls[i].r2 / (dx * dx + dy * dy + 1); | |
| } | |
| const bayer = BAYER[py % 8][px % 8]; | |
| let col; | |
| if (field > 1.2) { | |
| col = COL.coral; | |
| } else if (field > 0.5) { | |
| col = bayer <= ((field - 0.5) / 0.7) * 63 ? COL.coral : BEIGE; | |
| } else if (field > 0.15) { | |
| if (bayer <= ((field - 0.15) / 0.35) * 28) col = BEIGE; | |
| } | |
| if (col) { | |
| const i = (py * 640 + px) * 4; | |
| img.data[i] = col[0]; | |
| img.data[i + 1] = col[1]; | |
| img.data[i + 2] = col[2]; | |
| img.data[i + 3] = 255; | |
| } | |
| } | |
| } | |
| closeCtx.putImageData(img, 0, 0); | |
| } | |
| // ═══════════════════════════════════════════ | |
| // SOURCE CODE (actual mi/index.mjs) | |
| // ═══════════════════════════════════════════ | |
| const sourceLines = [ | |
| [2, "#!/usr/bin/env node"], | |
| [3, "import { createInterface } from 'readline'; import { readFileSync, existsSync, readdirSync } from 'fs'; import { spawn } from 'child_process'; import { homedir } from 'os';"], | |
| [5, "Object.assign(global, { spawn, readFileSync, existsSync, readdirSync, homedir }); const DIR = new URL('.', import.meta.url).pathname;"], | |
| [5, "Object.assign(process.env, { MI_DIR: DIR, MI_PATH: new URL(import.meta.url).pathname });"], | |
| [5, "if (!process.env.OPENAI_API_KEY && !process.argv.includes('-h')) { console.error('OPENAI_API_KEY required'); process.exit(1); }"], | |
| [6, "const toolMods = await Promise.all(readdirSync(`${DIR}tools`).filter(f => f.endsWith('.mjs')).map(f => import(`${DIR}tools/${f}`))),"], | |
| [6, " defs = toolMods.map(m => m.default), gray = s => `\\x1b[90m${s}\\x1b[0m`, red = s => `\\x1b[31m${s}\\x1b[0m`, orange = s => `\\x1b[38;5;208m${s}\\x1b[0m`, { listSkills } = toolMods.find(m => m.listSkills);"], | |
| [6, "const tools = Object.fromEntries(defs.map(d => [d.name, d.handler])), toolSchemas = defs.map(d => ({ type: 'function', function: { name: d.name, description: d.description, parameters: d.parameters } }));"], | |
| [8, "async function run(messages) { while (true) {"], | |
| [7, " const response = await fetch(`${(process.env.OPENAI_BASE_URL || 'https://api.openai.com').replace(/\\/+$/, '')}/v1/chat/completions`,"], | |
| [7, " { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },"], | |
| [7, " body: JSON.stringify({ model: process.env.MODEL || 'gpt-5.4', messages, tools: toolSchemas, stream: true }) });"], | |
| [5, " if (!response.ok) { const body = await response.json().catch(() => ({})); throw new Error(body.error?.message || `HTTP ${response.status}`); }"], | |
| [6, " const message = { role: 'assistant', content: '' }, decoder = new TextDecoder(); let buffer = '';"], | |
| [7, " for await (const chunk of response.body) { buffer += decoder.decode(chunk, { stream: true }); let pos;"], | |
| [7, " while ((pos = buffer.indexOf('\\n\\n')) >= 0) { const event = buffer.slice(0, pos); buffer = buffer.slice(pos + 2);"], | |
| [6, " for (const line of event.split('\\n')) { if (!line.startsWith('data: ')) continue; const payload = line.slice(6);"], | |
| [6, " if (payload === '[DONE]') continue; let json; try { json = JSON.parse(payload); } catch { continue; }"], | |
| [6, " if (json.error) throw new Error(json.error.message); const delta = json.choices?.[0]?.delta; if (!delta) continue;"], | |
| [6, " if (delta.content) { process.stdout.write(delta.content); message.content += delta.content; }"], | |
| [7, " if (delta.tool_calls) { message.tool_calls ||= []; for (const tc of delta.tool_calls) {"], | |
| [7, " const slot = message.tool_calls[tc.index] ||= { id: '', type: 'function', function: { name: '', arguments: '' } };"], | |
| [7, " if (tc.id) slot.id = tc.id; if (tc.type) slot.type = tc.type; const fn = tc.function; if (fn?.name) slot.function.name += fn.name; if (fn?.arguments) slot.function.arguments += fn.arguments;"], | |
| [7, " } } } } } if (message.content) process.stdout.write('\\n'); messages.push(message); if (!message.tool_calls) return;"], | |
| [8, " for (const toolCall of message.tool_calls) { const { name, arguments: rawArgs } = toolCall.function, args = JSON.parse(rawArgs);"], | |
| [7, " console.log(gray(`⟡ ${name}(${JSON.stringify(args)})`)); const result = String(await tools[name](args));"], | |
| [6, " console.log(gray(result.length > 200 ? `${result.slice(0, 200)}…` : result)); messages.push({ role: 'tool', tool_call_id: toolCall.id, content: result }); } } }"], | |
| [4, "const DEFAULT_PROMPT = 'You are mi, an autonomous agent. You run in a raw terminal. Be concise. Act rather than speculate. ...';"], | |
| [5, "const SYSTEM = (process.env.SYSTEM_PROMPT || DEFAULT_PROMPT) + `\\nCWD: ${process.cwd()}\\nDate: ${new Date().toISOString()}`;"], | |
| [5, "const history = [{ role: 'system', content: SYSTEM }], getArg = key => { const i = process.argv.indexOf(key); return i >= 0 && process.argv[i + 1]; };"], | |
| [4, "if (process.argv.includes('-h')) { console.log('usage: mi [-p prompt] [-f file] [-h]'); process.exit(0); }"], | |
| [5, "const sysMsg = history[0], fileArg = getArg('-f'); if (fileArg) sysMsg.content += `\\nFile (${fileArg}):\\n${readFileSync(fileArg, 'utf8')}`;"], | |
| [5, "if (existsSync('AGENTS.md')) sysMsg.content += `\\n${readFileSync('AGENTS.md', 'utf8')}`; const skills = listSkills(); if (skills.length) sysMsg.content += `\\nSkills:\\n${skills.join('\\n')}`;"], | |
| [6, "const prompt = getArg('-p'); if (prompt) { history.push({ role: 'user', content: prompt }); await run(history); process.exit(0); }"], | |
| [5, "if (!process.stdin.isTTY) { let input = ''; for await (const chunk of process.stdin) input += chunk; history.push({ role: 'user', content: input.trim() }); await run(history); process.exit(0); }"], | |
| [6, "const rl = createInterface({ input: process.stdin, output: process.stdout }); const ask = q => new Promise(r => rl.question(q, r));"], | |
| [5, "const version = JSON.parse(readFileSync(`${DIR}package.json`, 'utf8')).version; console.log(`${orange('◰ mi')}${gray(`/${version}`)}`);"], | |
| [8, "rl.on('close', () => process.exit(0)); while (true) { const input = await ask('\\n> '); if (input === '/reset') { history.splice(1); console.log(gray('✓ reset')); continue; }"], | |
| [7, " if (input.trim()) { history.push({ role: 'user', content: input }); try { await run(history); } catch (e) { console.error(red(`✗ ${e.message}`)); history.pop(); } } }"], | |
| ]; | |
| function esc(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } | |
| function highlight(text) { | |
| let h = esc(text); | |
| h = h.replace(/('(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, '<span class="st">$1</span>'); | |
| h = h.replace(/\b(const|let|if|for|while|return|await|async|function|import|from|continue|throw|new|true|false|of|in|try|catch)\b/g, '<span class="kw">$1</span>'); | |
| h = h.replace(/\b(\d+)\b/g, '<span class="nm">$1</span>'); | |
| return h; | |
| } | |
| document.getElementById("source-pre").innerHTML = sourceLines | |
| .map(([w, text]) => `<span class="w${w}">${highlight(text)}</span>`) | |
| .join("\n"); | |
| // ═══════════════════════════════════════════ | |
| // SPECTROGRAM DATA | |
| // ═══════════════════════════════════════════ | |
| const spectrogramLines = [ | |
| " 7000 | |", | |
| " 5375 | ██ |", | |
| " 4833 | |", | |
| " 4292 | ██ ██ ██ |", | |
| " 3750 | ██████████ ██ |", | |
| " 3208 | ██ ██ ██ ██ |", | |
| " 2667 | ██ ██ ██ ██ |", | |
| " 2125 | ██ ██ ██ ██ |", | |
| " 1583 | ██ ██ ██ ██ |", | |
| " 1042 | |", | |
| " 500 | |", | |
| ]; | |
| spectrogramLines.forEach((line, i) => { | |
| document.getElementById("t-img-" + i).textContent = line; | |
| }); | |
| // ═══════════════════════════════════════════ | |
| // TERMINAL CONTENT | |
| // ═══════════════════════════════════════════ | |
| document.getElementById("t-tc1").textContent = | |
| '⟡ read_wav({path: "mystery.wav"})'; | |
| document.getElementById("t-tr1").textContent = | |
| '{format: "PCM", sampleRate: 44100, channels: 1, bitDepth: 16, duration: "6.4s"}'; | |
| document.getElementById("t-tc2").textContent = | |
| '⟡ read_samples({path: "mystery.wav", offset: 0, count: 2048})'; | |
| document.getElementById("t-tr2").textContent = | |
| "[0.012, -0.034, 0.067, -0.023, 0.089, -0.056, 0.041, ...]"; | |
| document.getElementById("t-tc3").textContent = | |
| '⟡ spectrum({path: "mystery.wav", offset: 0, freqs: [500, 1042, 1583, 2125, 2667, 3208, 3750, 4292, 4833, 5375]})'; | |
| document.getElementById("t-tr3").textContent = | |
| "{500: 0.001, 1042: 0.002, 1583: 0.082, 2125: 0.079, 2667: 0.081, 3208: 0.077, 3750: 0.091, 4292: 0.003, 4833: 0.001, 5375: 0.001}"; | |
| document.getElementById("t-sc1").textContent = | |
| "⟡ spectrum({..., offset: 0}) → 1583: 0.082, 3750: 0.091 ..."; | |
| document.getElementById("t-sc2").textContent = | |
| "⟡ spectrum({..., offset: 17640}) → 1583: 0.079, 3750: 0.003 ..."; | |
| document.getElementById("t-sc3").textContent = | |
| "⟡ spectrum({..., offset: 35280}) → 1583: 0.002, 2667: 0.081 ..."; | |
| document.getElementById("t-sc4").textContent = | |
| "⟡ spectrum({..., offset: 52920}) → 1583: 0.002, 2125: 0.078 ..."; | |
| document.getElementById("t-sc5").textContent = | |
| "⟡ spectrum({..., offset: 70560}) → 1583: 0.081, 3750: 0.088 ..."; | |
| document.getElementById("t-sc6").textContent = | |
| "⟡ spectrum({..., offset: 88200}) → 2125: 0.002, 3208: 0.077 ..."; | |
| // ═══════════════════════════════════════════ | |
| // TEXT ANIMATION HELPERS | |
| // ═══════════════════════════════════════════ | |
| function tokenStream(tl, id, text, start, charDelay) { | |
| const el = document.getElementById(id); | |
| tl.set(el, { opacity: 1 }, start); | |
| const tokens = []; | |
| const words = text.match(/\s*[^\s]+/g) || [text]; | |
| for (const w of words) { | |
| if (w.length <= 5) { | |
| tokens.push(w); | |
| continue; | |
| } | |
| let p = 0; | |
| while (p < w.length) { | |
| const len = Math.min(2 + Math.floor(rand() * 3), w.length - p); | |
| tokens.push(w.slice(p, p + len)); | |
| p += len; | |
| } | |
| } | |
| let t = 0; | |
| let shown = ""; | |
| for (const tok of tokens) { | |
| shown += tok; | |
| const snap = shown; | |
| tl.call( | |
| () => { | |
| el.textContent = snap; | |
| }, | |
| null, | |
| start + t, | |
| ); | |
| t += tok.length * charDelay * (0.7 + rand() * 0.6); | |
| } | |
| } | |
| function userType(tl, id, text, start, charDelay) { | |
| const el = document.getElementById(id); | |
| tl.set(el, { opacity: 1 }, start); | |
| let t = 0; | |
| for (let i = 0; i <= text.length; i++) { | |
| const idx = i; | |
| tl.call( | |
| () => { | |
| el.textContent = text.slice(0, idx) + "▌"; | |
| }, | |
| null, | |
| start + t, | |
| ); | |
| let delay = charDelay * (0.5 + rand() * 1.0); | |
| const ch = text[idx]; | |
| if (ch === " ") delay += charDelay * rand() * 0.8; | |
| else if (ch === "." || ch === "," || ch === "—") | |
| delay += charDelay * (0.5 + rand() * 1.0); | |
| t += delay; | |
| } | |
| tl.call( | |
| () => { | |
| el.textContent = text; | |
| }, | |
| null, | |
| start + t + 0.15, | |
| ); | |
| } | |
| // ═══════════════════════════════════════════ | |
| // GSAP TIMELINE | |
| // ═══════════════════════════════════════════ | |
| window.__timelines = window.__timelines || {}; | |
| const tl = gsap.timeline({ paused: true }); | |
| const ditherProxy = { threshold: -1 }; | |
| // ── TITLE SEQUENCE (0–8s) — beat-synced to audio ── | |
| // Audio: silence 0–0.4, build 0.5–3.0, hit 3.0, break 3.2, | |
| // groove 3.5–6.0, break 6.0, loud 7.0–8.0 | |
| const gp = { x1: 0, y1: 0, x2: 0, y2: 0 }; | |
| // 10 beats on 120 BPM grid, 0.5 movement, multi-stop radials | |
| // Gradients render on same canvas as dither — no element swap, no jump | |
| const beats = [ | |
| [ | |
| 3.0, | |
| 0.5, | |
| [2.5, 0.3, -0.8, 0.8], | |
| [1.5, 0.5, -0.5, 0.6], | |
| [COL.coral, COL.olive, COL.cream], | |
| "#f0e4d8", | |
| "power2.out", | |
| ], | |
| [ | |
| 3.5, | |
| 0.5, | |
| [-1.5, 0.5, 2.0, 0.5], | |
| [-0.5, 0.3, 1.8, 0.7], | |
| [COL.olive, COL.cream], | |
| "#1e2818", | |
| "power1.inOut", | |
| ], | |
| [ | |
| 4.0, | |
| 0.5, | |
| [0.5, -1.5, 0.5, 2.0], | |
| [0.3, -0.5, 0.7, 1.8], | |
| [COL.cream, COL.olive, COL.coral], | |
| "#f0e4d8", | |
| "power1.inOut", | |
| ], | |
| [ | |
| 4.5, | |
| 0.5, | |
| [-1.2, 2.0, 1.8, -0.8], | |
| [-0.3, 1.2, 1.5, -0.3], | |
| [COL.olive, COL.coral, COL.cream], | |
| "#1e2818", | |
| "power1.inOut", | |
| ], | |
| [ | |
| 5.0, | |
| 0.5, | |
| [2.2, 0.8, -0.8, 0.2], | |
| [1.3, 0.6, -0.6, 0.4], | |
| [COL.coral, COL.cream], | |
| "#1e2818", | |
| "power1.inOut", | |
| ], | |
| [ | |
| 5.5, | |
| 0.5, | |
| [-1.2, -1.2, 2.0, 2.0], | |
| [-0.3, -0.3, 1.7, 1.7], | |
| [COL.cream, COL.olive], | |
| "#f0e4d8", | |
| "power1.inOut", | |
| ], | |
| ]; | |
| bgCtx.letterSpacing = "-2.67px"; | |
| bgCtx.font = "bold 93.33px Inter, sans-serif"; | |
| bgCtx.textAlign = "center"; | |
| bgCtx.textBaseline = "middle"; | |
| document.fonts.ready.then(() => { | |
| bgCtx.font = "bold 93.33px Inter, sans-serif"; | |
| }); | |
| // ── TITLE (0–6s): camera zoomed on banner "mi", dither overlay ── | |
| // Banner "mi" center is at ~(232, 126). Camera zooms ~12.7x so it fills screen. | |
| // Dither canvas sits above camera at z-index 50, renders its own "mi" at screen center. | |
| tl.set("#main-scene", { opacity: 1 }, 0); | |
| tl.set( | |
| "#camera", | |
| { scale: 12.7, x: -1965, y: -1070, transformOrigin: "0 0" }, | |
| 0, | |
| ); | |
| tl.set("#dither-canvas", { opacity: 1 }, 0); | |
| tl.to( | |
| ditherProxy, | |
| { | |
| threshold: 63, | |
| duration: 2.0, | |
| ease: "power2.in", | |
| onUpdate: () => renderDither(ditherProxy.threshold), | |
| }, | |
| 0, | |
| ); | |
| for (const [t, d, from, to, colors, textCol, ease] of beats) { | |
| tl.fromTo( | |
| gp, | |
| { x1: from[0], y1: from[1], x2: from[2], y2: from[3] }, | |
| { | |
| x1: to[0], | |
| y1: to[1], | |
| x2: to[2], | |
| y2: to[3], | |
| duration: d, | |
| ease, | |
| onUpdate: () => { | |
| drawRadialMulti(gp.x1, gp.y1, gp.x2, gp.y2, colors); | |
| bgCtx.fillStyle = textCol; | |
| bgCtx.fillText("mi", 320, 180); | |
| }, | |
| }, | |
| t, | |
| ); | |
| } | |
| // ── TRANSITION (6–8s): dither fades, ◰ mi with gradient sweep ── | |
| tl.to("#dither-canvas", { opacity: 0, duration: 0.3 }, 5.85); | |
| tl.to("#t-name", { opacity: 1, duration: 0.15 }, 6.0); | |
| tl.to("#t-icon", { opacity: 1, duration: 0.2, ease: "power2.out" }, 6.3); | |
| // ── Single large sweep: olive → slightly lighter olive (6.3–7.8s) ── | |
| const OLIVE_LIGHT = [75, 95, 52]; | |
| const sweepPos = { x1: -1.5, y1: -0.8, x2: -0.2, y2: 0.5 }; | |
| function renderSweep() { | |
| drawLinearStops(sweepPos.x1, sweepPos.y1, sweepPos.x2, sweepPos.y2, [ | |
| { p: 0.0, c: COL.olive }, | |
| { p: 1.0, c: OLIVE_LIGHT }, | |
| ]); | |
| } | |
| tl.to("#dither-canvas", { opacity: 0.7, duration: 0.3 }, 6.3); | |
| tl.to(sweepPos, { | |
| x1: 1.2, y1: 0.5, x2: 2.5, y2: 1.8, | |
| duration: 1.5, ease: "power2.inOut", | |
| onUpdate: renderSweep, | |
| }, 6.3); | |
| tl.to("#dither-canvas", { opacity: 0, duration: 0.2 }, 7.8); | |
| tl.call(() => renderEdgeDither(), null, 8.0); | |
| tl.to("#dither-canvas", { opacity: 0.7, duration: 0.3 }, 9.5); | |
| // Edge blob drift through terminal scenes | |
| tl.to( | |
| edgeBlobs, | |
| { | |
| ay: 250, | |
| ar: 18, | |
| by: 110, | |
| br: 35, | |
| cx: 260, | |
| cr: 15, | |
| dx: 420, | |
| dr: 30, | |
| ey: 180, | |
| er: 28, | |
| tlAngle: 0.55, | |
| tlReach: 0.3, | |
| brAngle: 0.85, | |
| brReach: 0.4, | |
| f1x: 180, | |
| f1y: 220, | |
| f1r: 120, | |
| f2x: 380, | |
| f2y: 100, | |
| f2r: 100, | |
| f3x: 480, | |
| f3y: 230, | |
| f3r: 130, | |
| f4x: 320, | |
| f4y: 290, | |
| f4r: 95, | |
| f5x: 100, | |
| f5y: 80, | |
| f5r: 90, | |
| f6x: 460, | |
| f6y: 200, | |
| f6r: 105, | |
| f7x: 280, | |
| f7y: 210, | |
| f7r: 150, | |
| duration: 18.5, | |
| ease: "sine.inOut", | |
| onUpdate: renderEdgeDither, | |
| }, | |
| 9.5, | |
| ); | |
| tl.to( | |
| edgeBlobs, | |
| { | |
| ay: 330, | |
| ar: 35, | |
| by: 230, | |
| br: 15, | |
| cx: 380, | |
| cr: 30, | |
| dx: 280, | |
| dr: 12, | |
| ey: 310, | |
| er: 22, | |
| tlAngle: 0.8, | |
| tlReach: 0.38, | |
| brAngle: 0.6, | |
| brReach: 0.32, | |
| f1x: 100, | |
| f1y: 250, | |
| f1r: 105, | |
| f2x: 470, | |
| f2y: 40, | |
| f2r: 85, | |
| f3x: 560, | |
| f3y: 300, | |
| f3r: 115, | |
| f4x: 240, | |
| f4y: 340, | |
| f4r: 80, | |
| f5x: 50, | |
| f5y: 30, | |
| f5r: 70, | |
| f6x: 530, | |
| f6y: 140, | |
| f6r: 100, | |
| f7x: 350, | |
| f7y: 160, | |
| f7r: 135, | |
| duration: 23, | |
| ease: "sine.inOut", | |
| onUpdate: renderEdgeDither, | |
| }, | |
| 28, | |
| ); | |
| tl.to("#dither-canvas", { opacity: 0, duration: 0.3 }, 50.7); | |
| // 8s: slide from "mi" to prompt area, zoom to reading distance (~3.5x) | |
| // Prompt text starts at ~x=218, y=195. Center on left portion of prompt. | |
| tl.to( | |
| "#camera", | |
| { scale: 3.5, x: -510, y: -142, duration: 0.35, ease: "power2.out" }, | |
| 8, | |
| ); | |
| tl.to("#t-prompt", { opacity: 1, duration: 0.15 }, 8.5); | |
| tl.to("#t-block-cursor", { opacity: 1, duration: 0.01 }, 8.5); | |
| tl.to("#t-block-cursor", { opacity: 0, duration: 0.08 }, 8.9); | |
| tl.to("#t-block-cursor", { opacity: 1, duration: 0.08 }, 8.98); | |
| tl.to("#t-block-cursor", { opacity: 0, duration: 0.08 }, 9.2); | |
| tl.to("#t-block-cursor", { opacity: 1, duration: 0.08 }, 9.28); | |
| tl.to("#t-block-cursor", { opacity: 0, duration: 0.01 }, 9.5); | |
| // 9.5–13s: follow the cursor as it types (pan right, stay at 3.5x) | |
| userType( | |
| tl, | |
| "t-prompt-text", | |
| "mystery.wav — something is hidden in this file. find it.", | |
| 9.5, | |
| 0.055, | |
| ); | |
| tl.to("#camera", { x: -1600, duration: 3.5, ease: "none" }, 9.5); | |
| // 13.2s: typing done — zoom out to show the entire prompt message | |
| tl.to( | |
| "#camera", | |
| { scale: 2.2, x: -305, y: 184, duration: 0.6, ease: "power2.inOut" }, | |
| 13.2, | |
| ); | |
| // 15.2s: agent responding — zoom out to full terminal context | |
| tl.to( | |
| "#camera", | |
| { scale: 1.5, x: -90, y: 240, duration: 0.8, ease: "power2.inOut" }, | |
| 15.2, | |
| ); | |
| tl.to("#t-sep", { opacity: 1, duration: 0.01 }, 14.7); | |
| // t-a1: 31ch × 0.05 ≈ 1.55s → done ~16.6, read until 17.5 = 0.9s | |
| tokenStream(tl, "t-a1", "i'll read the file header first.", 15.0, 0.05); | |
| // ── WAV reader (17–23s) ── | |
| tl.to("#camera", { y: 30, duration: 1.2, ease: "power2.inOut" }, 17.0); | |
| // t-a2: 26ch × 0.035 ≈ 0.9s → done ~18.4, read until 19.0 = 0.6s | |
| tokenStream(tl, "t-a2", "no wav reader. writing one.", 17.5, 0.035); | |
| tl.to("#code-flash-1", { opacity: 1, duration: 0.15 }, 19.0); | |
| tl.to("#code-flash-1", { opacity: 0, duration: 0.15 }, 20.3); | |
| // tool call + result: appear 20.5–20.7, read until 21.8 = 1.1s | |
| tl.to("#t-tc1", { opacity: 1, duration: 0.01 }, 20.5); | |
| tl.to("#t-tr1", { opacity: 1, duration: 0.01 }, 20.7); | |
| // t-a3: 60ch × 0.018 ≈ 1.1s → done ~22.9 | |
| tokenStream( | |
| tl, | |
| "t-a3", | |
| "pcm, 44100hz mono, 6.4 seconds. let me read the raw samples.", | |
| 21.8, | |
| 0.018, | |
| ); | |
| // ── Samples (23.8–28s) ── | |
| tl.to("#camera", { y: -150, duration: 1, ease: "power2.inOut" }, 23.8); | |
| tl.to("#code-flash-1b", { opacity: 1, duration: 0.15 }, 24.0); | |
| tl.to("#code-flash-1b", { opacity: 0, duration: 0.15 }, 25.0); | |
| // tool call + result: appear 25.2–25.4, read until 26.2 = 0.8s | |
| tl.to("#t-tc2", { opacity: 1, duration: 0.01 }, 25.2); | |
| tl.to("#t-tr2", { opacity: 1, duration: 0.01 }, 25.4); | |
| // t-a4: 53ch × 0.02 ≈ 1.1s → done ~27.3, read until 28.5 = 1.2s | |
| tokenStream( | |
| tl, | |
| "t-a4", | |
| "tonal signal but no obvious pattern in the waveform.", | |
| 26.2, | |
| 0.02, | |
| ); | |
| // ── The pivot (28.5–32s) ── | |
| tl.to("#camera", { y: -270, duration: 0.8, ease: "power2.inOut" }, 28.5); | |
| // t-a5: 52ch × 0.04 ≈ 2.1s → done ~31.6, read until 32.0 = 0.4s | |
| tokenStream(tl, "t-a5", "nothing visible in the waveform. checking frequencies.", 29.5, 0.04); | |
| // ── DFT tool (32–39s) ── | |
| tl.to( | |
| "#camera", | |
| { scale: 1.35, x: 15, y: -243, duration: 0.8, ease: "power2.inOut" }, | |
| 32.0, | |
| ); | |
| // t-a6: 43ch × 0.025 ≈ 1.1s → done ~33.6, read until 34.0 = 0.4s | |
| tokenStream(tl, "t-a6", "i need a fourier transform. writing one.", 32.5, 0.025); | |
| tl.to("#code-flash-2", { opacity: 1, duration: 0.2 }, 34.0); | |
| tl.to("#code-flash-2", { opacity: 0, duration: 0.2 }, 35.8); | |
| // tool call + result: appear 36.2–36.4, read until 37.2 = 0.8s | |
| tl.to("#t-tc3", { opacity: 1, duration: 0.01 }, 36.2); | |
| tl.to("#t-tr3", { opacity: 1, duration: 0.01 }, 36.4); | |
| // t-a7: 62ch × 0.028 ≈ 1.7s → done ~38.9, read until 39.5 = 0.6s | |
| tokenStream( | |
| tl, | |
| "t-a7", | |
| "strong signal at 1500–3800hz. i'll scan every time window.", | |
| 37.2, | |
| 0.028, | |
| ); | |
| // ── The scan (39.5–48s) ── | |
| tl.to( | |
| "#camera", | |
| { scale: 1.4, x: -20, y: 260, duration: 0.5, ease: "power2.out" }, | |
| 39.5, | |
| ); | |
| tl.to("#phase1", { opacity: 0, duration: 0.5 }, 39.5); | |
| tl.to("#phase2", { opacity: 1, duration: 0.3 }, 39.8); | |
| tl.to("#t-sc1", { opacity: 1, duration: 0.01 }, 40.0); | |
| tl.to("#t-sc2", { opacity: 1, duration: 0.01 }, 40.7); | |
| tl.to("#t-sc3", { opacity: 1, duration: 0.01 }, 41.3); | |
| tl.to("#t-sc4", { opacity: 1, duration: 0.01 }, 41.8); | |
| tl.to("#t-sc5", { opacity: 1, duration: 0.01 }, 42.2); | |
| tl.to("#t-sc6", { opacity: 1, duration: 0.01 }, 42.5); | |
| tl.to( | |
| "#camera", | |
| { scale: 1.15, x: 155, y: 80, duration: 1.5, ease: "power2.inOut" }, | |
| 42.5, | |
| ); | |
| // t-a8: 75ch × 0.012 ≈ 0.9s → done ~43.6, read until 44.5 = 0.9s | |
| tokenStream( | |
| tl, | |
| "t-a8", | |
| "amplitudes vary by position. mapping frequencies to rows, time to columns.", | |
| 42.7, | |
| 0.012, | |
| ); | |
| for (let i = 0; i < 11; i++) { | |
| tl.to("#t-img-" + i, { opacity: 1, duration: 0.01 }, 44.5 + i * 0.3); | |
| } | |
| // ── The reveal (48–51s) ── | |
| tl.to( | |
| "#camera", | |
| { scale: 1.25, x: 85, y: -60, duration: 0.5, ease: "power2.inOut" }, | |
| 47.5, | |
| ); | |
| // t-a9: 49ch × 0.018 ≈ 0.9s → done ~48.9, holds until 51 = 2.1s | |
| tokenStream( | |
| tl, | |
| "t-a9", | |
| 'the spectrogram encodes an image. it reads "mi".', | |
| 48, | |
| 0.018, | |
| ); | |
| // ── Source code (51–55s): zig-zag sweep ── | |
| // Top-down, no 3D. Camera = scale + x/y on #source-camera. | |
| // Code plane starts at (80, 80). Line height ~25.5px. 39 lines. | |
| // | |
| // Sweep 1 (51.0–52.2s): close-up 3.5x, pan right across imports | |
| // center (80,200)→(900,200) | |
| // Zig (52.2–52.8s): zoom out 3.5x→1.8x, reposition down-left | |
| // center (900,200)→(200,660) | |
| // Sweep 2 (52.8–54.5s): medium 1.8x, pan right across agent loop | |
| // center (200,550)→(1200,550) | |
| // Hold (54.5–55.0s): label fades in | |
| tl.set("#main-scene", { opacity: 0 }, 51); | |
| tl.set("#source-scene", { opacity: 1 }, 51); | |
| // Sweep 1: close-up right across imports (lines 5–9) | |
| tl.fromTo("#source-camera", | |
| { scale: 3.5, x: 680, y: -160, transformOrigin: "0 0" }, | |
| { x: -2190, duration: 1.2, ease: "power1.inOut" }, | |
| 51, | |
| ); | |
| // Zig: zoom out + reposition to agent loop | |
| tl.to("#source-camera", | |
| { scale: 1.4, x: 600, y: -450, duration: 0.6, ease: "power2.inOut" }, | |
| 52.2, | |
| ); | |
| // Sweep 2: medium zoom right across agent loop (lines 20–35) | |
| tl.to("#source-camera", | |
| { x: -1200, duration: 1.7, ease: "power1.inOut" }, | |
| 52.8, | |
| ); | |
| tl.to("#source-label", { opacity: 1, duration: 0.5 }, 54); | |
| // ── SCENE 10: Close (55–60s) ── | |
| tl.set("#source-scene", { opacity: 0 }, 55); | |
| tl.set("#close-scene", { opacity: 1 }, 55); | |
| tl.to( | |
| "#close-tagline", | |
| { opacity: 1, duration: 0.5, ease: "power2.out" }, | |
| 55.2, | |
| ); | |
| tl.to( | |
| "#close-url", | |
| { opacity: 1, duration: 0.6, ease: "power2.out" }, | |
| 55.8, | |
| ); | |
| // Close scene gradient — single cream corner drifting around the scene | |
| const cgp = { cx: -0.3, cy: -0.3 }; | |
| function renderCloseGrad() { | |
| drawCloseRadial(cgp.cx, cgp.cy, cgp.cx + 1.8, cgp.cy + 1.8, [COL.cream, COL.olive, COL.olive]); | |
| } | |
| tl.to("#close-grad", { opacity: 0.6, duration: 2, ease: "power2.out" }, 55); | |
| tl.to(cgp, { cx: 1.2, cy: -0.2, duration: 6, ease: "power1.inOut", onUpdate: renderCloseGrad }, 56); | |
| tl.to(cgp, { cx: 1.3, cy: 1.2, duration: 5, ease: "power1.inOut", onUpdate: renderCloseGrad }, 62); | |
| tl.to(cgp, { cx: -0.2, cy: 1.3, duration: 5, ease: "power1.inOut", onUpdate: renderCloseGrad }, 67); | |
| tl.to(cgp, { cx: -0.3, cy: -0.2, duration: 5, ease: "power1.inOut", onUpdate: renderCloseGrad }, 72); | |
| tl.to("#close-grad", { opacity: 0, duration: 2, ease: "power2.in" }, 77); | |
| // Close scene metaball field — lissajous orbits driven by single t param | |
| tl.call(() => renderMetaballs(), null, 55); | |
| tl.to("#close-dither", { opacity: 0.8, duration: 1.5, ease: "power2.out" }, 55); | |
| tl.to(metaT, { | |
| v: 1.6, | |
| duration: 25, ease: "none", | |
| onUpdate: renderMetaballs, | |
| }, 55); | |
| tl.to("#bgm", { volume: 0, duration: 2, ease: "power2.in" }, 78); | |
| tl.to("#close-scene", { opacity: 0, duration: 0.8 }, 79.2); | |
| window.__timelines["main"] = tl; | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment