From f34b76bb2fc1caf887cce12e4a81be882bdaf869 Mon Sep 17 00:00:00 2001
From: isaaclins <104733575+isaaclins@users.noreply.github.com>
Date: Wed, 7 Oct 2026 16:45:11 +0200
Subject: [PATCH 1/4] Intro film: made in code (tools/film), film block on the
home page
---
assets/css/_home.scss | 84 ++++++
assets/js/home.js | 26 ++
content/_index.md | 5 +-
layouts/shortcodes/film.html | 21 ++
tools/film/.gitignore | 2 +
tools/film/README.md | 49 ++++
tools/film/build.sh | 29 ++
tools/film/film.html | 9 +
tools/film/film.js | 509 +++++++++++++++++++++++++++++++++++
tools/film/icons.js | 193 +++++++++++++
tools/film/render.mjs | 77 ++++++
11 files changed, 1003 insertions(+), 1 deletion(-)
create mode 100644 layouts/shortcodes/film.html
create mode 100644 tools/film/.gitignore
create mode 100644 tools/film/README.md
create mode 100755 tools/film/build.sh
create mode 100644 tools/film/film.html
create mode 100644 tools/film/film.js
create mode 100644 tools/film/icons.js
create mode 100644 tools/film/render.mjs
diff --git a/assets/css/_home.scss b/assets/css/_home.scss
index 18453b62..4d8d9549 100644
--- a/assets/css/_home.scss
+++ b/assets/css/_home.scss
@@ -1231,6 +1231,90 @@
animation: home-rise .9s var(--ease) .1s both;
}
+ // ---------- intro film (shortcodes/film.html, tools/film/) ----------
+ // The box has its size before anything loads (aspect-ratio), so no shift.
+ .film {
+ position: relative;
+ margin: clamp(48px, 8vh, 96px) 0 0;
+ aspect-ratio: 16 / 9;
+ border-radius: 12px;
+ overflow: hidden;
+ background: #09090a;
+ box-shadow: 0 0 0 1px var(--line);
+
+ img,
+ video {
+ position: absolute;
+ inset: 0;
+ display: block;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ object-fit: cover;
+ }
+
+ video {
+ opacity: 0;
+ transition: opacity .3s var(--ease);
+ }
+
+ &.on video {
+ opacity: 1;
+ }
+ }
+
+ .film-btn {
+ position: absolute;
+ right: 14px;
+ bottom: 14px;
+ z-index: 2;
+ display: grid;
+ place-items: center;
+ width: 44px;
+ height: 44px;
+ padding: 0;
+ border: 1px solid rgba(255, 255, 255, .16);
+ border-radius: 50%;
+ background: rgba(9, 9, 10, .62);
+ color: #ecebe7;
+ cursor: pointer;
+ opacity: .72;
+ transition: opacity .2s var(--ease), transform .2s var(--ease);
+
+ &:hover,
+ &:focus-visible {
+ opacity: 1;
+ }
+
+ &:active {
+ transform: scale(.94);
+ }
+
+ svg {
+ display: none;
+ width: 16px;
+ height: 16px;
+ fill: currentColor;
+ }
+
+ &[data-state="play"] .i-play,
+ &[data-state="pause"] .i-pause,
+ &[data-state="replay"] .i-replay {
+ display: block;
+ }
+ }
+
+ @media (max-width: 640px) {
+ .film {
+ aspect-ratio: 4 / 5;
+ }
+
+ .film-btn {
+ right: 10px;
+ bottom: 10px;
+ }
+ }
+
// ---------- small screens ----------
@media (max-width: 960px) {
.case {
diff --git a/assets/js/home.js b/assets/js/home.js
index 1fd5daab..5a6e3243 100644
--- a/assets/js/home.js
+++ b/assets/js/home.js
@@ -148,6 +148,32 @@
}, { threshold: .3 }).observe(vid);
});
+ /* intro film: plays once while on screen, pauses off screen. Reduced motion
+ and Save-Data / slow connections keep the poster until someone presses play. */
+ var film = document.getElementById('film');
+ if (film) {
+ var fv = film.querySelector('video'), fb = film.querySelector('.film-btn');
+ var conn = navigator.connection || {};
+ var auto = !reduce && !conn.saveData && !/(^|-)2g|3g/.test(conn.effectiveType || '');
+ var wanted = false;
+ var setBtn = function (s, label) { fb.dataset.state = s; fb.setAttribute('aria-label', label); };
+ var play = function () {
+ fv.preload = 'auto';
+ fv.play().then(function () { film.classList.add('on'); setBtn('pause', 'Pause the intro film'); }, function () {});
+ };
+ fv.addEventListener('ended', function () { wanted = false; setBtn('replay', 'Play the intro film again'); });
+ fv.addEventListener('pause', function () { if (!fv.ended) setBtn('play', 'Play the intro film'); });
+ fb.addEventListener('click', function () {
+ if (fv.paused) { wanted = true; if (fv.ended) fv.currentTime = 0; play(); }
+ else { wanted = false; auto = false; fv.pause(); }
+ });
+ new IntersectionObserver(function (es) {
+ var vis = es[0].isIntersecting;
+ if (vis && (auto || wanted) && fv.paused && !fv.ended) { wanted = true; play(); }
+ else if (!vis && !fv.paused) fv.pause();
+ }, { threshold: .5 }).observe(film);
+ }
+
/* contact: click copies the address */
var mail = document.getElementById('mail'), hint = document.getElementById('copyhint');
if (mail && hint) {
diff --git a/content/_index.md b/content/_index.md
index b9ab4541..7cdffc00 100644
--- a/content/_index.md
+++ b/content/_index.md
@@ -8,7 +8,8 @@ description = "Isaac Lins. Application developer and security analyst at Swissco
{{< meta >}}
@@ -28,6 +29,8 @@ engineer and build systems that make it to production.
AI | tools & agents, from prompt to production
{{< /proof >}}
+{{< film >}}
+
{{< sec "about" >}}
{{< about pull="I started as a developer and moved into security. Both stuck." >}}
diff --git a/layouts/shortcodes/film.html b/layouts/shortcodes/film.html
new file mode 100644
index 00000000..66ae4b69
--- /dev/null
+++ b/layouts/shortcodes/film.html
@@ -0,0 +1,21 @@
+{{- /* Home: the intro film (source and render script in tools/film/).
+ 16:9 on wide screens, the 4:5 cut on phones. The picture underneath is
+ the poster; the video only loads when home.js decides to play it:
+ on screen, no reduced motion, no Save-Data. One button: play / pause / replay. */ -}}
+
+
+
+
+
+
+
+
diff --git a/tools/film/.gitignore b/tools/film/.gitignore
new file mode 100644
index 00000000..3a0d929e
--- /dev/null
+++ b/tools/film/.gitignore
@@ -0,0 +1,2 @@
+out/
+node_modules
diff --git a/tools/film/README.md b/tools/film/README.md
new file mode 100644
index 00000000..10bf61bf
--- /dev/null
+++ b/tools/film/README.md
@@ -0,0 +1,49 @@
+# Intro film
+
+The 18-second intro film on the home page, made entirely in code: one canvas,
+a fixed timeline, rendered frame by frame. No stock footage, no AI video.
+
+| File | What |
+|---|---|
+| `film.html` | Loads the two scripts. Open `film.html?w=1920&h=1080&f=500` (via any static server from the repo root) to see frame 500. |
+| `film.js` | The film. `T` at the top is the timeline (seconds); one `shotN(t)` function per shot. |
+| `icons.js` | The cast: one 16×16 pixel icon per project on the home page, drawn as grids or shape functions. |
+| `render.mjs` | Playwright drives the page frame by frame (`FILM.render(n)`), pipes PNGs into ffmpeg. |
+| `build.sh` | Renders both cuts and encodes everything into `out/` (git-ignored). |
+
+## Script
+
+| Time | Shot |
+|---|---|
+| 0.0 | black: "I build applications." |
+| 2.2 | paper: "and ship AI tools.", the seven projects print in around it |
+| 4.8 | the ring folds into a `git log --graph` with the real commit hashes; `HEAD -> main` lands on Stewie |
+| 9.3 | black: "from a prompt", a prompt types a real request from Stewie's post |
+| 11.6 | paper: "to production.", the prompt becomes `minecraft.isaaclins.com · live` |
+| 14.3 | black: the live dot pulls the cast in and types `////////// isaaclins.com` |
+
+Palette, font and the `//////////` mark come from the site (`assets/css/main.scss`,
+`static/fonts/`). Time is driven, never read from a clock, so frame n is
+always the same picture.
+
+## Render
+
+```sh
+cd tools/film
+npm i playwright && npx playwright install chromium-headless-shell # once
+./build.sh # masters + every web file
+./build.sh encode # only re-encode from the masters
+```
+
+About 1,080 frames per cut. Run it on a strong machine. Outputs in `out/`:
+
+| File | Use |
+|---|---|
+| `intro-16x9.mp4` / `.webm` | home page, wide screens → `static/video/` |
+| `intro-4x5-720.mp4` / `.webm` | home page, phones → `static/video/intro-4x5.*` |
+| `intro-16x9-poster.webp`, `intro-4x5-poster.webp` | posters → `images/home/intro-16x9.webp`, `intro-4x5.webp` |
+| `intro-4x5-linkedin.mp4` | 1080×1350, silent, for LinkedIn |
+| `intro-16x9-hq.mp4` | 1080p high quality, for sharing |
+
+The film block on the home page is `layouts/shortcodes/film.html` (+ `.film` in
+`assets/css/_home.scss`, the play/pause logic in `assets/js/home.js`).
diff --git a/tools/film/build.sh b/tools/film/build.sh
new file mode 100755
index 00000000..939ad8e8
--- /dev/null
+++ b/tools/film/build.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# Render both cuts and encode every deliverable into tools/film/out/.
+# ./build.sh masters + web files + poster
+# ./build.sh encode only re-encode from existing masters
+# ./build.sh masters only render the masters
+# Needs node + playwright (chromium) + ffmpeg on PATH. Heavy: run it on a
+# strong machine, not a laptop on battery (1080 frames per cut).
+set -euo pipefail
+cd "$(dirname "$0")"
+mkdir -p out
+if [ "${1:-}" != "encode" ]; then
+ node render.mjs --w 1920 --h 1080 --out out/master-16x9.mp4
+ node render.mjs --w 1080 --h 1350 --out out/master-4x5.mp4
+fi
+[ "${1:-}" = "masters" ] && exit 0
+X264='-c:v libx264 -preset veryslow -tune animation -profile:v high -pix_fmt yuv420p -movflags +faststart -an'
+VP9='-c:v libvpx-vp9 -b:v 0 -row-mt 1 -deadline good -cpu-used 1 -pix_fmt yuv420p -an'
+# 16:9, the homepage and anywhere else
+ffmpeg -v error -y -i out/master-16x9.mp4 $X264 -crf 27 out/intro-16x9.mp4
+ffmpeg -v error -y -i out/master-16x9.mp4 $VP9 -crf 40 out/intro-16x9.webm
+# 4:5, phones on the homepage (720 wide) and LinkedIn (full 1080x1350, silent for now)
+ffmpeg -v error -y -i out/master-4x5.mp4 -vf scale=720:900:flags=lanczos $X264 -crf 27 out/intro-4x5-720.mp4
+ffmpeg -v error -y -i out/master-4x5.mp4 -vf scale=720:900:flags=lanczos $VP9 -crf 40 out/intro-4x5-720.webm
+ffmpeg -v error -y -i out/master-4x5.mp4 $X264 -crf 18 out/intro-4x5-linkedin.mp4
+ffmpeg -v error -y -i out/master-16x9.mp4 $X264 -crf 18 out/intro-16x9-hq.mp4
+# posters: the finished git log (8.6 s)
+ffmpeg -v error -y -ss 8.6 -i out/master-16x9.mp4 -frames:v 1 -c:v libwebp -quality 82 out/intro-16x9-poster.webp
+ffmpeg -v error -y -ss 8.6 -i out/master-4x5.mp4 -frames:v 1 -vf scale=720:900:flags=lanczos -c:v libwebp -quality 82 out/intro-4x5-poster.webp
+ls -la out
diff --git a/tools/film/film.html b/tools/film/film.html
new file mode 100644
index 00000000..3e6c6e76
--- /dev/null
+++ b/tools/film/film.html
@@ -0,0 +1,9 @@
+
+
+
+
isaaclins.com intro film
+
+
+
+
diff --git a/tools/film/film.js b/tools/film/film.js
new file mode 100644
index 00000000..f85d2d6d
--- /dev/null
+++ b/tools/film/film.js
@@ -0,0 +1,509 @@
+// The intro film for isaaclins.com, drawn on one canvas.
+// Time is driven from outside: render(frame) draws exactly that frame, so the
+// same frame always gives the same pixels (no clocks, no Math.random).
+// ?w=1920&h=1080 is the 16:9 film, ?w=1080&h=1350 the 4:5 cut. Layout reads
+// TALL to pick line breaks and sizes; the timeline is the same for both.
+
+(async function () {
+ const Q = new URLSearchParams(location.search);
+ const W = +Q.get('w') || 1920, H = +Q.get('h') || 1080;
+ const FPS = 60;
+ const TALL = H > W;
+ const S = TALL ? W / 1080 : H / 1080;
+
+ const cv = document.getElementById('c');
+ cv.width = W; cv.height = H;
+ const ctx = cv.getContext('2d');
+
+ // ---------- timeline (seconds) ----------
+ const T = {
+ w1: .20, // black: "I build applications."
+ s2: 2.00, // cut to paper: "and ship AI tools."
+ ringIn: 2.40, // the cast prints in around it and orbits
+ textOut: 4.55,
+ morph: 4.90, // the ring folds into a git log
+ header: 5.15,
+ head: 6.55, // HEAD -> main
+ s3: 7.95, // cut to black: "from a prompt"
+ send: 9.95,
+ s4: 10.35, // cut to paper: "to production."
+ push: 12.65, // push into the live dot
+ s5: 12.95, // match cut to black on the dot
+ end: 16.80,
+ };
+ const DUR = T.end;
+ const FRAMES = Math.round(DUR * FPS);
+
+ // ---------- palette (the site's own tokens, assets/css/main.scss) ----------
+ const DARK = { bg: '#09090a', fg: '#ecebe7', fg2: '#a6a5a0', fg3: '#82817c', line: '#2c2c30', blue: '#6f95ff', card: '#1b1c20', rim: 'rgba(255,255,255,.14)' };
+ const PAPER = { bg: '#f5f4ef', fg: '#121212', fg2: '#55544f', fg3: '#6f6e69', line: '#d2d0c8', blue: '#2c55f0', card: '#fbfaf6', rim: '#d9d7cf' };
+
+ // ---------- easing ----------
+ const clamp01 = x => x < 0 ? 0 : x > 1 ? 1 : x;
+ const prog = (t, a, d) => clamp01((t - a) / d);
+ const lerp = (a, b, p) => a + (b - a) * p;
+ function bez(x1, y1, x2, y2) {
+ const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
+ const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
+ const sx = u => ((ax * u + bx) * u + cx) * u, sy = u => ((ay * u + by) * u + cy) * u;
+ return x => {
+ if (x <= 0) return 0; if (x >= 1) return 1;
+ let lo = 0, hi = 1;
+ for (let i = 0; i < 26; i++) { const m = (lo + hi) / 2; if (sx(m) < x) lo = m; else hi = m; }
+ return sy((lo + hi) / 2);
+ };
+ }
+ const E = {
+ out: bez(.16, 1, .3, 1), // arrivals
+ in: bez(.7, 0, .84, 0), // exits
+ snap: bez(.7, 0, .2, 1), // morphs: slow out, fast across, soft landing
+ site: bez(.2, .8, .2, 1), // --ease on the site
+ quart: x => x * x * x * x,
+ inExpo: x => x <= 0 ? 0 : Math.pow(2, 10 * x - 10),
+ };
+ const back = (x, s = 1.70158) => x >= 1 ? 1 : 1 + (s + 1) * Math.pow(x - 1, 3) + s * Math.pow(x - 1, 2);
+
+ function rng(seed) { return () => { seed |= 0; seed = seed + 0x6D2B79F5 | 0; let t = Math.imul(seed ^ seed >>> 15, 1 | seed); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
+
+ // ---------- font ----------
+ const face = new FontFace('JetBrains Mono', 'url(../../static/fonts/jetbrains-mono.woff2)', { weight: '100 800' });
+ await face.load(); document.fonts.add(face);
+
+ // set the font; returns the advance of one character (mono, 0.6 em + tracking)
+ function font(size, weight, ls = -.04) {
+ ctx.font = `${weight} ${size}px "JetBrains Mono"`;
+ ctx.letterSpacing = (size * ls) + 'px';
+ ctx.textBaseline = 'alphabetic';
+ ctx.textAlign = 'left';
+ return size * .6 + size * ls;
+ }
+
+ // ---------- grain ----------
+ // Fine, static, neutral grain: overlay of mid-grey noise (128 = no change),
+ // so the paper keeps the site's #f5f4ef and the black stays black.
+ function texture(seed, amp) {
+ const c = document.createElement('canvas'); c.width = W; c.height = H;
+ const g = c.getContext('2d'), img = g.createImageData(W, H), d = img.data, r = rng(seed);
+ for (let i = 0; i < d.length; i += 4) { const v = 128 + (r() - .5) * 2 * amp; d[i] = d[i + 1] = d[i + 2] = v; d[i + 3] = 255; }
+ g.putImageData(img, 0, 0); return c;
+ }
+ const GRAIN = { paper: texture(7, 34), dark: texture(11, 60) };
+
+ function background(th) {
+ ctx.fillStyle = th.bg; ctx.fillRect(0, 0, W, H);
+ const R = Math.hypot(W, H) / 2;
+ const g = ctx.createRadialGradient(W / 2, H * .48, R * .3, W / 2, H / 2, R);
+ if (th === PAPER) { g.addColorStop(0, 'rgba(60,50,30,0)'); g.addColorStop(1, 'rgba(60,50,30,.05)'); }
+ else { g.addColorStop(0, 'rgba(111,149,255,.03)'); g.addColorStop(1, 'rgba(0,0,0,0)'); }
+ ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
+ }
+ function grain(th) {
+ ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0);
+ ctx.globalCompositeOperation = 'overlay'; ctx.globalAlpha = th === PAPER ? .22 : .5;
+ ctx.drawImage(th === PAPER ? GRAIN.paper : GRAIN.dark, 0, 0);
+ ctx.restore();
+ }
+ // a slow push-in per shot, so nothing ever sits dead still
+ function camera(t, a, b, amt = .03, cx = W / 2, cy = H / 2) {
+ const k = 1 + amt * E.site(prog(t, a, b - a));
+ ctx.translate(cx, cy); ctx.scale(k, k); ctx.translate(-cx, -cy);
+ }
+
+ // ---------- type ----------
+ function line(segs) {
+ const chars = [];
+ segs.forEach(([txt, col]) => { for (const c of txt) chars.push({ c, col }); });
+ const ws = []; let s = null;
+ chars.forEach((ch, i) => { if (ch.c === ' ') { if (s !== null) { ws.push({ s, e: i }); s = null; } } else if (s === null) s = i; });
+ if (s !== null) ws.push({ s, e: chars.length });
+ return { chars, ws, n: chars.length };
+ }
+ function runs(L, from, to, x, y, cw) {
+ let i = from;
+ while (i < to) {
+ const col = L.chars[i].col; let j = i, str = '';
+ while (j < to && L.chars[j].col === col) { str += L.chars[j].c; j++; }
+ ctx.fillStyle = col; ctx.fillText(str, x + i * cw, y); i = j;
+ }
+ }
+ const textW = (n, size, cw, ls) => n * cw - size * ls; // no trailing tracking
+ // baseline y of line i in a block of n lines centred on cy
+ const lineY = (i, n, size, cy, lh = 1.12) => cy - (n - 1) * size * lh / 2 + i * size * lh + size * .36;
+
+ // Move 1, word mask reveal: each word rises out of its own slot.
+ function reveal(lines, size, weight, t, tin, cy, o = {}) {
+ const ls = -.04, cw = font(size, weight, ls);
+ const wmax = Math.max(...lines.map(l => textW(l.n, size, cw, ls)));
+ const x = W / 2 - wmax / 2;
+ const st = o.stagger ?? .085, du = o.dur ?? .62;
+ let k = 0;
+ lines.forEach((L, li) => {
+ const y = lineY(li, lines.length, size, cy);
+ L.ws.forEach(w => {
+ const p = E.out(prog(t, tin + k++ * st, du)); if (p <= 0) return;
+ const dy = (1 - p) * size * 1.15;
+ ctx.save(); ctx.beginPath();
+ ctx.rect(x + w.s * cw - size * .12, y - size * 1.02, (w.e - w.s) * cw + size * .24, size * 1.32); ctx.clip();
+ runs(L, w.s, w.e, x, y + dy, cw); ctx.restore();
+ });
+ });
+ }
+ // Move 2, tracking settle: wide letter-spacing and a touch large, settling in.
+ function settle(lines, size, weight, t, tin, tout, cy) {
+ const p = E.out(prog(t, tin, .7)); if (p <= 0) return;
+ const q = tout == null ? 0 : E.in(prog(t, tout, .32)); if (q >= 1) return;
+ const ls = lerp(.28, -.04, p) - q * .06, k = lerp(1.07, 1, p) * (1 - .04 * q);
+ ctx.save(); ctx.globalAlpha = Math.min(1, p * 1.6) * (1 - q);
+ ctx.translate(W / 2, cy); ctx.scale(k, k); ctx.translate(-W / 2, -cy);
+ const cw = font(size, weight, ls);
+ lines.forEach((L, i) => runs(L, 0, L.n, W / 2 - textW(L.n, size, cw, ls) / 2, lineY(i, lines.length, size, cy), cw));
+ ctx.restore();
+ }
+ function caretBox(x, y, size, col) { ctx.fillStyle = col; ctx.fillRect(x + size * .1, y - size * .76, size * .44, size * .9); }
+ const blinkOn = (t, from) => t < from || ((t - from) % 1.06) < .53;
+
+ // ---------- pixel icons ----------
+ // Printed in row by row (2 rows a frame, the row being printed in the accent),
+ // popped in with a little overshoot, soft shadow on paper. On black the
+ // outline is lifted to a dark grey rim so dark icons keep their edge.
+ const ICON_Q = 12, oc = document.createElement('canvas'); oc.width = oc.height = 16 * ICON_Q;
+ const ox = oc.getContext('2d');
+ function drawIcon(id, t, cx, cy, size, o = {}) {
+ const { rows, pal } = ICONS.icon(id, t);
+ const th = o.th || PAPER, build = o.build ?? 1;
+ const nrows = build >= 1 ? 16 : Math.floor(build * 16);
+ if (nrows <= 0 || size <= .5) return;
+ ox.clearRect(0, 0, oc.width, oc.height);
+ for (let y = 0; y < nrows; y++) for (let x = 0; x < 16; x++) {
+ const ch = rows[y][x]; if (ch === '.') continue;
+ ox.fillStyle = build < 1 && y >= nrows - 2 ? th.blue : th === DARK && ch === 'k' ? '#3d3e47' : pal[ch];
+ ox.fillRect(x * ICON_Q, y * ICON_Q, ICON_Q, ICON_Q);
+ }
+ ctx.save(); ctx.translate(cx, cy); if (o.rot) ctx.rotate(o.rot);
+ ctx.globalAlpha *= (o.alpha ?? 1);
+ if (th === PAPER && o.shadow !== false) {
+ const k = size / (180 * S);
+ ctx.shadowColor = 'rgba(40,32,20,.2)'; ctx.shadowBlur = 28 * S * k; ctx.shadowOffsetY = 16 * S * k;
+ }
+ ctx.imageSmoothingEnabled = !!o.rot; ctx.imageSmoothingQuality = 'high';
+ ctx.drawImage(oc, -size / 2, -size / 2, size, size);
+ ctx.restore();
+ }
+ const pop = (t, tin) => .6 + .4 * back(prog(t, tin, .35), 2.6); // .6 -> ~1.08 -> 1
+
+ const CAST = ICONS.CAST;
+
+ // the cast on a tilted, turning ellipse; depth = sin(angle), front = bottom
+ function ring(i, t, o) {
+ const th = -Math.PI / 2 + i * 2 * Math.PI / CAST.length + (t - o.t0) * o.speed;
+ const ex = o.rx * Math.cos(th), ey = o.ry * Math.sin(th), c = Math.cos(o.tilt), s = Math.sin(o.tilt);
+ const depth = Math.sin(th);
+ return { x: o.cx + ex * c - ey * s, y: o.cy + ex * s + ey * c, depth, s: 1 + .22 * depth, rot: .12 * Math.sin(th * 2 + i * 1.7) };
+ }
+
+ // ---------- layout ----------
+ const L1 = TALL ? [line([['I build', DARK.fg]]), line([['applications.', DARK.fg]])] : [line([['I build applications.', DARK.fg]])];
+ const L2 = TALL ? [line([['and ship', PAPER.fg]]), line([['AI tools.', PAPER.blue]])] : [line([['and ship ', PAPER.fg], ['AI tools.', PAPER.blue]])];
+ const SZ1 = (TALL ? 132 : 106) * S, SZ2 = (TALL ? 120 : 110) * S;
+ const RING = TALL
+ ? { rx: 410 * S, ry: 530 * S, tilt: .14, size: 150 * S, speed: .52 }
+ : { rx: 800 * S, ry: 330 * S, tilt: -.21, size: 180 * S, speed: .52 };
+
+ // git log
+ const LOG = TALL
+ ? { rowH: 140 * S, icon: 84 * S, name: 50 * S, hash: 38 * S, head: 30 * S, hdr: 38 * S }
+ : { rowH: 112 * S, icon: 86 * S, name: 60 * S, hash: 32 * S, head: 30 * S, hdr: 34 * S };
+ {
+ const cwN = LOG.name * .57, cwH = LOG.hash * .6;
+ LOG.hashW = 7 * cwH; LOG.gap = 40 * S; LOG.iconX = 34 * S + LOG.icon / 2; LOG.nameX = 34 * S + LOG.icon + 34 * S;
+ const total = LOG.hashW + LOG.gap + LOG.nameX + 20 * cwN;
+ LOG.railX = W / 2 - total / 2 + LOG.hashW + LOG.gap;
+ LOG.y0 = H / 2 - (CAST.length - 1) * LOG.rowH / 2 + (TALL ? 46 : 38) * S;
+ LOG.hdrY = LOG.y0 - (TALL ? 124 : 100) * S;
+ }
+ const rowPos = i => ({ x: LOG.railX + LOG.iconX, y: LOG.y0 + i * LOG.rowH });
+ const landAt = i => T.morph + i * .075 + .78;
+
+ // the prompt (black) and the live chip (paper)
+ const SZ3 = (TALL ? 118 : 120) * S;
+ const TXT_CY = H * (TALL ? .30 : .34);
+ const PROMPT = TALL ? ['my friends want', 'to play Minecraft'] : ['my friends want to play Minecraft'];
+ const PROMPT_N = PROMPT.join(' ').length;
+ const PILL = TALL ? { w: 900 * S, h: 220 * S, r: 56 * S, txt: 50 * S } : { w: 1300 * S, h: 140 * S, r: 70 * S, txt: 50 * S };
+ PILL.cx = W / 2; PILL.cy = H * (TALL ? .57 : .62);
+ const BTN = 56 * S; // send button radius
+ // seeded human typing: ~30 ms a key, a little longer after a space
+ const keyT = (() => { const r = rng(42), a = [], s = PROMPT.join(' '); let t = T.s3 + .85; for (let i = 0; i < s.length; i++) { a.push(t); t += .019 + r() * .017 + (s[i] === ' ' ? .028 : 0); } return a; })();
+ const keysAt = t => { let n = 0; while (n < keyT.length && keyT[n] <= t) n++; return n; };
+
+ const CHIP = (() => {
+ const url = 'minecraft.isaaclins.com', pad = 32 * S, ic = 66 * S;
+ const txt = (TALL ? 50 : 46) * S, small = (TALL ? 38 : 34) * S;
+ const cw = txt * .58, urlW = url.length * cw - txt * -.02;
+ const c = { url, pad, ic, txt, small, cw };
+ if (TALL) { c.w = pad + ic + 22 * S + urlW + pad; c.h = 200 * S; c.r = 48 * S; }
+ else { c.w = pad + ic + 22 * S + urlW + 26 * S + 2 * S + 22 * S + 14 * S + 12 * S + 4 * small * .6 + pad; c.h = 120 * S; c.r = 60 * S; }
+ c.x = W / 2 - c.w / 2; c.y = PILL.cy - c.h / 2;
+ const row1 = TALL ? c.y + pad + ic / 2 : PILL.cy;
+ c.icX = c.x + pad + ic / 2; c.icY = row1; c.urlX = c.x + pad + ic + 22 * S; c.urlY = row1 + txt * .36;
+ if (TALL) { c.dotX = c.urlX + 7 * S; c.dotY = c.y + c.h - pad - 22 * S; c.liveX = c.dotX + 7 * S + 14 * S; c.liveY = c.dotY + small * .36; }
+ else { c.divX = c.urlX + urlW + 26 * S; c.dotX = c.divX + 2 * S + 22 * S + 7 * S; c.dotY = PILL.cy; c.liveX = c.dotX + 7 * S + 12 * S; c.liveY = PILL.cy + small * .36; }
+ return c;
+ })();
+
+ function rr(x, y, w, h, r) { ctx.beginPath(); ctx.roundRect(x, y, w, h, Math.min(r, h / 2, w / 2)); }
+
+ // ---------- shots ----------
+ function shot1(t) { // black: "I build applications."
+ background(DARK);
+ ctx.save(); camera(t, 0, T.s2, .03);
+ reveal(L1, SZ1, 700, t, T.w1, H / 2, { stagger: .11 });
+ ctx.restore();
+ grain(DARK);
+ }
+
+ function shot2(t) { // paper: "and ship AI tools." -> ring -> git log
+ background(PAPER);
+ ctx.save(); camera(t, T.s2, T.s3, .028);
+ const ro = { ...RING, cx: W / 2, cy: H / 2, t0: T.s2 };
+ const icons = CAST.map((c, i) => {
+ const tin = T.ringIn + i * .09;
+ if (t < tin) return null;
+ const r = ring(i, t, ro);
+ const mt = T.morph + i * .075, pm = E.snap(prog(t, mt, .78));
+ const to = rowPos(i);
+ const arc = Math.sin(Math.PI * pm) * (i % 2 ? -1 : 1) * 70 * S;
+ return {
+ c, i, pm, depth: r.depth,
+ x: lerp(r.x, to.x, pm) + arc, y: lerp(r.y, to.y, pm),
+ size: lerp(RING.size * r.s * pop(t, tin), LOG.icon, pm), rot: lerp(r.rot, 0, pm), build: prog(t, tin, .14),
+ };
+ }).filter(Boolean);
+ const draw = ic => drawIcon(ic.c.id, t, ic.x, ic.y, ic.size, { build: ic.build, rot: ic.rot, th: PAPER });
+ // depth: icons behind the line, the line, icons in front of it
+ icons.filter(ic => ic.pm === 0 && ic.depth < 0).sort((a, b) => a.depth - b.depth).forEach(draw);
+ settle(L2, SZ2, 700, t, T.s2 + .05, T.textOut, H / 2);
+
+ // header, typed
+ const hx = LOG.railX - LOG.hashW - LOG.gap;
+ const hdr = line([['$ ', PAPER.blue], ['git log --graph', PAPER.fg2]]);
+ const nh = (t - T.header) / .02;
+ if (nh > 0) { const cw = font(LOG.hdr, 500, 0); runs(hdr, 0, Math.min(hdr.n, Math.floor(nh)), hx, LOG.hdrY, cw); }
+
+ // the graph: a blue line that grows down as the commits land
+ const railTop = LOG.y0 - 34 * S, lastY = LOG.y0 + (CAST.length - 1) * LOG.rowH;
+ const rp = E.site(prog(t, landAt(0) - .2, landAt(CAST.length - 1) - landAt(0) + .35));
+ if (rp > 0) { ctx.fillStyle = PAPER.blue; ctx.fillRect(LOG.railX - 1.5 * S, railTop, 3 * S, (lastY + 34 * S - railTop) * rp); }
+
+ CAST.forEach((c, i) => {
+ const la = landAt(i), y = LOG.y0 + i * LOG.rowH;
+ if (t < la - .04) return;
+ const pn = back(prog(t, la - .04, .3), 2.2);
+ ctx.save(); ctx.translate(LOG.railX, y); ctx.scale(pn, pn);
+ ctx.beginPath(); ctx.arc(0, 0, 10 * S, 0, Math.PI * 2);
+ if (i === 0 && t >= T.head) { ctx.fillStyle = PAPER.blue; ctx.fill(); }
+ else { ctx.fillStyle = PAPER.bg; ctx.fill(); ctx.lineWidth = 3 * S; ctx.strokeStyle = PAPER.blue; ctx.stroke(); }
+ ctx.restore();
+ if (i === 0 && t >= T.head) { // HEAD lands: one pulse on the node
+ const q = prog(t, T.head, .7);
+ if (q < 1) { ctx.beginPath(); ctx.arc(LOG.railX, y, 10 * S + E.out(q) * 26 * S, 0, Math.PI * 2); ctx.lineWidth = 2 * S; ctx.strokeStyle = PAPER.blue; ctx.globalAlpha = .45 * (1 - q); ctx.stroke(); ctx.globalAlpha = 1; }
+ }
+ const pt = E.out(prog(t, la, .3));
+ ctx.fillStyle = PAPER.blue; ctx.fillRect(LOG.railX + 12 * S, y - 1.5 * S, (LOG.iconX - LOG.icon / 2 - 18 * S) * pt, 3 * S);
+ font(LOG.hash, 500, 0); ctx.textAlign = 'right';
+ const nhh = Math.floor((t - la) * 60 / 1.2);
+ if (nhh > 0) { ctx.fillStyle = PAPER.fg2; ctx.fillText(c.hash.slice(0, nhh), LOG.railX - LOG.gap, y + LOG.hash * .36); }
+ ctx.textAlign = 'left';
+ const cwn = font(LOG.name, 650, -.03);
+ const nn = (t - la - .05) * 60;
+ if (nn > 0) { ctx.fillStyle = PAPER.fg; ctx.fillText(c.name.slice(0, Math.floor(nn)), LOG.railX + LOG.nameX, y + LOG.name * .36); }
+ if (i === 0 && t >= T.head) { // HEAD -> main slides in from the right
+ const ph = prog(t, T.head, .28), e = back(ph, 2);
+ const tx = LOG.railX + LOG.nameX + textW(c.name.length, LOG.name, cwn, -.03) + 28 * S + (1 - e) * 40 * S;
+ const cwt = font(LOG.head, 600, 0), label = 'HEAD -> main';
+ const tw = label.length * cwt + 30 * S, thh = LOG.head * 1.7;
+ ctx.save(); ctx.globalAlpha = Math.min(1, ph * 2.5);
+ rr(tx, y - thh / 2, tw, thh, 7 * S); ctx.lineWidth = 2.5 * S; ctx.strokeStyle = PAPER.blue; ctx.stroke();
+ ctx.fillStyle = PAPER.blue; ctx.fillText(label, tx + 15 * S, y + LOG.head * .36);
+ ctx.restore();
+ }
+ });
+ icons.filter(ic => !(ic.pm === 0 && ic.depth < 0)).sort((a, b) => (a.pm > 0) - (b.pm > 0) || a.depth - b.depth).forEach(draw);
+ ctx.restore();
+ grain(PAPER);
+ }
+
+ // the prompt text, typed over one or two lines; returns the caret position
+ function promptText(n, x, y0, cw, lh, col) {
+ // the space between two lines is the line break
+ let left = n, at = null;
+ PROMPT.forEach((s, i) => {
+ const k = Math.max(0, Math.min(s.length, left)), y = y0 + i * lh;
+ if (k > 0) { ctx.fillStyle = col; ctx.fillText(s.slice(0, k), x, y); }
+ if (!at && left <= s.length) at = { cx: x + k * cw, cy: y };
+ left -= s.length + 1;
+ });
+ return at || { cx: x + PROMPT[PROMPT.length - 1].length * cw, cy: y0 + (PROMPT.length - 1) * lh };
+ }
+
+ function shot3(t) { // black: "from a prompt" + the prompt pill
+ background(DARK);
+ ctx.save(); camera(t, T.s3, T.s4, .022);
+ reveal([line([['from a prompt', DARK.fg]])], SZ3, 700, t, T.s3 + .03, TXT_CY);
+ // a dot, then a pill (spring), then contents; after send it collapses to a circle
+ const appear = back(prog(t, T.s3 + .18, .3), 2);
+ const grow = E.out(prog(t, T.s3 + .3, .6));
+ const col = E.snap(prog(t, T.send + .05, .35));
+ let w = lerp(PILL.h, PILL.w, grow), h = PILL.h;
+ if (grow <= 0) { w *= appear; h *= appear; }
+ w = lerp(w, 112 * S, col); h = lerp(h, 112 * S, col);
+ const r = lerp(Math.min(PILL.r, h / 2), h / 2, col);
+ if (appear > 0) {
+ rr(PILL.cx - w / 2, PILL.cy - h / 2, w, h, r); ctx.fillStyle = DARK.card; ctx.fill();
+ ctx.lineWidth = 1.5 * S; ctx.strokeStyle = DARK.rim; ctx.stroke();
+ }
+ const alpha = prog(t, T.s3 + .7, .2) * (1 - prog(t, T.send + .03, .12));
+ if (alpha > 0) {
+ ctx.save(); ctx.globalAlpha = alpha;
+ const x0 = PILL.cx - PILL.w / 2, lh = PILL.txt * 1.3;
+ const y0 = PILL.cy - (PROMPT.length - 1) * lh / 2 + PILL.txt * .36;
+ const cwp = font(PILL.txt, 700, 0);
+ ctx.fillStyle = DARK.blue; ctx.fillText('>', x0 + 46 * S, y0);
+ const cwt = font(PILL.txt, 450, -.02);
+ const tx = x0 + 46 * S + cwp + 24 * S;
+ const n = keysAt(t), c = promptText(n, tx, y0, cwt, lh, DARK.fg);
+ if (blinkOn(t, keyT[keyT.length - 1] + .1) && t < T.send) caretBox(c.cx, c.cy, PILL.txt, DARK.blue);
+ // send button: dim until there is text, lights up, gets pressed
+ const bx = x0 + PILL.w - 14 * S - BTN, by = PILL.cy + (TALL ? PILL.h / 2 - 14 * S - BTN : 0);
+ const lit = prog(t, keyT[0], .15);
+ const bp = t >= T.send ? 1 - .12 * Math.sin(Math.PI * prog(t, T.send, .18)) : 1;
+ ctx.save(); ctx.translate(bx, by); ctx.scale(bp, bp);
+ ctx.beginPath(); ctx.arc(0, 0, BTN - 8 * S, 0, Math.PI * 2);
+ ctx.fillStyle = lit > 0 ? `rgba(111,149,255,${.2 + .8 * lit})` : 'rgba(236,235,231,.08)'; ctx.fill();
+ ctx.strokeStyle = lit > .5 ? DARK.bg : DARK.fg3; ctx.lineWidth = 4.5 * S; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
+ ctx.beginPath(); ctx.moveTo(0, 14 * S); ctx.lineTo(0, -14 * S); ctx.moveTo(-12 * S, -2 * S); ctx.lineTo(0, -14 * S); ctx.lineTo(12 * S, -2 * S); ctx.stroke();
+ ctx.restore();
+ ctx.restore();
+ }
+ const fl = prog(t, T.send, .4); // send: a ring flashes out of the pill
+ if (fl > 0 && fl < 1) {
+ rr(PILL.cx - w / 2 - E.out(fl) * 20 * S, PILL.cy - h / 2 - E.out(fl) * 20 * S, w + E.out(fl) * 40 * S, h + E.out(fl) * 40 * S, r + E.out(fl) * 20 * S);
+ ctx.lineWidth = 2 * S; ctx.strokeStyle = DARK.blue; ctx.globalAlpha = .5 * (1 - fl); ctx.stroke(); ctx.globalAlpha = 1;
+ }
+ ctx.restore();
+ grain(DARK);
+ }
+
+ function liveDot(t, t0, x, y, th) {
+ if (t < t0) return;
+ const p = back(prog(t, t0, .3), 2.4);
+ for (let pt = t0 + .15; pt < t; pt += 1.2) { // pulse every 1.2 s
+ const q = prog(t, pt, 1.2); if (q >= 1) continue;
+ ctx.beginPath(); ctx.arc(x, y, 7 * S + E.out(q) * 15 * S, 0, Math.PI * 2);
+ ctx.lineWidth = 2 * S; ctx.strokeStyle = th.blue; ctx.globalAlpha = .35 * (1 - q); ctx.stroke(); ctx.globalAlpha = 1;
+ }
+ ctx.beginPath(); ctx.arc(x, y, 7 * S * p, 0, Math.PI * 2); ctx.fillStyle = th.blue; ctx.fill();
+ }
+
+ function shot4(t) { // paper: "to production." circle -> spinner -> check -> live chip
+ background(PAPER);
+ ctx.save();
+ camera(t, T.s4, T.push, .02);
+ if (t > T.push) { // push into the live dot for the match cut
+ const k = 1 + 5 * E.inExpo(prog(t, T.push, T.s5 - T.push));
+ ctx.translate(CHIP.dotX, CHIP.dotY); ctx.scale(k, k); ctx.translate(-CHIP.dotX, -CHIP.dotY);
+ }
+ settle([line([['to production.', PAPER.fg]])], SZ3, 700, t, T.s4 + .03, null, TXT_CY);
+ const t1 = T.s4 + .45, t2 = t1 + .22, t3 = t2 + .06; // spinner until t1, check until t3, then expand
+ const ex = back(prog(t, t3, .42), 1.1);
+ const w = lerp(112 * S, CHIP.w, ex), h = lerp(112 * S, CHIP.h, ex), r = lerp(56 * S, CHIP.r, ex);
+ ctx.save(); ctx.shadowColor = 'rgba(40,32,20,.12)'; ctx.shadowBlur = 40 * S; ctx.shadowOffsetY = 14 * S;
+ rr(W / 2 - w / 2, PILL.cy - h / 2, w, h, r); ctx.fillStyle = PAPER.card; ctx.fill(); ctx.restore();
+ rr(W / 2 - w / 2, PILL.cy - h / 2, w, h, r); ctx.lineWidth = 1.5 * S; ctx.strokeStyle = PAPER.rim; ctx.stroke();
+ ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = PAPER.blue; ctx.lineWidth = 6 * S;
+ if (t < t1) { // spinner
+ const a = (t - T.s4) * 9, len = 1.1 + .6 * Math.sin((t - T.s4) * 7);
+ ctx.beginPath(); ctx.arc(W / 2, PILL.cy, 24 * S, a, a + len * Math.PI); ctx.stroke();
+ } else if (ex < .25) { // check, drawn, then fading as the chip opens
+ const p = E.out(prog(t, t1, .22));
+ ctx.save(); ctx.globalAlpha = 1 - ex * 4; ctx.translate(W / 2, PILL.cy);
+ const pts = [[-15, 1], [-4, 12], [17, -11]].map(([a, b]) => [a * S, b * S]);
+ const l1 = Math.hypot(11, 11), l2 = Math.hypot(21, 23), d = p * (l1 + l2);
+ ctx.beginPath(); ctx.moveTo(...pts[0]);
+ if (d <= l1) ctx.lineTo(lerp(pts[0][0], pts[1][0], d / l1), lerp(pts[0][1], pts[1][1], d / l1));
+ else { ctx.lineTo(...pts[1]); const q = (d - l1) / l2; ctx.lineTo(lerp(pts[1][0], pts[2][0], q), lerp(pts[1][1], pts[2][1], q)); }
+ ctx.stroke(); ctx.restore();
+ }
+ const ci = t3 + .22; // chip contents: the block prints in, the address types, the dot goes live
+ if (t >= ci) {
+ drawIcon('minecraft', t, CHIP.icX, CHIP.icY, CHIP.ic, { build: prog(t, ci, .14), th: PAPER, shadow: false });
+ font(CHIP.txt, 550, -.02);
+ const n = Math.floor((t - ci - .08) * 60);
+ if (n > 0) { ctx.fillStyle = PAPER.fg; ctx.fillText(CHIP.url.slice(0, n), CHIP.urlX, CHIP.urlY); }
+ const live = ci + .08 + CHIP.url.length / 60 + .08;
+ if (!TALL && t >= live) { ctx.fillStyle = PAPER.rim; ctx.fillRect(CHIP.divX, PILL.cy - 22 * S * E.out(prog(t, live, .3)), 2 * S, 44 * S * E.out(prog(t, live, .3))); }
+ liveDot(t, live + .05, CHIP.dotX, CHIP.dotY, PAPER);
+ if (t >= live + .1) {
+ const pl = E.out(prog(t, live + .1, .4));
+ ctx.save(); ctx.globalAlpha = pl; font(CHIP.small, 600, 0); ctx.fillStyle = PAPER.blue;
+ ctx.fillText('live', CHIP.liveX + (1 - pl) * 12 * S, CHIP.liveY); ctx.restore();
+ }
+ }
+ ctx.restore();
+ grain(PAPER);
+ }
+
+ // the end line, mono, centred: "////////// isaaclins.com" + caret
+ const ENDS = (TALL ? 62 : 88) * S;
+ const ENDL = line([['//////////', DARK.blue], [' isaaclins.com', DARK.fg]]);
+ function shot5(t) { // black: the dot pulls the cast in and types the address
+ background(DARK);
+ ctx.save(); camera(t, T.s5, T.end, .03);
+ const ls = -.02, cw = font(ENDS, 600, ls);
+ const ex = W / 2 - textW(ENDL.n, ENDS, cw, ls) / 2 - ENDS * .27, ey = H / 2 + ENDS * .36;
+ // the dot arrives big (the push-in), shrinks and glides to the centre
+ const pg = E.snap(prog(t, T.s5, .45));
+ const dx = lerp(CHIP.dotX, W / 2, pg), dy = lerp(CHIP.dotY, H / 2, pg);
+ let rDot = lerp(42 * S, 11 * S, pg);
+ // the ring comes back once, then falls into the dot
+ const ringT = T.s5 + .25, suck = T.s5 + .62, ro = { rx: (TALL ? 340 : 540) * S, ry: (TALL ? 420 : 240) * S, tilt: TALL ? .14 : -.21, speed: .7, t0: T.s5 - 1.5 };
+ let bump = 0;
+ CAST.forEach((c, i) => {
+ const tin = ringT + i * .045; if (t < tin) return;
+ const ts = suck + i * .06, ps = E.quart(prog(t, ts, .45));
+ if (ps >= 1) { bump = Math.max(bump, 1 - prog(t, ts + .45, .22)); return; }
+ const r = ring(i, t, { ...ro, rx: ro.rx * (1 - ps), ry: ro.ry * (1 - ps), cx: dx, cy: dy });
+ drawIcon(c.id, t, r.x, r.y, 120 * S * r.s * pop(t, tin) * (1 - ps * .8), { build: prog(t, tin, .14), rot: r.rot + ps * 1.6, th: DARK });
+ });
+ rDot *= 1 + .25 * bump;
+ // dot -> caret, then the caret types the line
+ const pc = E.snap(prog(t, T.s5 + 1.2, .25));
+ const ty = T.s5 + 1.5;
+ const k = (t - ty) * 30, shown = t < ty ? 0 : Math.max(0, Math.min(ENDL.n, Math.floor(k <= 10 ? k : Math.max(10, k - 4))));
+ if (pc <= 0) { ctx.beginPath(); ctx.arc(dx, dy, rDot, 0, Math.PI * 2); ctx.fillStyle = DARK.blue; ctx.fill(); }
+ else {
+ font(ENDS, 600, ls); runs(ENDL, 0, shown, ex, ey, cw);
+ const cxT = ex + shown * cw + ENDS * .1, cyT = ey - ENDS * .76;
+ const x = lerp(W / 2 - rDot, cxT, pc), y = lerp(H / 2 - rDot, cyT, pc);
+ const w = lerp(rDot * 2, ENDS * .44, pc), h = lerp(rDot * 2, ENDS * .9, pc);
+ const done = ty + (ENDL.n + 4) / 30 + .35;
+ if (blinkOn(t, done)) { rr(x, y, w, h, lerp(rDot, 0, pc)); ctx.fillStyle = DARK.blue; ctx.fill(); }
+ }
+ ctx.restore();
+ grain(DARK);
+ }
+
+ function render(f) {
+ const t = f / FPS;
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
+ if (t < T.s2) shot1(t);
+ else if (t < T.s3) shot2(t);
+ else if (t < T.s4) shot3(t);
+ else if (t < T.s5) shot4(t);
+ else shot5(t);
+ }
+
+ window.FILM = { W, H, FPS, FRAMES, T, render };
+ render(+Q.get('f') || 0);
+ document.body.dataset.ready = '1';
+})().catch(e => { document.body.dataset.error = String(e && e.stack || e); throw e; });
diff --git a/tools/film/icons.js b/tools/film/icons.js
new file mode 100644
index 00000000..784d5b7d
--- /dev/null
+++ b/tools/film/icons.js
@@ -0,0 +1,193 @@
+// The cast: one 16x16 pixel icon per project on the home page, drawn here
+// in code. Each icon is either a hand-placed grid (one char per pixel) or a
+// shape function; shape icons get a 1 px dark outline added automatically.
+// icon(name, t) returns { rows: string[16], pal: {char: color} } for time t,
+// so an icon can move by itself (Stewie blinks, the record spins).
+
+(function () {
+ const K = '#0b0c10'; // outline
+ const N = 16;
+
+ function check(rows, name) {
+ if (rows.length !== N || rows.some(r => r.length !== N)) throw new Error('bad grid: ' + name);
+ return rows;
+ }
+
+ // shape(x, y) -> palette char or '.', sampled at pixel centres; then outline.
+ function fromShape(shape) {
+ const g = [];
+ for (let y = 0; y < N; y++) { let r = ''; for (let x = 0; x < N; x++) r += shape(x + .5, y + .5, x, y) || '.'; g.push(r); }
+ const out = g.map(r => r.split(''));
+ for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
+ if (g[y][x] !== '.') continue;
+ const n = [[1, 0], [-1, 0], [0, 1], [0, -1]].some(([dx, dy]) => {
+ const v = g[y + dy] && g[y + dy][x + dx];
+ return v && v !== '.';
+ });
+ if (n) out[y][x] = 'k';
+ }
+ return out.map(r => r.join(''));
+ }
+
+ // Seeded hash, so speckles are the same in every frame and every render.
+ const h2 = (x, y) => { let s = Math.sin(x * 127.1 + y * 311.7) * 43758.5453; return s - Math.floor(s); };
+
+ // Stewie: an agent with its own computer. A monitor with a face.
+ const STEWIE = check([
+ '......kkkk......',
+ '......kbbk......',
+ '......kkkk......',
+ '.......kk.......',
+ '..kkkkkkkkkkkk..',
+ '.kbbbbbbbbbbbbk.',
+ '.kbssssssssssBk.',
+ '.kbssssssssssBk.',
+ '.kbsswwsswwssBk.',
+ '.kbsswwsswwssBk.',
+ '.kbssssssssssBk.',
+ '.kbsssbbbbsssBk.',
+ '.kbssssssssssBk.',
+ '.kBBBBBBBBBBBBk.',
+ '..kkkkkkkkkkkk..',
+ '.....kkkkkk.....',
+ ], 'stewie');
+
+ // Rental Law Navigator: a house, windows lit.
+ const HOUSE = check([
+ '................',
+ '.......kk.......',
+ '......kLLk......',
+ '.....kLLLLk.....',
+ '....kLLLLLLk....',
+ '...kLLLLLLLLk...',
+ '..kLLLLLLLLLLk..',
+ '.kLLLLLLLLLLLLk.',
+ '.kkkkkkkkkkkkkk.',
+ '..kwwwwwwwwwwk..',
+ '..kwccwwwwccwk..',
+ '..kwccwwwwccwk..',
+ '..kwwwkkkkwwwk..',
+ '..kwwwkddkwwwk..',
+ '..kwwwkddkwwwk..',
+ '.kkkkkkkkkkkkkk.',
+ ], 'house');
+
+ // demo-video-skill: a clapperboard with a play button.
+ const CLAP = check([
+ '................',
+ '.kkkkkkkkkkkkkk.',
+ '.kwwkkwwkkwwkkk.',
+ '.kkwwkkwwkkwwkk.',
+ '.kkkkkkkkkkkkkk.',
+ '.kyyyyyyyyyyyyk.',
+ '.kyyyyyyyyyyyyk.',
+ '.kyyyykyyyyyyyk.',
+ '.kyyyykkyyyyyyk.',
+ '.kyyyykkkyyyyyk.',
+ '.kyyyykkkkyyyyk.',
+ '.kyyyykkkyyyyyk.',
+ '.kyyyykkyyyyyyk.',
+ '.kyyyykyyyyyyyk.',
+ '.kYYYYYYYYYYYYk.',
+ '.kkkkkkkkkkkkkk.',
+ ], 'clap');
+
+ // Challenge Server: a grass block, isometric, computed face by face.
+ const GRASS = fromShape((px, py, x, y) => {
+ const top = Math.abs(px - 8) / 7 + Math.abs(py - 4.5) / 3.5 <= 1;
+ if (top) return h2(x, y) > .72 ? 'G' : 'g';
+ if (px >= 1 && px <= 8 && py >= 4.5 + .5 * (px - 1) && py <= 11.5 + .5 * (px - 1)) {
+ const d = py - (4.5 + .5 * (px - 1));
+ if (d < 1.6 || (d < 2.8 && h2(x, 3) > .45)) return 'G';
+ return h2(x, y) > .7 ? 'D' : 'd';
+ }
+ if (px >= 8 && px <= 15 && py >= 8 - .5 * (px - 8) && py <= 15 - .5 * (px - 8)) {
+ const d = py - (8 - .5 * (px - 8));
+ if (d < 1.6 || (d < 2.8 && h2(x, 5) > .45)) return 'H';
+ return h2(x, y) > .7 ? 'E' : 'D';
+ }
+ return '';
+ });
+
+ // FreeSnitch: a shield with a check.
+ const CHECK = new Set(['4,8', '4,9', '5,9', '5,10', '6,10', '6,11', '7,10', '7,9', '8,9', '8,8', '9,8', '9,7', '10,7', '10,6', '11,6', '11,5']);
+ const SHIELD = fromShape((px, py, x, y) => {
+ if (py < 1.2 || py > 15) return '';
+ const hw = py <= 8.5 ? 6.2 : 6.2 * Math.pow(Math.max(0, 1 - (py - 8.5) / 6.6), .75);
+ if (Math.abs(px - 8) > hw) return '';
+ if (CHECK.has(x + ',' + y)) return 'w';
+ return px < 8 ? 'm' : 'M';
+ });
+
+ // pi-vault: a padlock.
+ const KEYHOLE = new Set(['7,9', '8,9', '6,10', '7,10', '8,10', '9,10', '7,11', '8,11', '7,12', '8,12']);
+ const LOCK = fromShape((px, py, x, y) => {
+ if (px > 2.5 && px < 13.5 && py > 7.5 && py < 14.8) {
+ if (KEYHOLE.has(x + ',' + y)) return 'k';
+ return py > 13.2 || px > 12.2 ? 'C' : 'c';
+ }
+ const d = Math.hypot(px - 8, py - 6.6);
+ if (py < 7.6 && d >= 2.6 && d <= 4.7) return px > 8.6 ? 'S' : 's';
+ if (py >= 6.6 && py <= 7.6 && ((px > 3.2 && px < 5.4) || (px > 10.6 && px < 12.8))) return px > 8 ? 'S' : 's';
+ return '';
+ });
+
+ // Spotiglass: a vinyl record. The sheen turns with t, so it spins.
+ function vinyl(t) {
+ const phi = t * 2.6;
+ return fromShape((px, py) => {
+ const d = Math.hypot(px - 8, py - 8);
+ if (d > 7.1) return '';
+ if (d < .8) return 'k';
+ if (d < 2.9) return d < 2.0 ? 'p' : 'P';
+ const a = Math.atan2(py - 8, px - 8);
+ const sheen = Math.cos(2 * (a - phi));
+ if (sheen > .86 && d > 3.4 && d < 6.6) return 'h';
+ return (d > 4.4 && d < 5.1) || d > 6.4 ? 'V' : 'v';
+ });
+ }
+
+ const PAL = {
+ stewie: { k: K, b: '#6f95ff', B: '#3e63d8', s: '#141a33', w: '#ecebe7' },
+ navigator: { k: K, L: '#8fb1ff', w: '#ecebe7', c: '#ffd36b', d: '#4a6fe0' },
+ 'demo-video': { k: K, w: '#ecebe7', y: '#f5b83d', Y: '#c98a17' },
+ minecraft: { k: K, g: '#79c94a', G: '#559c33', H: '#467f2a', d: '#916038', D: '#6f4727', E: '#55361d' },
+ freesnitch: { k: K, m: '#34d399', M: '#1ea372', w: '#f5f4ef' },
+ 'pi-vault': { k: K, c: '#3fd0ff', C: '#1f9ccb', s: '#d4d3cd', S: '#8f8e88' },
+ spotiglass: { k: K, v: '#1c1c22', V: '#2b2b34', h: '#6a6a78', p: '#ff5a72', P: '#d83a55' },
+ };
+
+ // Stewie blinks twice in the film (times in seconds); one blink = 0.12 s.
+ const BLINKS = [3.9, 8.1, 13.2, 17.4];
+
+ function icon(name, t) {
+ let rows;
+ switch (name) {
+ case 'stewie':
+ rows = STEWIE;
+ if (BLINKS.some(b => t >= b && t < b + .12)) rows = rows.map((r, i) => i === 8 ? r.replace(/w/g, 's') : i === 9 ? r.replace(/w/g, 'b') : r);
+ break;
+ case 'navigator': rows = HOUSE; break;
+ case 'demo-video': rows = CLAP; break;
+ case 'minecraft': rows = GRASS; break;
+ case 'freesnitch': rows = SHIELD; break;
+ case 'pi-vault': rows = LOCK; break;
+ case 'spotiglass': rows = vinyl(t); break;
+ default: throw new Error('no icon ' + name);
+ }
+ return { rows, pal: PAL[name] };
+ }
+
+ // The order of the git log on the home page, newest first (data/projects.toml).
+ const CAST = [
+ { id: 'stewie', name: 'Stewie', hash: 'a4d82da' },
+ { id: 'navigator', name: 'Rental Law Navigator', hash: '417d035' },
+ { id: 'demo-video', name: 'demo-video-skill', hash: '1b623a3' },
+ { id: 'minecraft', name: 'Challenge Server', hash: '851cf10' },
+ { id: 'freesnitch', name: 'FreeSnitch', hash: 'cc88217' },
+ { id: 'pi-vault', name: 'pi-vault', hash: '977842a' },
+ { id: 'spotiglass', name: 'Spotiglass', hash: '3e34eb7' },
+ ];
+
+ window.ICONS = { icon, CAST, N };
+})();
diff --git a/tools/film/render.mjs b/tools/film/render.mjs
new file mode 100644
index 00000000..6b51a722
--- /dev/null
+++ b/tools/film/render.mjs
@@ -0,0 +1,77 @@
+// Renders the film frame by frame with Playwright and pipes the frames into ffmpeg.
+// node render.mjs --w 1920 --h 1080 --out out/master-16x9.mp4 [--jobs 4] whole film -> near-lossless master
+// node render.mjs --w 1920 --h 1080 --stills 0,300,600 --outdir out single frames as PNG
+// Needs: node, playwright (chromium), ffmpeg. The time is driven: frame n is
+// always the same picture, however slow the machine is.
+import http from 'node:http';
+import fs from 'node:fs';
+import path from 'node:path';
+import { spawn } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { chromium } from 'playwright';
+
+const args = Object.fromEntries(process.argv.slice(2).reduce((a, v, i, all) => (v.startsWith('--') ? a.concat([[v.slice(2), all[i + 1]]]) : a), []));
+const W = +(args.w || 1920), H = +(args.h || 1080);
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
+
+const types = { '.html': 'text/html', '.js': 'text/javascript', '.woff2': 'font/woff2' };
+const server = http.createServer((req, res) => {
+ const p = path.join(root, decodeURIComponent(new URL(req.url, 'http://x').pathname));
+ if (!p.startsWith(root) || !fs.existsSync(p)) { res.writeHead(404); return res.end(); }
+ res.writeHead(200, { 'content-type': types[path.extname(p)] || 'application/octet-stream' });
+ fs.createReadStream(p).pipe(res);
+}).listen(0, '127.0.0.1');
+await new Promise(r => server.once('listening', r));
+const url = `http://127.0.0.1:${server.address().port}/tools/film/film.html?w=${W}&h=${H}`;
+
+const JOBS = +(args.jobs || 4);
+const browser = await chromium.launch();
+async function openPage() {
+ const page = await browser.newPage({ viewport: { width: W, height: H }, deviceScaleFactor: 1 });
+ page.on('pageerror', e => { console.error(e); process.exit(1); });
+ page.on('console', m => { if (m.type() === 'error') console.error('page:', m.text()); });
+ page.on('requestfailed', r => console.error('request failed:', r.url(), r.failure()?.errorText));
+ await page.goto(url);
+ // plain polling: waitForFunction never resolves in headless shell under WSL
+ for (let i = 0; !(await page.evaluate(() => document.body.dataset.ready || document.body.dataset.error)); i++) {
+ if (i > 300) { console.error('film page never got ready'); process.exit(1); }
+ await new Promise(r => setTimeout(r, 100));
+ }
+ const err = await page.evaluate(() => document.body.dataset.error);
+ if (err) { console.error(err); process.exit(1); }
+ return page;
+}
+const shoot = async (page, f) => { await page.evaluate(f => FILM.render(f), f); return page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: W, height: H } }); };
+
+if (args.stills) {
+ const page = await openPage();
+ fs.mkdirSync(args.outdir || 'out', { recursive: true });
+ for (const f of args.stills.split(',').map(Number)) fs.writeFileSync(path.join(args.outdir || 'out', `f${String(f).padStart(4, '0')}.png`), await shoot(page, f));
+} else {
+ const out = args.out || `out/master-${W}x${H}.mp4`;
+ fs.mkdirSync(path.dirname(out), { recursive: true });
+ const pages = await Promise.all(Array.from({ length: JOBS }, openPage));
+ const FRAMES = await pages[0].evaluate(() => FILM.FRAMES);
+ // near-lossless 4:4:4 master; the web files are encoded from it (encode step in build.sh)
+ const ff = spawn('ffmpeg', ['-v', 'error', '-y', '-f', 'image2pipe', '-framerate', '60', '-c:v', 'png', '-i', '-',
+ '-c:v', 'libx264', '-preset', 'medium', '-crf', '4', '-pix_fmt', 'yuv444p', '-color_primaries', 'bt709', '-color_trc', 'bt709', '-colorspace', 'bt709', out], { stdio: ['pipe', 'inherit', 'inherit'] });
+ const t0 = Date.now();
+ // JOBS pages render ahead in parallel; frames go to ffmpeg strictly in order
+ const pending = new Map(); let next = 0, written = 0;
+ const work = async (page, k) => { for (let f = k; f < FRAMES; f += JOBS) { while (f - written > JOBS * 4) await new Promise(r => setTimeout(r, 5)); pending.set(f, await shoot(page, f)); } };
+ const writer = (async () => {
+ while (next < FRAMES) {
+ if (!pending.has(next)) { await new Promise(r => setTimeout(r, 2)); continue; }
+ const buf = pending.get(next); pending.delete(next);
+ if (!ff.stdin.write(buf)) await new Promise(r => ff.stdin.once('drain', r));
+ written = ++next;
+ if (next % 120 === 0) console.log(`frame ${next}/${FRAMES} ${((Date.now() - t0) / 1000).toFixed(0)} s`);
+ }
+ })();
+ await Promise.all([...pages.map(work), writer]);
+ ff.stdin.end();
+ await new Promise(r => ff.on('close', r));
+ console.log(`done: ${out} in ${((Date.now() - t0) / 1000).toFixed(0)} s`);
+}
+await browser.close();
+server.close();
From e0d5ee599948cafda4d46b0bf1e85202a1db4efb Mon Sep 17 00:00:00 2001
From: isaaclins <104733575+isaaclins@users.noreply.github.com>
Date: Wed, 7 Oct 2026 16:46:31 +0200
Subject: [PATCH 2/4] Intro film: render round 2 (web files + posters)
---
images/home/intro-16x9.webp | Bin 0 -> 23852 bytes
images/home/intro-4x5.webp | Bin 0 -> 6704 bytes
static/video/intro-16x9.mp4 | Bin 0 -> 817869 bytes
static/video/intro-16x9.webm | Bin 0 -> 1078407 bytes
static/video/intro-4x5.mp4 | Bin 0 -> 410743 bytes
static/video/intro-4x5.webm | Bin 0 -> 543573 bytes
6 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 images/home/intro-16x9.webp
create mode 100644 images/home/intro-4x5.webp
create mode 100644 static/video/intro-16x9.mp4
create mode 100644 static/video/intro-16x9.webm
create mode 100644 static/video/intro-4x5.mp4
create mode 100644 static/video/intro-4x5.webm
diff --git a/images/home/intro-16x9.webp b/images/home/intro-16x9.webp
new file mode 100644
index 0000000000000000000000000000000000000000..4764b2a35d58449199511d2dc5396d64799738da
GIT binary patch
literal 23852
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
4LjJiTLDi4p)h=p$KH~3R$lARzea$5pkoxK(X
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