diff --git a/.github/ISSUE_TEMPLATE/tweak-book.yml b/.github/ISSUE_TEMPLATE/tweak-book.yml index cd0e9ae3..7b75ce87 100644 --- a/.github/ISSUE_TEMPLATE/tweak-book.yml +++ b/.github/ISSUE_TEMPLATE/tweak-book.yml @@ -17,6 +17,7 @@ body: options: # BOOKS:START (auto-generated by generator/sync-issue-books.mjs — do not edit by hand) - "Already Clean — neural-networks-already-clean" + - "Handwired · If-else, loops and state as matrix multiply and relu — handwired" - "Jeva: Fresh Decisions · Fixed options, set-aware judgment, and reusable memory — jeva-fresh-decisions" - "Replay QA: { fully managed } · End-to-end testing that explores, runs and records on its own — replay-qa-promo" - "Speaking in Coordinates · How GroundingPI turns images and phrases into structured visual primitives — groundingpi-speaking-in-coordinates" diff --git a/apps/bookshelf/src/viz/books/handwired/chapter-1.stories.tsx b/apps/bookshelf/src/viz/books/handwired/chapter-1.stories.tsx new file mode 100644 index 00000000..ce4e8bdd --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired/chapter-1.stories.tsx @@ -0,0 +1,28 @@ +// Handwired — chapter 1 story. Scene authored by Claude Opus 5.5 (claude-opus-5-5). +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Player } from '../../core'; +import { Render, vizScene } from './chapter-1'; + +/** + * Both Branches Run — the README's if-else taken apart on a number line: two relu ramps subtract + * into a step, the step's bit picks ten or twelve while both are computed, then builder code, the + * four operations and a stack of layers that cannot loop. + */ +function Chapter1() { + const scene = vizScene(); + return ( +
+ + {(s) => } + +
+ ); +} + +const meta: Meta = { + title: 'Books/Handwired/Chapter 1', + component: Chapter1, +}; +export default meta; + +export const Scene: StoryObj = {}; diff --git a/apps/bookshelf/src/viz/books/handwired/chapter-1.tsx b/apps/bookshelf/src/viz/books/handwired/chapter-1.tsx new file mode 100644 index 00000000..20244dd0 --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired/chapter-1.tsx @@ -0,0 +1,1262 @@ +// Both Branches Run +// +// Handwired — chapter 1. Source pin: chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46 (MIT) +// README.md:3 (the pitch), 10-11 (the ternary), 14, 18-21 (the arithmetic: above4 and the pick), 24 +// docs/README.md:3 (no converter; builder code), 15 (the four operations) +// docs/tutorial.md:13 (nothing branches or loops at runtime), 74 (builder code, circuit.reglu([], 1, terms, bias)) +// template/toggle.ts:50-51 (the `not` line) +// docs/execution-model.md:21-28 (the four operations), 40 (layers) +// src/handwiredos/README.md:3, 13 (scale numbers, label only) +// +// Authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan; captions are the fixed +// narration contract (verbatim). The layer stack and the ledger rows are SCHEMATIC (labelled on screen); +// the skyline's 130 columns are one per layer with schematic heights. Every other number is the README's +// own arithmetic, computed below. +import type { ReactNode } from 'react'; +import { scaleLinear } from 'd3'; +import { + CAMERA_HOME, + Camera, + MathLabel, + STAGE_H, + STAGE_W, + Timeline, + cameraInterp, + colors, + ease, + mulberry32, +} from '../../core'; +import type { CameraState, ChannelRef, SceneState } from '../../core'; +import { FunctionPlot, MatrixGrid, Vec } from '../../primitives'; + +const clamp01 = (u: number): number => (u < 0 ? 0 : u > 1 ? 1 : u); +const lerp = (a: number, b: number, u: number): number => a + (b - a) * u; +const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace'; +const CH14 = 8.4; // mono advance at 14 px +const CH13 = 7.8; // mono advance at 13 px +const FAINT = 'rgba(148, 163, 184, 0.12)'; + +// --------------------------------------------------------------------------- +// The README's arithmetic (README.md:18-21), exact +// --------------------------------------------------------------------------- + +const relu = (x: number): number => Math.max(0, x); +const RAMP_A = (a: number): number => relu(a - 4); +const RAMP_B = (a: number): number => relu(a - 5); +const ABOVE4 = (a: number): number => RAMP_A(a) - RAMP_B(a); +const RESULT = (a: number): number => ABOVE4(a) * 10 + (1 - ABOVE4(a)) * 12; +const A_INTS = Array.from({ length: 10 }, (_, i) => i); +const ANSWERS = A_INTS.map(RESULT); // 12 12 12 12 12 10 10 10 10 10 +for (const a of A_INTS) { + // the README example assumes integer a: the bit is exact there + if (ABOVE4(a) !== (a > 4 ? 1 : 0) || RESULT(a) !== (a > 4 ? 10 : 12)) throw new Error('above4 mismatch'); +} + +/** the input as the labels show it: one decimal, so every displayed product reconciles */ +const quant = (a: number): number => Math.round(a * 10) / 10; +const fmt = (v: number): string => (Math.abs(v - Math.round(v)) < 0.02 ? String(Math.round(v)) : v.toFixed(1)); +const commas = (n: number): string => String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ','); + +// --------------------------------------------------------------------------- +// Layout (stage 1280×720; caption band y ≥ 630 stays empty) +// --------------------------------------------------------------------------- + +const X = scaleLinear([0, 9], [150, 690]); +const PLOT_TOP = 130; +const PLOT_BOT = 420; +const DOM_RAMP: [number, number] = [-0.3, 5.3]; +const DOM_STEP: [number, number] = [-0.15, 1.35]; +const yScale = (u: number) => + scaleLinear([lerp(DOM_RAMP[0], DOM_STEP[0], u), lerp(DOM_RAMP[1], DOM_STEP[1], u)], [PLOT_BOT, PLOT_TOP]); +const STRIP_Y = 442; + +// pick machine (right) +const M = { disc: 790, tile: 920, prod: 1040, res: 1170, r1: 235, r2: 375, resY: 305 }; + +// intro composition and its emblem spot (top-left, beside the header chips) +const INTRO_C = { x: 640, y: 330 }; +const INTRO_E = { x: 470, y: 95 }; +const INTRO_GRID = Array.from({ length: 5 }, (_, i) => { + const rand = mulberry32(17 + i * 31); + return Array.from({ length: 5 }, () => (rand() < 0.3 ? 1 : 0)); +}); + +// B7/B8 layout +const C1A = ['const a = 5', 'const result = (a > 4) ? 10 : 12']; +const C1B = [ + 'const above4 = Math.max(0, a - 4) - Math.max(0, a - 5)', + '// Both branches compute, and above4 picks one', + 'const result = above4 * 10 + (1 - above4) * 12', +]; +const C1D = ['// JS: !bit', 'const not = (bit: Wire) => circuit.affine([[bit, -1]], 1)']; +const chipW = (lines: string[]) => Math.max(...lines.map((l) => l.length)) * CH14 + 30; +const LEDGER = { x: 905, y: 140, w: 325 }; +const FLIP_Y = 378; // beside the appended ledger row +const LEDGER_ROWS: Array<{ name: string; expr: string; relu: boolean }> = [ + { name: 'reglu', expr: 'max(0, a − 4)', relu: true }, + { name: 'reglu', expr: 'max(0, a − 5)', relu: true }, + { name: 'affine', expr: 'first − second', relu: false }, + { name: 'affine', expr: 'bit·10 + (1 − bit)·12', relu: false }, + { name: 'affine', expr: 'not: bit × (−1) + 1', relu: false }, +]; + +// B9 cards +const CARD = { w: 270, h: 180, y: 200, xs: [190, 490, 790, 1090] }; +const CARDS = [ + { title: 'weighted sum', fn: 'circuit.affine', tex: '\\sum_i w_i x_i + b', size: 24 }, + { title: 'gated relu', fn: 'circuit.reglu', tex: '\\mathrm{left}\\cdot\\max(0,\\mathrm{gate})', size: 20 }, + { title: 'rounded score', fn: 'circuit.boolean', tex: '\\mathrm{score}\\mapsto\\{0,1\\}', size: 22 }, + { title: 'table read', fn: 'circuit.indexRead', tex: '\\mathrm{values}[\\lfloor \\mathrm{index}\\rfloor]', size: 22 }, +]; +const FAN_FROM = { x: 1067, y: 260 }; + +// B10/B11 stack — schematic of the README example: 4 layers of sparse 6×6 tiles +const LAYER_LIT: Array> = [ + [[1, 1], [4, 3]], // layer 1: the two relu gates, both reading only the input + [[2, 2], [2, 4]], // the difference + [[1, 4], [3, 1], [4, 4]], // the pick + [[3, 3], [0, 2]], // the sum +]; +const LAYER_VALUES = LAYER_LIT.map((lit) => + Array.from({ length: 6 }, (_, i) => Array.from({ length: 6 }, (_, j) => (lit.some(([a, b]) => a === i && b === j) ? 1 : 0))), +); +const LAYER_DESC = ['two relu gates', 'the difference', 'the pick', 'the sum']; +const POSES = [ + { cx: 640, cy: 290, s: 1 }, + { cx: 250, cy: 280, s: 0.5 }, + { cx: 640, cy: 245, s: 0.8 }, +]; +const poseAt = (p: number) => { + const a = p <= 1 ? POSES[0] : POSES[1]; + const b = p <= 1 ? POSES[1] : POSES[2]; + const u = p <= 1 ? clamp01(p) : clamp01(p - 1); + return { cx: lerp(a.cx, b.cx, u), cy: lerp(a.cy, b.cy, u), s: lerp(a.s, b.s, u) }; +}; +const TILE = 117; // 6 cells × 17 + 5 gaps × 3 +const PITCH = 180; + +// the skyline: one column per layer of the desktop demo's program (130), heights schematic +const SKY = (() => { + const rand = mulberry32(4646); + return Array.from({ length: 130 }, (_, j) => ({ x: 60 + j * (1160 / 130), h: 24 + 126 * rand() * rand() + 10 * rand() })); +})(); +const SKY_BASE = 545; +const COUNTERS = [ + { n: 130, label: 'layers' }, + { n: 4646, label: 'live operations' }, + { n: 9909, label: 'weights' }, +]; + +// B11: the loop arrow tries to wrap from layer 4 back to layer 1 and hits the boundary +const BOUND = { x: 350, y: 120, w: 580, h: 235 }; +const LOOP_PTS = (() => { + const p0 = [870, 196], p1 = [900, 40], p2 = [390, 40], p3 = [410, 196]; + const pts: Array<[number, number]> = []; + for (let i = 0; i <= 240; i++) { + const t = i / 240, m = 1 - t; + pts.push([ + m * m * m * p0[0] + 3 * m * m * t * p1[0] + 3 * m * t * t * p2[0] + t * t * t * p3[0], + m * m * m * p0[1] + 3 * m * m * t * p1[1] + 3 * m * t * t * p2[1] + t * t * t * p3[1], + ]); + } + return pts; +})(); +const LOOP_HIT = LOOP_PTS.findIndex(([, y]) => y <= BOUND.y + 3); + +// header chips +const PIN = 'handwired@4ea7fcb'; +const SOURCES = [ + 'README.md:18 · docs/README.md:3', + 'README.md:10-11', + 'README.md:19-21', + 'README.md:19', + 'README.md:18-19', + 'README.md:20-21', + 'README.md:10, 21', + 'README.md:18, 21', + 'docs/README.md:3', + 'template/toggle.ts:50-51 · docs/tutorial.md:74', + 'docs/README.md:15 · docs/execution-model.md:21-28', + 'docs/execution-model.md:40', + 'src/handwiredos/README.md:13', + 'docs/tutorial.md:13', +]; +const SOURCE_AT = [0, 7.9, 15.2, 17.8, 28.0, 37.1, 45.1, 54.2, 63.7, 72.8, 80.4, 92.1, 99.5, 103.1]; + +const HOME: CameraState = CAMERA_HOME; +const CAM_INTRO: CameraState = { x: 640, y: 340, k: 1.15 }; +const CAM_RELU: CameraState = { x: 760, y: 330, k: 1.35 }; +const CAM_MACHINE: CameraState = { x: 960, y: 300, k: 1.3 }; +const CAM_CODE: CameraState = { x: 785, y: 372, k: 1.4 }; +const CAM_STACK: CameraState = { x: 640, y: 300, k: 1.12 }; + +/** per-item progress from one group channel: item i starts `each` seconds after item i-1. */ +const staggerU = (u: number, i: number, n: number, each: number, dur: number): number => { + const total = dur + each * (n - 1); + return clamp01((u * total - i * each) / dur); +}; + +// --------------------------------------------------------------------------- +// Timeline — fifteen fixed captions, ≈114 s +// --------------------------------------------------------------------------- + +export function buildScene() { + const tl = new Timeline(); + const cam = tl.channel('cam', CAM_INTRO, cameraInterp); + const src = tl.channel('src', 0); + const lens = tl.channel('lens', { x: X(5), y: 404, r: 16 }); + const lensOp = tl.channel('lensOp', 0); + + // B0 intro + const gridU = tl.channel('introGridU', 0); + const reluDraw = tl.channel('introReluDraw', 0); + const introLabels = tl.channel('introLabels', 0); + const texRelu = tl.channel('texRelu', 0); + const ifU = tl.channel('ifElseU', 0); + const shrinkU = tl.channel('introShrink', 0); + const introOp = tl.channel('introOp', 1); + // B1–B6 the a-axis and the pick machine + const c1aU = tl.channel('c1aU', 0); + const c1bU = tl.channel('c1bU', 0); + const codeOp = tl.channel('codeOp', 1); + const axisU = tl.channel('axisU', 0); + const dotsU = tl.channel('dotsU', 0); + const stripU = tl.channel('stripU', 0); + const stripOp = tl.channel('stripOp', 1); + const aReveal = tl.channel('rampA', 0); + const rideU = tl.channel('rideU', 0); + const texA = tl.channel('texA', 0); + const ghostU = tl.channel('ghostU', 0); + const ghostShift = tl.channel('ghostShift', 0); + const bSolid = tl.channel('rampB', 0); + const texB = tl.channel('texB', 0); + const segU = tl.channel('segU', 0); + const morphU = tl.channel('morphU', 0); + const bOp = tl.channel('rampBOp', 1); + const styleU = tl.channel('stepStyle', 0); + const discU = tl.channel('discU', 0); + const texD = tl.channel('texD', 0); + const tagU = tl.channel('tagU', 0); + const plotOp = tl.channel('plotOp', 1); + const tilesU = tl.channel('tilesU', 0); + const row1U = tl.channel('row1U', 0); + const row2U = tl.channel('row2U', 0); + const sumU = tl.channel('sumU', 0); + const texR = tl.channel('texR', 0); + const machOp = tl.channel('machOp', 1); + const markerU = tl.channel('markerU', 0); + const aVal = tl.channel('a', 5); + const pulse2 = tl.channel('pulse2', 0); + const readU = tl.channel('readU', 0); + const slopeGlow = tl.channel('slopeGlow', 0); + const blendU = tl.channel('blendU', 0); + const noteU = tl.channel('noteU', 0); + // B7/B8 builder code + const c1aMove = tl.channel('c1aMove', 0); + const convU = tl.channel('convU', 0); + const crossU = tl.channel('crossU', 0); + const quoteU = tl.channel('quoteU', 0); + const writeU = tl.channel('writeU', 0); + const ledgerU = tl.channel('ledgerU', 0); + const rowsU = tl.channel('rowsU', 0); + const row1Dim = tl.channel('b7Row1Dim', 0); + const b7Op = tl.channel('b7Op', 1); + const c1dU = tl.channel('c1dU', 0); + const c1dGlow = tl.channel('c1dGlow', 0); + const flipU = tl.channel('flipU', 0); + const collapseU = tl.channel('ledgerCollapse', 0); + // B9 cards + const fanU = tl.channel('fanU', 0); + const spotU = tl.channel('cardSpot', 0); + const focus = tl.channel('cardFocus', 0); + const fp32U = tl.channel('fp32Note', 0); + // B10/B11 stack + const dropU = tl.channel('dropU', 0); + const stackLbl = tl.channel('stackLabels', 0); + const gatesU = tl.channel('gatesU', 0); + const pose = tl.channel('stackPose', 0); + const skyU = tl.channel('skyU', 0); + const skyOp = tl.channel('skyOp', 1); + const countU = tl.channel('countU', 0); + const countOp = tl.channel('countOp', 1); + const stackOp = tl.channel('stackOp', 1); + const boundU = tl.channel('boundU', 0); + const loopU = tl.channel('loopU', 0); + const loopX = tl.channel('loopCross', 0); + const loopOp = tl.channel('loopOp', 1); + const qU = tl.channel('questionU', 0); + + SOURCE_AT.forEach((at, i) => tl.set(src, i, at)); + + // — B0 · weighted sums plus one bend — + tl.caption({ at: 0.4, dur: 7.2, text: 'A basic neural net layer is a matrix multiply followed by one bend, called relu. Could an ordinary decision be built from nothing else?' }); + tl.tween(gridU, 1, { at: 0.6, dur: 1.2, ease: ease.linear }); + tl.tween(cam, CAM_RELU, { at: 1.6, dur: 1.3, ease: ease.move }); + tl.tween(reluDraw, 1, { at: 2.0, dur: 1.3, ease: ease.draw }); + tl.tween(introLabels, 1, { at: 2.8, dur: 0.6, ease: ease.enter }); + tl.tween(texRelu, 1, { at: 3.4, dur: 0.6, ease: ease.enter }); + tl.tween(cam, CAM_INTRO, { at: 4.5, dur: 1.2, ease: ease.move }); + tl.tween(ifU, 1, { at: 5.2, dur: 0.7, ease: ease.enter }); + tl.tween(shrinkU, 1, { at: 6.6, dur: 1.1, ease: ease.move }); + tl.hold(7.6, 0.4); + + // — B1 · one decision, and its rewrite — + tl.caption({ at: 8, dur: 9.5, text: "Take one decision: if the input a is greater than four, the answer is ten, otherwise twelve. The repo's front page rewrites it as arithmetic." }); + tl.tween(cam, HOME, { at: 8.0, dur: 1.2, ease: ease.move }); + tl.tween(c1aU, 1, { at: 8.2, dur: 0.7, ease: ease.enter }); + tl.tween(axisU, 1, { at: 9.0, dur: 1.4, ease: ease.draw }); + tl.tween(dotsU, 1, { at: 9.9, dur: 0.9, ease: ease.linear }); + tl.set(lens, { x: X(5), y: yScale(0)(0), r: 16 }, 10.9); + tl.tween(lensOp, 1, { at: 11.0, dur: 0.5, ease: ease.enter }); + tl.tween(stripU, 1, { at: 11.6, dur: 0.9, ease: ease.linear }); + tl.tween(lensOp, 0, { at: 14.2, dur: 0.5, ease: ease.move }); + tl.tween(c1bU, 1, { at: 15.2, dur: 0.8, ease: ease.enter }); + tl.hold(17.5, 0.4); + + // — B2 · two ramps — + tl.caption({ at: 17.9, dur: 9.8, text: 'Two ramps: relu of the input minus four, flat until four, and relu of the input minus five, the same ramp one step to the right.' }); + tl.tween(stripOp, 0.35, { at: 17.9, dur: 0.6, ease: ease.move }); + tl.tween(codeOp, 0.35, { at: 17.9, dur: 0.6, ease: ease.move }); + tl.tween(introOp, 0, { at: 17.9, dur: 0.6, ease: ease.move }); + tl.tween(aReveal, 1, { at: 18.3, dur: 1.4, ease: ease.draw }); + tl.tween(rideU, 1, { at: 19.4, dur: 1.0, ease: ease.move }); + tl.set(lens, { x: X(4), y: yScale(0)(0), r: 20 }, 19.5); + tl.tween(lensOp, 1, { at: 19.6, dur: 0.5, ease: ease.enter }); + tl.tween(texA, 1, { at: 20.2, dur: 0.6, ease: ease.enter }); + tl.tween(lensOp, 0, { at: 22.0, dur: 0.5, ease: ease.move }); + tl.tween(ghostU, 1, { at: 23.0, dur: 0.4, ease: ease.enter }); + tl.tween(ghostShift, 1, { at: 23.4, dur: 1.2, ease: ease.move }); + tl.tween(bSolid, 1, { at: 24.6, dur: 0.5, ease: ease.enter }); + tl.tween(ghostU, 0, { at: 24.6, dur: 0.5, ease: ease.move }); + tl.tween(texB, 1, { at: 25.0, dur: 0.6, ease: ease.enter }); + tl.hold(27.7, 0.4); + + // — B3 · subtract: the difference is a step, the dots become bits — + tl.caption({ at: 28.1, dur: 2.6, text: 'Subtract the second ramp from the first.' }); + tl.caption({ at: 30.7, dur: 6.1, text: 'For whole numbers, the difference is zero up to four, and exactly one from five on.' }); + tl.tween(segU, 1, { at: 28.4, dur: 1.0 + 0.12 * 4, ease: ease.linear }); + tl.tween(texA, 0, { at: 30.4, dur: 0.5, ease: ease.move }); + tl.tween(texB, 0, { at: 30.4, dur: 0.5, ease: ease.move }); + tl.tween(morphU, 1, { at: 30.6, dur: 1.4, ease: ease.move }); + tl.tween(bOp, 0.15, { at: 30.6, dur: 1.0, ease: ease.move }); + tl.tween(styleU, 1, { at: 32.1, dur: 0.5, ease: ease.enter }); + tl.tween(discU, 1, { at: 32.4, dur: 0.6, ease: ease.enter }); + tl.tween(texD, 1, { at: 32.8, dur: 0.6, ease: ease.enter }); + tl.tween(tagU, 1, { at: 33.6, dur: 0.5, ease: ease.enter }); + tl.hold(36.8, 0.4); + + // — B4 · the pick machine: both answers computed — + tl.caption({ at: 37.2, dur: 7.6, text: 'Both answers get computed. The bit multiplies the ten, one minus the bit multiplies the twelve, and the products add.' }); + tl.tween(cam, CAM_MACHINE, { at: 37.2, dur: 1.2, ease: ease.move }); + tl.tween(plotOp, 0.35, { at: 37.2, dur: 0.8, ease: ease.move }); + tl.tween(codeOp, 0, { at: 37.2, dur: 0.8, ease: ease.move }); + tl.tween(tilesU, 1, { at: 37.8, dur: 0.6, ease: ease.enter }); + tl.tween(texR, 1, { at: 38.4, dur: 0.6, ease: ease.enter }); + tl.tween(row1U, 1, { at: 39.2, dur: 0.6, ease: ease.enter }); + tl.set(lens, { x: M.tile, y: M.r1, r: 44 }, 39.3); + tl.tween(lensOp, 1, { at: 39.4, dur: 0.4, ease: ease.enter }); + tl.tween(lens, { x: M.tile, y: M.r2, r: 44 }, { at: 41.0, dur: 0.6, ease: ease.move }); + tl.tween(row2U, 1, { at: 41.0, dur: 0.6, ease: ease.enter }); + tl.tween(lensOp, 0, { at: 42.6, dur: 0.4, ease: ease.move }); + tl.tween(sumU, 1, { at: 43.0, dur: 0.7, ease: ease.enter }); + tl.hold(44.8, 0.4); + + // — B5 · slide the input: the bit flips between four and five — + tl.caption({ at: 45.2, dur: 8.7, text: 'Slide the input along. At five the bit becomes one: ten passes through, and twelve is multiplied by zero, though it was computed.' }); + tl.tween(cam, HOME, { at: 45.2, dur: 1.2, ease: ease.move }); + tl.tween(plotOp, 1, { at: 45.2, dur: 0.8, ease: ease.move }); + tl.set(aVal, 3, 45.9); + tl.tween(markerU, 1, { at: 46.0, dur: 0.5, ease: ease.enter }); + tl.tween(readU, 1, { at: 46.2, dur: 0.6, ease: ease.enter }); + // whole-number hops (no overshoot: the readout must show 5, not 5.1, as the bit lands) + [4, 5, 6, 7].forEach((v, i) => tl.tween(aVal, v, { at: 47.0 + i * 0.75, dur: 0.45, ease: ease.move })); + tl.tween(pulse2, 1, { at: 50.6, dur: 0.4, ease: ease.enter }); + tl.tween(pulse2, 0, { at: 51.2, dur: 0.7, ease: ease.move }); + // the slide to 4.5 lands before the blend caption, so the marker rests there for all of it + tl.tween(aVal, 4.5, { at: 52.8, dur: 1.3, ease: ease.move }); + tl.hold(53.9, 0.4); + + // — B6 · between whole numbers: a blend — + tl.caption({ at: 54.3, dur: 9.1, text: 'Between whole numbers the bit is a fraction: at four and a half the answer is eleven, a blend. The example assumes whole numbers.' }); + tl.tween(slopeGlow, 1, { at: 55.2, dur: 0.6, ease: ease.enter }); + tl.tween(blendU, 1, { at: 57.4, dur: 0.5, ease: ease.pop }); + tl.tween(noteU, 1, { at: 60.6, dur: 0.6, ease: ease.enter }); + tl.hold(63.4, 0.4); + + // — B7 · nothing converts your code: you write builder code — + tl.caption({ at: 63.8, dur: 8.7, text: 'To be exact, nothing converts your existing Javascript. You write builder code: ordinary Javascript that runs once and appends operations to a circuit.' }); + for (const ch of [plotOp, machOp]) tl.tween(ch, 0, { at: 63.8, dur: 0.8, ease: ease.move }); + for (const ch of [markerU, readU, blendU, noteU, slopeGlow, codeOp]) tl.tween(ch, 0, { at: 63.8, dur: 0.6, ease: ease.move }); + tl.tween(c1aMove, 1, { at: 63.9, dur: 1.2, ease: ease.move }); + tl.tween(convU, 1, { at: 65.0, dur: 0.6, ease: ease.enter }); + tl.tween(crossU, 1, { at: 65.6, dur: 0.8, ease: ease.draw }); + tl.tween(quoteU, 1, { at: 66.3, dur: 0.6, ease: ease.enter }); + tl.tween(writeU, 1, { at: 67.2, dur: 0.8, ease: ease.enter }); + tl.tween(ledgerU, 1, { at: 68.8, dur: 0.6, ease: ease.enter }); + tl.tween(rowsU, 4, { at: 69.6, dur: 2.2, ease: ease.linear }); + tl.hold(72.5, 0.4); + + // — B8 · one line of builder code: not — + tl.caption({ at: 72.9, dur: 7.2, text: 'Each line of builder code appends the operations for one familiar expression. Here, not becomes one minus the bit.' }); + tl.tween(cam, CAM_CODE, { at: 72.9, dur: 1.2, ease: ease.move }); + tl.tween(row1Dim, 1, { at: 72.9, dur: 0.6, ease: ease.move }); + tl.tween(c1dU, 1, { at: 73.2, dur: 0.6, ease: ease.enter }); + tl.tween(c1dGlow, 1, { at: 74.0, dur: 0.4, ease: ease.enter }); + tl.tween(c1dGlow, 2, { at: 76.8, dur: 0.4, ease: ease.move }); + tl.tween(rowsU, 5, { at: 77.6, dur: 0.6, ease: ease.enter }); + tl.tween(flipU, 1, { at: 78.4, dur: 0.8, ease: ease.move }); + tl.hold(80.1, 0.4); + + // — B9 · four kinds of operation — + tl.caption({ at: 80.5, dur: 11.3, text: 'Only four kinds of operation exist: a weighted sum, a gated relu, a score rounded to zero or one, and a table read. Every result is a thirty-two-bit float.' }); + tl.tween(cam, HOME, { at: 80.5, dur: 1.2, ease: ease.move }); + tl.tween(b7Op, 0, { at: 80.5, dur: 0.6, ease: ease.move }); + tl.tween(collapseU, 1, { at: 80.6, dur: 0.6, ease: ease.move }); + for (const ch of [plotOp, machOp]) tl.tween(ch, 0, { at: 80.5, dur: 0.6, ease: ease.move }); + tl.tween(fanU, 1, { at: 81.0, dur: 1.4, ease: ease.move }); + tl.tween(spotU, 1, { at: 82.4, dur: 0.3, ease: ease.move }); + [1, 2, 3].forEach((f, i) => tl.tween(focus, f, { at: [83.9, 85.2, 87.1][i], dur: 0.3, ease: ease.move })); + tl.tween(spotU, 0, { at: 88.5, dur: 0.5, ease: ease.move }); + // the FP32 note lands with the spoken "every result is a thirty-two-bit float" + tl.tween(fp32U, 1, { at: 88.7, dur: 0.6, ease: ease.enter }); + tl.hold(91.8, 0.4); + + // — B10 · a layer is one sparse matrix multiply plus the gates; a program is a stack — + tl.caption({ at: 92.2, dur: 8.0, text: "Operations that don't depend on each other share a layer, and a layer runs as one sparse matrix multiply plus the gates." }); + tl.caption({ at: 100.2, dur: 2.6, text: 'A program is a stack of layers.' }); + tl.tween(cam, CAM_STACK, { at: 92.2, dur: 1.2, ease: ease.move }); + tl.tween(fp32U, 0, { at: 92.2, dur: 0.5, ease: ease.move }); + tl.tween(dropU, 1, { at: 92.4, dur: 1.4, ease: ease.move }); + tl.tween(stackLbl, 1, { at: 93.6, dur: 0.6, ease: ease.enter }); + tl.tween(gatesU, 1, { at: 94.4, dur: 0.6, ease: ease.enter }); + tl.tween(cam, HOME, { at: 99.5, dur: 1.3, ease: ease.move }); + tl.tween(pose, 1, { at: 99.5, dur: 1.3, ease: ease.move }); + tl.tween(skyU, 1, { at: 100.1, dur: 1.6, ease: ease.linear }); + tl.tween(countU, 1, { at: 100.5, dur: 1.8, ease: ease.enter }); + tl.hold(102.8, 0.4); + + // — B11 · nothing branches or loops at runtime — + tl.caption({ at: 103.2, dur: 4.3, text: 'Nothing in a stack like that branches or loops at runtime.' }); + tl.caption({ at: 107.5, dur: 4.0, text: 'So if every branch always runs, what happens to a loop?' }); + tl.tween(skyOp, 0.12, { at: 103.2, dur: 0.8, ease: ease.move }); + tl.tween(countOp, 0, { at: 103.2, dur: 0.6, ease: ease.move }); + tl.tween(pose, 2, { at: 103.3, dur: 1.2, ease: ease.move }); + tl.tween(boundU, 1, { at: 104.4, dur: 1.0, ease: ease.draw }); + tl.tween(loopU, 1, { at: 105.4, dur: 1.0, ease: ease.draw }); + tl.tween(loopU, 0.8, { at: 106.4, dur: 0.5, ease: ease.pop }); + tl.tween(loopX, 1, { at: 106.5, dur: 0.4, ease: ease.pop }); + for (const ch of [stackOp, loopOp]) tl.tween(ch, 0.12, { at: 107.4, dur: 0.8, ease: ease.move }); + tl.tween(skyOp, 0, { at: 107.4, dur: 0.8, ease: ease.move }); + tl.tween(qU, 1, { at: 107.8, dur: 0.7, ease: ease.enter }); + tl.hold(111.5, 2.5); + + return { + tl, cam, src, lens, lensOp, gridU, reluDraw, introLabels, texRelu, ifU, shrinkU, introOp, + c1aU, c1bU, codeOp, axisU, dotsU, stripU, stripOp, aReveal, rideU, texA, ghostU, ghostShift, bSolid, texB, + segU, morphU, bOp, styleU, discU, texD, tagU, plotOp, tilesU, row1U, row2U, sumU, texR, machOp, + markerU, aVal, pulse2, readU, slopeGlow, blendU, noteU, + c1aMove, convU, crossU, quoteU, writeU, ledgerU, rowsU, row1Dim, b7Op, c1dU, c1dGlow, flipU, collapseU, + fanU, spotU, focus, fp32U, dropU, stackLbl, gatesU, pose, skyU, skyOp, countU, countOp, stackOp, + boundU, loopU, loopX, loopOp, qU, + }; +} + +const scene = buildScene(); + +// --------------------------------------------------------------------------- +// Glyph kit (book-wide spec, redrawn locally) +// --------------------------------------------------------------------------- + +/** Violet hockey stick: flat to the middle, then a 45° rise. (x, y) is the bottom-left. */ +function ReluGlyph({ x, y, w = 22, color = colors.SECONDARY, width = 2.5, reveal = 1, opacity = 1 }: { + x: number; y: number; w?: number; color?: string; width?: number; reveal?: number; opacity?: number; +}) { + if (opacity <= 0.005 || reveal <= 0) return null; + const len = w / 2 + (w / 2) * Math.SQRT2; + return ( + + ); +} + +/** The bit: amber disc when 1, ring when 0, filled from the bottom when fractional. */ +function BitDisc({ cx, cy, b, r = 11, opacity = 1, showNum = true }: { + cx: number; cy: number; b: number; r?: number; opacity?: number; showNum?: boolean; +}) { + if (opacity <= 0.005) return null; + const v = clamp01(b); + let fill: ReactNode = null; + if (v >= 0.995) fill = ; + else if (v > 0.005) { + const ly = cy + r - 2 * r * v; + const w = Math.sqrt(Math.max(0, r * r - (ly - cy) * (ly - cy))); + fill = 0.5 ? 1 : 0} 0 ${cx + w} ${ly} Z`} fill={colors.WARM} />; + } + return ( + + + {fill} + + {showNum && r >= 11 && ( + 0.6 ? '#1a1206' : v > 0.05 ? '#ffffff' : colors.WARM} + stroke={v > 0.05 && v <= 0.6 ? '#0a0e1a' : 'none'} + strokeWidth={3} + paintOrder="stroke" + > + {fmt(v)} + + )} + + ); +} + +/** Thin white ring with a short leader: the spotlight (never a box). */ +function LensRing({ x, y, r, opacity }: { x: number; y: number; r: number; opacity: number }) { + if (opacity <= 0.005) return null; + const a = -Math.PI / 4; + return ( + + + + + ); +} + +/** Verbatim code chip: mono 14 px, PANEL fill, GRID border, a role-color stripe. */ +function CodeChip({ x, y, lines, stripe, opacity = 1, glow }: { + x: number; y: number; lines: string[]; stripe: string; opacity?: number; glow?: number[]; +}) { + if (opacity <= 0.005) return null; + const w = chipW(lines); + const h = lines.length * 20 + 14; + return ( + + + + {lines.map((l, i) => ( + + {glow && glow[i] > 0.01 && } + + {l} + + + ))} + + ); +} + +/** One ledger row: [name] [expression], relu operations carry the violet stripe and glyph. */ +function OpChip({ x, y, w, name, expr, isRelu, opacity }: { + x: number; y: number; w: number; name: string; expr: string; isRelu: boolean; opacity: number; +}) { + if (opacity <= 0.005) return null; + return ( + + + + + {name} + + + {expr} + + {isRelu && } + + ); +} + +function Tile({ x, y, w, h, text, stroke, opacity, dashed = false, size = 26, fillOpacity = 1 }: { + x: number; y: number; w: number; h: number; text: string; stroke: string; opacity: number; dashed?: boolean; size?: number; fillOpacity?: number; +}) { + if (opacity <= 0.005) return null; + return ( + + + + {text} + + + ); +} + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +export function Render({ s }: { s: SceneState }) { + const v = (ch: ChannelRef): number => s.get(ch); + + const cam = s.get(scene.cam); + const srcIdx = Math.max(0, Math.min(SOURCES.length - 1, Math.round(v(scene.src)))); + const lens = s.get(scene.lens); + + // ---- plot state + const morph = v(scene.morphU); + const Y = yScale(morph); + const y0 = Y(0); + const plotOp = v(scene.plotOp); + const axisU = v(scene.axisU); + const rideU = v(scene.rideU); + const discU = v(scene.discU); + const styleU = v(scene.styleU); + const segU = v(scene.segU); + const ghostShift = v(scene.ghostShift); + const a = v(scene.aVal); + const bit = ABOVE4(a); + const aQ = quant(a); + const bitQ = ABOVE4(aQ); + const markerU = v(scene.markerU); + const stripOp = v(scene.stripOp) * v(scene.stripU); + const dotsU = v(scene.dotsU); + + // ---- intro + const shrink = v(scene.shrinkU); + const introS = lerp(1, 0.25, shrink); + const introOp = v(scene.introOp) * lerp(1, 0.35, shrink); + const introTx = lerp(INTRO_C.x, INTRO_E.x, shrink); + const introTy = lerp(INTRO_C.y, INTRO_E.y, shrink); + + // ---- machine + const machOp = v(scene.machOp); + const tilesU = v(scene.tilesU); + const row1U = v(scene.row1U); + const row2U = v(scene.row2U); + const sumU = v(scene.sumU); + const p1 = bitQ * 10; + const p2 = (1 - bitQ) * 12; + const pulse2 = v(scene.pulse2); + + // ---- builder code + const c1aMove = v(scene.c1aMove); + const b7Op = v(scene.b7Op); + const r1Op = b7Op * (1 - v(scene.row1Dim)); + const rowsU = v(scene.rowsU); + const collapse = v(scene.collapseU); + const ledgerOp = v(scene.ledgerU) * (1 - collapse); + const c1dGlow = v(scene.c1dGlow); + const flipU = v(scene.flipU); + + // ---- cards + stack + const fanU = v(scene.fanU); + const dropU = v(scene.dropU); + const spotU = v(scene.spotU); + const focus = v(scene.focus); + const P = poseAt(v(scene.pose)); + const stackOp = v(scene.stackOp); + const tileX = (i: number) => P.cx + (i - 1.5) * PITCH * P.s; + const T = TILE * P.s; + const shrunk = clamp01(v(scene.pose)) * (v(scene.pose) <= 1 ? 1 : 1 - clamp01(v(scene.pose) - 1)); + const tilesIn = clamp01((dropU - 0.55) / 0.45); + const skyU = v(scene.skyU); + const countU = v(scene.countU); + const loopU = v(scene.loopU); + const loopX = v(scene.loopX); + const qU = v(scene.qU); + + // loop arrow geometry (drawn up to the boundary contact, then the bounce) + const loopN = Math.max(1, Math.round(LOOP_HIT * clamp01(loopU))); + const loopPts = LOOP_PTS.slice(0, loopN + 1); + const tip = loopPts[loopPts.length - 1]; + const prev = loopPts[Math.max(0, loopPts.length - 4)]; + const ang = Math.atan2(tip[1] - prev[1], tip[0] - prev[0]); + const hit = LOOP_PTS[LOOP_HIT]; + + const chip = `${PIN} · ${SOURCES[srcIdx]}`; + + return ( + <> + + + + + + + + {/* ================= B0 · weighted sums + one bend (shrinks to an emblem) ================= */} + {introOp > 0.005 && ( + + staggerU(v(scene.gridU), i * 5 + j, 25, 0.04, 0.3)} + fill={(val) => (val > 0 ? colors.ACCENT : FAINT)} + /> + + + + + 0 ? 0.7 : 0} /> + 0 ? 0.7 : 0} /> + + + + weighted sums + + + relu + + + + {v(scene.ifU) > 0.005 && ( + + + + an if / else? + + + )} + + )} + + {/* ================= B1–B6 · the a-axis: ramps → step → bits ================= */} + {plotOp > 0.005 && axisU > 0 && ( + + {/* axes (local: integer ticks only, so the step reads 0 and 1) */} + + + {A_INTS.map((i) => ( + + + + {i} + + + ))} + + a + + {[1, 2, 3, 4, 5].map((t) => { + const yy = Y(t); + const op = clamp01((yy - PLOT_TOP + 4) / 20) * axisU; + if (op <= 0.01) return null; + return ( + + + + + {t} + + + ); + })} + + 0 + + + {/* the curves (clipped to the plot box while the y-domain changes) */} + + {/* ramp B: the ghost copy of A slides one step right, then settles in rose */} + {v(scene.ghostU) > 0.005 && ( + relu(t - 4 - ghostShift)} color={colors.ACCENT} width={2.5} dash opacity={0.55 * v(scene.ghostU)} /> + )} + + {/* ramp A morphs into the difference: accent → amber */} + {styleU < 0.999 && ( + <> + + + + )} + {styleU > 0.001 && ( + + + + + {v(scene.slopeGlow) > 0.005 && ( + + + + + )} + + )} + + {/* "+1 step": A's ramp at height 3 moves one step right */} + + {ghostShift > 0.05 && morph < 0.99 && ( + + +1 step + + )} + {/* subtraction segments for a = 5..9: drop from A by B's value (gone early in the morph) */} + {segU > 0 && morph < 0.34 && ( + + {[5, 6, 7, 8, 9].map((i, k) => { + const u = staggerU(segU, k, 5, 0.12, 1.0); + if (u <= 0) return null; + const yA = Y(RAMP_A(i)); + const yEnd = Y(RAMP_A(i) - RAMP_B(i)); + return ; + })} + + )} + {/* small rose dots on ramp B */} + {v(scene.bSolid) > 0.005 && + A_INTS.map((i) => ( + = PLOT_TOP ? 1 : 0)} /> + ))} + {/* the ten integer dots: on the axis → ride ramp A → fall onto the step → bit discs */} + {A_INTS.map((i) => { + const u = staggerU(dotsU, i, 10, 0.06, 0.4); + if (u <= 0) return null; + const base = RAMP_A(i) * rideU; + const val = base + (ABOVE4(i) - base) * morph; + const cy = Y(val); + return ( + + {discU < 0.999 && } + 0.9} /> + + ); + })} + + + + {v(scene.tagU) > 0.005 && ( + + 0 + 1 + + )} + + {/* the marker a: drives the pick machine */} + {markerU > 0.005 && ( + + + + + + + a = {fmt(aQ)} + + + )} + + {/* answer strip: a = 0..9 → 12 12 12 12 12 10 10 10 10 10 */} + {stripOp > 0.005 && ( + + + result + + {A_INTS.map((i) => { + const u = staggerU(v(scene.stripU), i, 10, 0.05, 0.45); + const hl = markerU * clamp01(1 - Math.abs(a - i)); + return ( + + + + {ANSWERS[i]} + + + ); + })} + + )} + + )} + + {/* ================= B1 code chips: the ternary and its rewrite ================= */} + + {v(scene.c1bU) > 0.005 && v(scene.codeOp) > 0.005 && ( + + + + + + rewrite + + + + + )} + + {/* ================= B4–B6 · the pick machine ================= */} + {machOp > 0.005 && tilesU > 0.005 && ( + + + {/* both answer tiles: always computed, always lit */} + + + {/* row 1: above4 × 10 */} + + + + above4 + + × + = + + + {/* row 2: (1 − above4) × 12 */} + + {pulse2 > 0.005 && } + + + 1 − above4 + + 0.3 ? colors.WARM : colors.MUTED}>× + = + 0.5} /> + + {/* the products add */} + + + + + + result + + + + {v(scene.blendU) > 0.005 && ( + + + + blend + + + )} + {/* live readouts */} + {v(scene.readU) > 0.005 && ( + + a = {fmt(aQ)} above4 = {fmt(bitQ)} result = {fmt(p1 + p2)} + + )} + {v(scene.noteU) > 0.005 && ( + + the example assumes an integer a (README.md:18) + + )} + + )} + + + + {/* ================= B7/B8 · builder code, not a converter ================= */} + {b7Op > 0.005 && c1aMove > 0.005 && ( + + {/* the ternary moves up: the one line nobody converts */} + + + + + + converter + + + {v(scene.crossU) > 0.005 && ( + + + 0.29 ? 1 : 0} /> + + )} + + + + + “No tool converts existing JavaScript source into a program.” + + + docs/README.md:3 + + + + + + you write + + + + + + builder code + + + ordinary JavaScript · runs once + + + + appends operations + + + {/* B8: one line of builder code, and the bits it flips */} + + {v(scene.c1dU) > 0.005 && ( + + source: template/toggle.ts:50-51 + + )} + {flipU > 0.005 && ( + + not + + 1 → 0 + + 0 → 1 + + )} + + )} + {/* the Circuit ledger (schematic) */} + {ledgerOp > 0.005 && ( + + + + Circuit + + + schematic + + {LEDGER_ROWS.map((r, i) => ( + + {i === 4 && c1dGlow > 1.01 && } + + + ))} + + )} + + {/* ================= B9 · the four kinds of operation → B10 layers ================= */} + {fanU > 0.005 && dropU < 0.999 && + CARDS.map((c, i) => { + const u = clamp01(fanU * 1.6 - i * 0.2); + if (u <= 0.005) return null; + const slotX = CARD.xs[i]; + const slotY = CARD.y + CARD.h / 2; + // fan from the ledger, then drop into the stack column i + const fx = lerp(FAN_FROM.x, slotX, u); + const fy = lerp(FAN_FROM.y, slotY, u); + const fs = lerp(0.3, 1, u); + const dp = clamp01(dropU / 0.6); // the card lands on its column before the tile fades in + const tx = lerp(fx, tileX(i), dp); + const ty = lerp(fy, P.cy, dp); + const sw = lerp(CARD.w * fs, T + 12, dp); + const sh = lerp(CARD.h * fs, T + 12, dp); + const spot = 1 - spotU * 0.65 * clamp01(Math.abs(focus - i)); + const contentOp = clamp01(1 - dropU * 2.2); + const k = fs * lerp(1, 0.45, dp); + return ( + + + {contentOp > 0.005 && ( + + + {c.title} + + + {c.fn} + + + + {i === 2 && ( + + rounds at one half + + )} + + )} + + ); + })} + {v(scene.fp32U) > 0.005 && ( + + + every value: FP32 (Math.fround) + + + docs/tutorial.md:74 · card math drawn unrounded · names from docs/README.md:15 + + + )} + + {/* ================= B10/B11 · the stack (schematic of the example) ================= */} + {skyU > 0.005 && ( + + {SKY.map((c, j) => { + const u = clamp01(skyU * 130 - j); + if (u <= 0) return null; + return ; + })} + + the desktop demo's program · one column per layer, heights schematic + + + )} + {dropU > 0.005 && stackOp > 0.005 && ( + + {[0, 1, 2, 3].map((i) => ( + + + clamp01(tilesIn * 1.5 - (r * 6 + c) / 72)} + fill={(val) => (val > 0 ? (i === 0 ? colors.SECONDARY : colors.ACCENT) : FAINT)} + /> + {i < 3 && } + 0.7 ? 15 : 13} fill={colors.TEXT} opacity={v(scene.stackLbl)}> + layer {i + 1} + + + {LAYER_DESC[i]} + + + ))} + {/* layer 1: the two relu gates side by side (both read only the input) */} + + + + + + schematic of the example + + + )} + {countU > 0.005 && ( + + + the desktop demo's program + + {COUNTERS.map((c, i) => ( + + + {commas(c.n * countU)} + + + {c.label} + + + ))} + + src/handwiredos/README.md:13 + + + )} + {v(scene.boundU) > 0.005 && ( + + + + a fixed list + + + )} + {loopU > 0.005 && ( + + p.join(',')).join(' ')} fill="none" stroke={colors.NEGATIVE} strokeWidth={1.5} strokeDasharray="3 6" opacity={0.3 * loopX} /> + p.join(',')).join(' ')} fill="none" stroke={colors.NEGATIVE} strokeWidth={3.5} strokeLinecap="round" /> + + + loop? + + {loopX > 0.005 && ( + + + + + )} + + )} + {qU > 0.005 && ( + + + + If every branch always runs, what happens to a loop? + + + )} + + + {/* ================= screen-space header: title chip + source chip ================= */} + + + 1 · Both Branches Run + + + + {chip} + + + ); +} + +/** Small per-card glyphs for the four operation kinds. */ +function CardGlyph({ i, cx, cy }: { i: number; cx: number; cy: number }) { + if (i === 0) { + // weighted sum: three inputs converge on one node + return ( + + {[-16, 0, 16].map((dy, k) => ( + + + + + ))} + + + + ); + } + if (i === 1) return ; + if (i === 2) { + // rounded score: a staircase 0 → 1 with the cut at one half + const x0 = cx - 50, x1 = cx + 50, yb = cy + 16, yt = cy - 16; + return ( + + + + + 0.5 + + ); + } + // table read: a row of cells, one read + return ( + + {[0, 1, 2, 3, 4].map((k) => ( + + ))} + + + ); +} + +export const vizScene = () => scene; diff --git a/apps/bookshelf/src/viz/books/handwired/chapter-2.stories.tsx b/apps/bookshelf/src/viz/books/handwired/chapter-2.stories.tsx new file mode 100644 index 00000000..75d8936f --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired/chapter-2.stories.tsx @@ -0,0 +1,27 @@ +// Handwired — chapter 2 story. Scene authored by Claude Opus 5.5 (claude-opus-5-5). +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Player } from '../../core'; +import { Render, vizScene } from './chapter-2'; + +/** + * A Switch of Arithmetic — the toggle's 20-cell table as a wire tape: four comparisons, an and + * as relu, two selects, and the spring as a matrix power the host writes into four cells. + */ +function Chapter2() { + const scene = vizScene(); + return ( +
+ + {(s) => } + +
+ ); +} + +const meta: Meta = { + title: 'Books/Handwired/Chapter 2', + component: Chapter2, +}; +export default meta; + +export const Scene: StoryObj = {}; diff --git a/apps/bookshelf/src/viz/books/handwired/chapter-2.tsx b/apps/bookshelf/src/viz/books/handwired/chapter-2.tsx new file mode 100644 index 00000000..b4bb1764 --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired/chapter-2.tsx @@ -0,0 +1,1396 @@ +// A Switch of Arithmetic +// +// Handwired — chapter 2. Source pin: chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46 (MIT, Cheng Lou) +// template/toggle.ts:9-38 layout, spring constants, the 20 cells (toggleCells) +// template/toggle.ts:40-84 builder code: 46-47 track edges, 48-58 all / not / insideTrack, +// 60-65 the three state updates, 67-82 animation and table.next +// docs/tutorial.md:3, 53, 74, 93, 106, 145 what the switch does, comparison cost, select, +// new wire, fixed order, both hit tests, spring as a matrix power +// src/runtime/cells.ts:27-32 what a cell kind means +// docs/expressions.md:11 select: both sides always compute +// docs/execution-model.md:173, 178, 243 host writes the matrix; 14 operations in 3 layers; bound 10.36 +// docs/patterns.md:198-211 a linear step repeated a runtime number of times +// src/runtime/host-timing.ts:154-163 springStepMatrix +// template/toggle.test.ts:9, 22 track geometry at 1280 × 800, the scripted press at (640, 400) +// +// Authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan; captions are the +// fixed narration contract (verbatim). +// +// Every spring number on screen is DERIVED at module scope from springStepMatrix's formula +// (host-timing.ts:160-162) with stiffness 650, damping 45 and a 4 ms step, and asserted against the +// figures the repo prints (bound 10.36, peak 207 px/s, 0.009 px overshoot). Nothing here is runtime output. +import type { ReactNode } from 'react'; +import { interpolateRgb, scaleLinear } from 'd3'; +import { CAMERA_HOME, Camera, MathLabel, STAGE_H, STAGE_W, Timeline, cameraInterp, colors, ease } from '../../core'; +import type { CameraState, ChannelRef, SceneState } from '../../core'; +import { Axes, FunctionPlot, MatrixGrid, Vec } from '../../primitives'; + +const clamp01 = (u: number): number => (u < 0 ? 0 : u > 1 ? 1 : u); +const lerp = (a: number, b: number, u: number): number => a + (b - a) * u; +const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace'; +const SANS = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'; +const minus = (s: string): string => s.replace(/-/g, '−'); + +// --------------------------------------------------------------------------- +// The spring, derived (host-timing.ts:154-163, toggle.ts:10, host-timing.ts:37) +// --------------------------------------------------------------------------- + +type Mat = readonly [number, number, number, number]; +const SPRING = { stiffness: 650, damping: 45 }; +const STEP_MS = 4; +function springStepMatrix(): Mat { + const dt = STEP_MS / 1000; + const velocityKeep = 1 - SPRING.damping * dt; + return [1 - SPRING.stiffness * dt * dt, dt * velocityKeep, -SPRING.stiffness * dt, velocityKeep]; +} +const mul = (a: Mat, b: Mat): Mat => [ + a[0] * b[0] + a[1] * b[2], + a[0] * b[1] + a[1] * b[3], + a[2] * b[0] + a[3] * b[2], + a[2] * b[1] + a[3] * b[3], +]; +const apply = (m: Mat, d: number, v: number): [number, number] => [m[0] * d + m[1] * v, m[2] * d + m[3] * v]; + +const M1 = springStepMatrix(); +const POWERS: Mat[] = [M1]; +for (let i = 0; i < 4; i++) POWERS.push(mul(POWERS[i], POWERS[i])); +const POWER_N = [1, 2, 4, 8, 16]; +const M16 = POWERS[4]; + +// the phase-portrait path: released 20 px from the destination, 16 steps of 4 ms +const PATH: Array<[number, number]> = [[-20, 0]]; +for (let i = 0; i < 16; i++) PATH.push(apply(M1, PATH[i][0], PATH[i][1])); +const JUMP = apply(M16, -20, 0); + +// largest |entry| of M^n, n = 0..300 (springMatrixBound) +let BOUND = 1; +{ + let p: Mat = [1, 0, 0, 1]; + for (let n = 1; n <= 300; n++) { + p = mul(p, M1); + BOUND = Math.max(BOUND, ...p.map(Math.abs)); + } +} +// the thumb's displacement for 300 steps after a flip (drives the world toggle) +const THUMB_D: number[] = [-20]; +let PEAK_V = 0; +let OVERSHOOT = 0; +{ + let d = -20; + let v = 0; + for (let n = 0; n < 300; n++) { + [d, v] = apply(M1, d, v); + THUMB_D.push(d); + PEAK_V = Math.max(PEAK_V, Math.abs(v)); + OVERSHOOT = Math.max(OVERSHOOT, d); + } +} +function check(ok: boolean, what: string): void { + if (!ok) throw new Error(`handwired chapter 2: derived spring number disagrees with the repo (${what})`); +} +const near = (a: number, b: number, tol: number): boolean => Math.abs(a - b) <= tol; +check([0.9896, 0.00328, -2.6, 0.82].every((v, i) => near(M1[i], v, 1e-5)), 'M'); +check([0.4487, 0.011, -8.753, -0.1223].every((v, i) => near(M16[i], v, 5e-4)), 'M^16'); +check(near(BOUND, 10.36, 0.005), 'matrix bound 10.36'); +check(near(PATH[16][0], -8.974, 1e-3) && near(PATH[16][1], 175.059, 1e-3), '16-step state'); +check(near(JUMP[0], PATH[16][0], 1e-9) && near(JUMP[1], PATH[16][1], 1e-9), 'M^16 equals 16 steps'); +check(near(PEAK_V, 207, 0.5) && near(OVERSHOOT, 0.009, 0.001), 'peak 207 px/s, 0.009 px overshoot'); + +/** Thumb offset 0..20 px from the off side, `t` seconds after a flip (4 ms steps). */ +function thumbOffset(t: number): number { + const s = Math.max(0, Math.min(300, t / (STEP_MS / 1000))); + const i = Math.min(299, Math.floor(s)); + return 20 + lerp(THUMB_D[i], THUMB_D[i + 1], s - i); +} + +// --------------------------------------------------------------------------- +// Layout (stage 1280×720; the caption band y ≥ 630 stays empty) +// --------------------------------------------------------------------------- + +// world panel: the toggle at 3× (track 52×32 → 156×96, inset 4 → 12, travel 20 → 60) +const TRACK = { x: 70, y: 190, w: 156, h: 96 }; +const THUMB_R = 36; +const THUMB_X0 = TRACK.x + 12 + THUMB_R; // 118 +const THUMB_Y = TRACK.y + TRACK.h / 2; // 238 +const PTR = { x: TRACK.x + 26 * 3, y: TRACK.y + 16 * 3 }; // (640, 400) inside 614..666 × 384..416 +const XR = { y: 330, x0: TRACK.x, x1: TRACK.x + TRACK.w }; // x-ruler 614 … 666 +const YR = { x: 300, y0: TRACK.y, y1: TRACK.y + TRACK.h }; // y-ruler 384 … 416 +const DISC_Y = 470; + +// the tape +const TAPE_X = 385; +const CELL_W = 38; +const CELL_H = 30; +const PITCH = 41; +const CELL_Y = 110; +const cellX = (i: number): number => TAPE_X + i * PITCH; +const cellCx = (i: number): number => cellX(i) + CELL_W / 2; +const ROW_Y = (i: number): number => 206 + i * 28; +const ROW_X = 385; +const ROW_W = 345; +const BIT_X = 712; + +type Kind = 'input' | 'event' | 'state'; +const KIND_COLOR: Record = { input: colors.ACCENT, event: colors.WARM, state: colors.POSITIVE }; +const CELLS: Array<{ name: string; kind: Kind }> = [ + { name: 'viewportWidth', kind: 'input' }, + { name: 'viewportHeight', kind: 'input' }, + { name: 'down.active', kind: 'event' }, + { name: 'down.x', kind: 'input' }, + { name: 'down.y', kind: 'input' }, + { name: 'up.active', kind: 'event' }, + { name: 'up.x', kind: 'input' }, + { name: 'up.y', kind: 'input' }, + { name: 'cancel', kind: 'event' }, + { name: 'on', kind: 'state' }, + { name: 'armed', kind: 'state' }, + { name: 'springMatrix[0]', kind: 'input' }, + { name: 'springMatrix[1]', kind: 'input' }, + { name: 'springMatrix[2]', kind: 'input' }, + { name: 'springMatrix[3]', kind: 'input' }, + { name: 'settleCheck', kind: 'input' }, + { name: 'snap', kind: 'input' }, + { name: 'thumb.displacement', kind: 'state' }, + { name: 'thumb.velocity', kind: 'state' }, + { name: 'thumb.destination', kind: 'state' }, +]; +const GROUPS: Array<{ name: string; from: number; to: number; low?: boolean }> = [ + { name: 'viewport', from: 0, to: 1 }, + { name: 'down', from: 2, to: 4 }, + { name: 'up', from: 5, to: 7 }, + { name: 'cancel', from: 8, to: 8 }, + { name: 'on', from: 9, to: 9 }, + { name: 'armed', from: 10, to: 10 }, + { name: 'springMatrix', from: 11, to: 14 }, + { name: 'settleCheck', from: 15, to: 15, low: true }, + { name: 'snap', from: 16, to: 16 }, + { name: 'thumb', from: 17, to: 19 }, +]; +const I_ON = 9; +const I_ARMED = 10; + +// gadget area (right of the rows) +const G = { x: 760, r: 1225 }; +// the relu plot shared by B4 (four bits) and B8 (three bits): x = sum of bits − (n − 1) +const RX = scaleLinear([-3, 1.5], [915, 1200]); +const RY = scaleLinear([0, 1.5], [430, 310]); +const RELU = (x: number): number => Math.max(0, x); +const SUM = { x: 800, y: 280 }; + +// spring panel +const PX = scaleLinear([-21, -7], [410, 790]); +const PY = scaleLinear([0, 230], [590, 240]); +const MAT = { x: 905, y: 310, cell: 100, gap: 10 }; + +// closing composition +const CLOSE = { x: 420, y: 360, k: 1.6 }; + +const CAM_TAPE: CameraState = { x: 795, y: 126, k: 1.35 }; +const CAM_PRESS: CameraState = { x: 575, y: 300, k: 1.15 }; // world panel's left edge stays on stage +const CAM_AND: CameraState = { x: 760, y: 330, k: 1.15 }; +const CAM_SELECT: CameraState = { x: 990, y: 340, k: 1.4 }; + +const PIN = 'chenglou/handwired@4ea7fcb'; +const SOURCES = [ + 'docs/tutorial.md:3', + 'docs/tutorial.md:3', + 'template/toggle.ts:18-38 · docs/tutorial.md:145', + 'src/runtime/cells.ts:27-32', + 'template/toggle.ts:46-58 · docs/tutorial.md:53 · template/toggle.test.ts:9, 22', + 'template/toggle.ts:48-49', + 'docs/expressions.md:11 · docs/tutorial.md:93', + 'docs/tutorial.md:93 · template/toggle.ts:61', + 'template/toggle.ts:62-63', + 'template/toggle.ts:62-63', + 'template/toggle.ts:64-65', + 'docs/tutorial.md:106 · numbers derived from src/runtime/host-timing.ts:160-162', + 'docs/tutorial.md:106 · numbers derived from src/runtime/host-timing.ts:160-162', + 'docs/execution-model.md:173, 178', + 'docs/tutorial.md:93', +]; + +// code chip bank (verbatim excerpts; long lines wrapped, ≤ 3 lines) +type CodeLine = { text: string; prose?: boolean }; +const CODE: Array<{ lines: CodeLine[]; color: string }> = [ + { color: colors.ACCENT, lines: [{ text: 'const trackLeft = circuit.affine([[cells.viewportWidth, 0.5]], -layout.trackWidth / 2)' }] }, // toggle.ts:46 + { color: colors.WARM, lines: [{ text: "emitCompare(circuit, point.x, '>=', trackLeft)," }] }, // toggle.ts:54 + { + color: colors.WARM, + lines: [ + { text: '// JS: bits.every(bit => bit === 1), as relu(sum of bits - (count - 1)).' }, + { text: 'const all = (...bits: Wire[]) => circuit.reglu([], 1,' }, + { text: ' bits.map(bit => [bit, 1] as const), 1 - bits.length)' }, + ], + }, // toggle.ts:48-49 + { + color: colors.POSITIVE, + lines: [{ text: 'emitSelect(circuit, condition, off, on)' }, { text: 'computes both sides and picks one', prose: true }], + }, // tutorial.md:93 + { + color: colors.POSITIVE, + lines: [ + { text: 'const armedAfterDown = emitSelect(circuit, cells.down.active, cells.armed,' }, + { text: ' insideTrack(cells.down))' }, + ], + }, // toggle.ts:61 + { + color: colors.POSITIVE, + lines: [ + { text: '// JS: if (up && armed && insideTrack(up)) on = !on' }, + { text: 'const on = emitSelect(circuit, all(cells.up.active, armedAfterDown, insideTrack(cells.up)),' }, + { text: ' cells.on, not(cells.on))' }, + ], + }, // toggle.ts:62-63 + { + color: colors.WARM, + lines: [ + { text: '// JS: if (up || cancel) armed = false' }, + { text: 'const armed = all(armedAfterDown, not(cells.up.active), not(cells.cancel))' }, + ], + }, // toggle.ts:64-65 +]; + +// --------------------------------------------------------------------------- +// Timeline — 112.6 s, fifteen fixed captions +// --------------------------------------------------------------------------- + +export function buildScene() { + const tl = new Timeline(); + const ch = (key: string, v = 0): ChannelRef => tl.channel(key, v); + const cam = tl.channel('cam', CAMERA_HOME, cameraInterp); + const src = ch('src'); + const codeIdx = ch('codeIdx', -1); + const codeU = ch('codeU'); + + // world panel + const worldOp = ch('worldOp', 1); + const toggleU = ch('toggleU'); + const ptrU = ch('ptrU'); + const ptrPulse = ch('ptrPulse'); + const wArmed = ch('wArmed'); + const wOn = ch('wOn'); + const thumbT = ch('thumbT'); + const thumbHome = ch('thumbHome'); // 1 = thumb pulled back to the off side (the replay) + const stampA = ch('stampA'); + const stampB = ch('stampB'); + const rulerU = ch('rulerU'); + const rulerOp = ch('rulerOp', 1); + + // tape + const cellsU = ch('cellsU'); + const bracketU = ch('bracketU'); + const counterU = ch('counterU'); + const legendU = ch('legendU'); + const spot = ch('spot', -1); + const spotU = ch('spotU'); + const arrA = ch('arrA'); + const arrB1 = ch('arrB1'); + const arrB2 = ch('arrB2'); + const arcC = ch('arcC'); + const vwFlash = ch('vwFlash'); + const dActive = ch('dActive'); + const dXY = ch('dXY'); + const uActive = ch('uActive'); + const uXY = ch('uXY'); + const mWritten = ch('mWritten'); + const tapeOp = ch('tapeOp', 1); + + // derived-wire rows + const rows = Array.from({ length: 12 }, (_, i) => ch(`row${i}`)); + const rowDim = Array.from({ length: 12 }, (_, i) => ch(`rowDim${i}`)); + const bits = [2, 3, 4, 5].map((i) => ch(`bit${i}`)); + const opsTag = ch('opsTag'); + const gridNote = ch('gridNote'); // B3: the grid precondition behind the 2-operation cost + const rowsOut = ch('rowsOut'); + const rowsWhisper = ch('rowsWhisper'); // B5: rows fade out behind the select (the push would cut them at the left edge) + + // B4 / B8: and as relu + const flyU = ch('flyU'); + const sumU = ch('sumU'); + const allTex = ch('allTex'); + const eq4 = ch('eq4'); + const plotU = ch('plotU'); + const plotOp = ch('plotOp', 1); + const markU = ch('markU'); + const ghostU = ch('ghostU'); + const andOut = ch('andOut'); // B4 sum / equations fade out + const b8U = ch('b8U'); + const markX = ch('markX', 1); + + // B5–B7: the select gadget + const selU = ch('selU'); // 0..3: formula+inputs, tiles, sum + const selB = ch('selB'); + const selOp = ch('selOp', 1); + const flipU = ch('flipU'); + const offSpot = ch('offSpot'); + const curveU = ch('curveU'); + const tag1 = ch('tag1'); + const tag2 = ch('tag2'); + const gateTex = ch('gateTex'); + + // B9–B10: the spring + const springU = ch('springU'); + const startU = ch('startU'); + const hop = ch('hop'); + const trailN = ch('trailN'); + const trailOp = ch('trailOp', 1); + const matU = ch('matU'); + const sq = ch('sq'); + const eqM = ch('eqM'); + const jumpU = ch('jumpU'); + const writeU = ch('writeU'); + const opsU = ch('opsU'); + const springOp = ch('springOp', 1); + + // B11 + const closeU = ch('closeU'); + const listU = ch('listU'); + const ev = [1, 2, 3].map((i) => ch(`ev${i}`)); + + const CAP = [0.2, 1.7, 9.5, 17.9, 26.3, 36.4, 45.1, 54.2, 64.1, 66.0, 74.0, 81.6, 85.3, 92.6, 101.0]; + CAP.forEach((at, i) => tl.set(src, i, Math.max(0, at - 0.1))); + + const showCode = (i: number, at: number): void => { + tl.tween(codeU, 0, { at: at - 0.3, dur: 0.25, ease: ease.move }); + tl.set(codeIdx, i, at - 0.03); + tl.tween(codeU, 1, { at, dur: 0.45, ease: ease.enter }); + }; + const hideCode = (at: number): void => { + tl.tween(codeU, 0, { at, dur: 0.35, ease: ease.move }); + }; + const fadeIn = (c: ChannelRef, at: number, dur = 0.6): number => tl.tween(c, 1, { at, dur, ease: ease.enter }); + const to = (c: ChannelRef, v: number, at: number, dur = 0.6, e = ease.move): number => tl.tween(c, v, { at, dur, ease: e }); + + // — B0 · a toggle has memory — + tl.caption({ at: 0.2, dur: 1.5, text: 'A toggle has memory.' }); + tl.caption({ at: 1.7, dur: 7.8, text: 'A press on the track arms it, a release on the track while armed flips it, and the thumb springs across.' }); + tl.tween(toggleU, 1, { at: 0.5, dur: 1.0, ease: ease.draw }); + fadeIn(ptrU, 1.9, 0.3); + tl.tween(ptrPulse, 1, { at: 2.1, dur: 0.6, ease: ease.enter }); + fadeIn(wArmed, 2.3, 0.4); + fadeIn(stampA, 2.6, 0.5); + tl.set(ptrPulse, 0, 4.4); + tl.tween(ptrPulse, 1, { at: 4.5, dur: 0.6, ease: ease.enter }); + fadeIn(wOn, 4.7, 0.4); + tl.tween(thumbT, 1.2, { at: 4.7, dur: 1.2, ease: ease.linear }); + to(wArmed, 0, 4.9, 0.4); + fadeIn(stampB, 5.0, 0.5); + to(ptrU, 0, 6.6, 0.5); + + // — B1 · one table of twenty cells — + tl.caption({ at: 9.5, dur: 8, text: 'The program reads one table of twenty cells: viewport, press, release, cancel, the on bit, the armed bit, and the spring.' }); + tl.tween(cam, CAM_TAPE, { at: 9.5, dur: 1.3, ease: ease.move }); + to(worldOp, 0, 9.5, 0.8); // the zoomed-in tape pushes the panel past the left edge + to(stampA, 0, 9.5, 0.5); + to(stampB, 0, 9.5, 0.5); + tl.tween(cellsU, 20, { at: 10.2, dur: 2.6, ease: ease.linear }); + tl.tween(bracketU, 1, { at: 12.6, dur: 1.0, ease: ease.draw }); + fadeIn(counterU, 14.0, 0.5); + // the replay: the switch goes back to off while it is dimmed + to(wOn, 0, 11.0, 0.8); + to(thumbHome, 1, 11.0, 0.9); + + // — B2 · who writes each cell — + tl.caption({ at: 17.9, dur: 8, text: 'Each cell says who writes it. Javascript writes inputs and raises events, the program resets events, and the program computes states.' }); + fadeIn(legendU, 18.0, 0.5); + tl.set(spot, 0, 18.1); + fadeIn(spotU, 18.1, 0.4); + tl.tween(arrA, 1, { at: 18.4, dur: 0.6, ease: ease.move }); + tl.tween(vwFlash, 1, { at: 18.9, dur: 0.5, ease: ease.pop }); + to(arrA, 0, 20.3, 0.3); + to(vwFlash, 0, 20.3, 0.3); + tl.set(spot, 2, 20.6); + tl.tween(arrB1, 1, { at: 20.7, dur: 0.5, ease: ease.move }); + tl.set(dActive, 1, 21.15); + to(arrB1, 0, 21.8, 0.3); + tl.tween(arrB2, 1, { at: 22.0, dur: 0.5, ease: ease.move }); + tl.set(dActive, 0, 22.45); + to(arrB2, 0, 22.9, 0.3); + tl.set(spot, I_ON, 23.2); + tl.tween(arcC, 1, { at: 23.3, dur: 1.1, ease: ease.draw }); + to(arcC, 0, 25.6, 0.4); + to(spotU, 0, 25.6, 0.4); + tl.set(spot, -1, 26.0); + to(legendU, 0, 26.0, 0.4); + to(counterU, 0, 26.0, 0.4); + + // — B3 · four comparisons — + tl.caption({ at: 26.3, dur: 9.9, text: 'Did the press land on the track? Four comparisons, one per edge. With coordinates on a fine grid, each is an exact bit of just two operations.' }); + tl.tween(cam, CAM_PRESS, { at: 26.3, dur: 1.2, ease: ease.move }); + to(worldOp, 1, 26.6, 0.6); // after the push has carried the panel back on stage + fadeIn(ptrU, 26.7, 0.3); + tl.set(ptrPulse, 0, 26.6); + tl.tween(ptrPulse, 1, { at: 26.9, dur: 0.6, ease: ease.enter }); + tl.tween(rulerU, 1, { at: 27.0, dur: 0.9, ease: ease.draw }); + tl.set(dActive, 1, 27.4); + fadeIn(dXY, 27.4, 0.4); + showCode(0, 27.7); + fadeIn(rows[0], 27.7, 0.5); + fadeIn(rows[1], 27.9, 0.5); + showCode(1, 28.9); + [2, 3, 4, 5].forEach((r, k) => { + const at = 28.7 + k * 0.95; + fadeIn(rows[r], at, 0.5); + tl.tween(bits[k], 1, { at: at + 0.45, dur: 0.4, ease: ease.pop }); + }); + fadeIn(opsTag, 29.3, 0.5); + fadeIn(gridNote, 30.7, 0.6); // "with coordinates on a fine grid" (tutorial.md:53) + + // — B4 · and as relu — + tl.caption({ at: 36.4, dur: 8.3, text: 'Combine the four bits with an and: add them, subtract three, take relu. Only when all four are one does anything survive.' }); + tl.tween(cam, CAM_AND, { at: 36.4, dur: 1.2, ease: ease.move }); + to(worldOp, 0, 36.4, 0.6); // the and push leaves the panel cut by the left edge + to(opsTag, 0, 36.4, 0.4); + to(gridNote, 0, 36.4, 0.4); + to(tapeOp, 0.35, 36.4, 0.6); + to(ptrU, 0, 36.4, 0.4); + [0, 1, 2, 3, 4, 5].forEach((r) => to(rowDim[r], 1, 36.5, 0.6)); + tl.tween(flyU, 1, { at: 36.9, dur: 1.2, ease: ease.move }); + tl.tween(sumU, 1, { at: 37.9, dur: 0.5, ease: ease.pop }); + fadeIn(eq4, 38.4, 0.5); + showCode(2, 38.7); + fadeIn(allTex, 38.8, 0.6); + tl.tween(plotU, 1, { at: 39.3, dur: 1.2, ease: ease.draw }); + tl.tween(markU, 1, { at: 40.6, dur: 0.5, ease: ease.pop }); + fadeIn(rows[6], 41.1, 0.5); + fadeIn(ghostU, 42.1, 0.7); + + // — B5 · the select — + tl.caption({ at: 45.1, dur: 8.7, text: 'An if becomes a select: compute both values, multiply one by the gate and the other by one minus the gate, and add.' }); + tl.tween(cam, CAM_SELECT, { at: 45.1, dur: 1.3, ease: ease.move }); + to(andOut, 1, 45.1, 0.5); + to(plotOp, 0, 45.1, 0.5); + to(tapeOp, 0, 45.1, 0.6); // the zoomed tape would sit under the header chips + to(rowsWhisper, 1, 45.1, 0.6); + to(rowDim[6], 1, 45.1, 0.6); + showCode(3, 45.7); + tl.tween(selU, 1, { at: 45.7, dur: 1.0, ease: ease.enter }); + tl.tween(selU, 2, { at: 46.9, dur: 0.8, ease: ease.enter }); + tl.tween(selU, 3, { at: 47.9, dur: 0.6, ease: ease.pop }); + tl.tween(selB, 1, { at: 49.1, dur: 1.4, ease: ease.enter }); + fadeIn(rows[7], 52.1, 0.5); + fadeIn(wArmed, 52.1, 0.5); + + // — B6 · a new wire — + tl.caption({ at: 54.2, dur: 9.5, text: "Where Javascript reassigns a variable, builder code makes a new wire. Armed after down holds the press's result; the armed cell keeps the old value." }); + tl.tween(cam, CAMERA_HOME, { at: 54.2, dur: 1.3, ease: ease.move }); + to(selB, 0, 54.2, 0.5); + to(tapeOp, 1, 54.8, 0.6); // once the camera is nearly home: no half-lit rows at the edge + to(rowsWhisper, 0, 54.8, 0.6); + to(worldOp, 1, 54.2, 0.6); + showCode(4, 54.5); + to(offSpot, 1, 54.7, 0.6); + tl.tween(curveU, 1, { at: 55.3, dur: 1.2, ease: ease.draw }); + fadeIn(tag1, 56.5, 0.5); + fadeIn(tag2, 58.1, 0.5); + + // — B7 · the flip — + tl.caption({ at: 64.1, dur: 1.9, text: 'The flip is another select.' }); + tl.caption({ at: 66.0, dur: 7.6, text: 'If a release lands on the track while armed, the next on bit is the opposite of the old one.' }); + to(curveU, 0, 64.1, 0.4); + to(tag1, 0, 64.1, 0.4); + to(tag2, 0, 64.1, 0.4); + to(offSpot, 0, 64.1, 0.5); + [7].forEach((r) => to(rowDim[r], 1, 64.2, 0.6)); + showCode(5, 64.4); + fadeIn(ptrU, 64.5, 0.3); + tl.set(ptrPulse, 0, 64.4); + tl.tween(ptrPulse, 1, { at: 64.7, dur: 0.6, ease: ease.enter }); + tl.set(uActive, 1, 64.9); + fadeIn(uXY, 64.9, 0.4); + fadeIn(rows[8], 65.5, 0.5); + fadeIn(rows[9], 66.7, 0.5); + fadeIn(gateTex, 66.9, 0.6); + tl.tween(flipU, 1, { at: 67.9, dur: 0.9, ease: ease.move }); + fadeIn(rows[10], 69.9, 0.5); + fadeIn(wOn, 70.3, 0.4); + to(thumbHome, 0, 70.25, 0.05); + tl.set(thumbT, 0, 70.2); + tl.tween(thumbT, 1.2, { at: 70.3, dur: 1.2, ease: ease.linear }); + to(ptrU, 0, 72.1, 0.5); + + // — B8 · disarm — + tl.caption({ at: 74.0, dur: 7.2, text: 'A release or a cancel disarms the switch, so the next armed bit is an and of three bits.' }); + to(selOp, 0, 74.0, 0.6); + to(gateTex, 0, 74.0, 0.5); + [8, 9, 10].forEach((r) => to(rowDim[r], 1, 74.1, 0.6)); + showCode(6, 74.3); + fadeIn(rows[11], 74.5, 0.5); + fadeIn(b8U, 75.1, 0.6); + to(plotOp, 1, 75.7, 0.6); + tl.set(markX, 0, 75.5); + to(wArmed, 0, 77.5, 0.5); + + // — B9 · the spring is a matrix power — + tl.caption({ at: 81.6, dur: 3.7, text: 'The spring looks like a loop of four-millisecond steps.' }); + tl.caption({ at: 85.3, dur: 6.9, text: 'But one step is a two by two matrix, and n steps are that matrix to the power n.' }); + hideCode(81.6); + tl.tween(rowsOut, 1, { at: 81.6, dur: 0.9, ease: ease.move }); + to(plotOp, 0, 81.6, 0.5); + to(b8U, 0, 81.6, 0.5); + to(worldOp, 0.35, 81.6, 0.6); + tl.tween(springU, 1, { at: 82.5, dur: 1.2, ease: ease.draw }); + fadeIn(startU, 83.5, 0.4); + tl.tween(hop, 16, { at: 84.1, dur: 1.28, ease: ease.linear }); + tl.tween(trailN, 16, { at: 84.1, dur: 1.28, ease: ease.linear }); + fadeIn(eqM, 85.7, 0.6); + fadeIn(matU, 85.9, 0.7); + [0, 1, 2, 3].forEach((k) => tl.tween(sq, k + 1, { at: 87.1 + k * 1.15, dur: 0.9, ease: ease.move })); + + // — B10 · the host writes the power — + tl.caption({ at: 92.6, dur: 8, text: "Javascript counts the steps and writes that power's four numbers into four cells. The program applies them once, in three layers." }); + tl.set(hop, 0, 92.7); + to(eqM, 0, 92.6, 0.5); + to(trailOp, 0.3, 92.7, 0.5); + tl.tween(jumpU, 1, { at: 93.2, dur: 1.2, ease: ease.move }); + tl.tween(writeU, 1, { at: 95.5, dur: 0.8, ease: ease.draw }); + tl.tween(mWritten, 1, { at: 96.2, dur: 0.6, ease: ease.pop }); + tl.tween(opsU, 1, { at: 97.5, dur: 0.9, ease: ease.enter }); + + // — B11 · one fixed list — + tl.caption({ at: 101.0, dur: 9.1, text: 'So the switch is one fixed list: two hit tests, two selects, a spring. Both hit tests run every evaluation, press or no press.' }); + to(springOp, 0, 101.0, 0.9); + to(tapeOp, 0.12, 101.0, 0.9); + to(worldOp, 0.12, 101.0, 0.9); + to(rulerOp, 0, 101.0, 0.9); + tl.tween(closeU, 1, { at: 101.3, dur: 1.4, ease: ease.move }); + tl.tween(listU, 3, { at: 102.8, dur: 1.2, ease: ease.linear }); + ev.forEach((e, k) => tl.tween(e, 1, { at: 104.5 + k * 1.6, dur: 0.8, ease: ease.linear })); + tl.hold(110.1, 2.5); + + return { + tl, cam, src, codeIdx, codeU, + worldOp, toggleU, ptrU, ptrPulse, wArmed, wOn, thumbT, thumbHome, stampA, stampB, rulerU, rulerOp, + cellsU, bracketU, counterU, legendU, spot, spotU, arrA, arrB1, arrB2, arcC, vwFlash, dActive, dXY, uActive, uXY, mWritten, tapeOp, + rows, rowDim, bits, opsTag, gridNote, rowsOut, rowsWhisper, + flyU, sumU, allTex, eq4, plotU, plotOp, markU, ghostU, andOut, b8U, markX, + selU, selB, selOp, flipU, offSpot, curveU, tag1, tag2, gateTex, + springU, startU, hop, trailN, trailOp, matU, sq, eqM, jumpU, writeU, opsU, springOp, + closeU, listU, ev, + }; +} + +const scene = buildScene(); + +// --------------------------------------------------------------------------- +// Glyph kit (redrawn locally; book-wide spec) +// --------------------------------------------------------------------------- + +/** The bit: amber disc when 1, hollow ring when 0 (the fill fades through transitions). */ +function BitDisc({ x, y, v, r = 11, color = colors.WARM, opacity = 1, showNum = true }: { x: number; y: number; v: number; r?: number; color?: string; opacity?: number; showNum?: boolean }) { + if (opacity <= 0.005) return null; + return ( + + + + {showNum && r >= 11 && ( + = 0.5 ? '#08101f' : color}> + {v >= 0.5 ? '1' : '0'} + + )} + + ); +} + +/** Violet hockey stick, 22×16: flat to the middle, then a 45° rise. */ +function ReluGlyph({ x, y, opacity = 1 }: { x: number; y: number; opacity?: number }) { + return ( + + + + + ); +} + +function Pill({ x, y, w, h = 26, text, color = colors.GRID, textColor = colors.TEXT, fontSize = 13, dashed = false, opacity = 1, fill = colors.PANEL, anchor = 'start' }: { + x: number; y: number; w: number; h?: number; text: ReactNode; color?: string; textColor?: string; fontSize?: number; dashed?: boolean; opacity?: number; fill?: string; anchor?: 'start' | 'middle'; +}) { + if (opacity <= 0.005) return null; + return ( + + + + {text} + + + ); +} + +const fmtCell = (v: number): string => { + if (Math.abs(v) < 1) return minus((v < 0 ? '-' : '') + Math.abs(v).toFixed(3).slice(1)); + return minus(v.toFixed(2)); +}; +const fmt4 = (v: number): string => minus(v.toFixed(4)); +const fmt2 = (v: number): string => minus(v.toFixed(2)); + +/** Up to three lines of verbatim code, screen space, above the caption band. */ +function CodeChip({ idx, u }: { idx: number; u: number }) { + if (idx < 0 || u <= 0.01) return null; + const c = CODE[idx]; + const maxLen = Math.max(...c.lines.map((l) => l.text.length)); + const w = Math.min(870, maxLen * 8.43 + 30); + const h = c.lines.length * 19 + 14; + const x = 370; + const y = 622 - h; + return ( + + + + {c.lines.map((l, i) => ( + + {l.text} + + ))} + + ); +} + +/** The switch itself (world panel), drawn at 3×. */ +function Toggle({ on, armed, offset, draw }: { on: number; armed: number; offset: number; draw: number }) { + if (draw <= 0.01) return null; + const fill = interpolateRgb('#1b2436', colors.POSITIVE)(clamp01(on)); + const r = TRACK.h / 2; + return ( + + {armed > 0.01 && ( + + )} + + + + ); +} + +/** Select gadget: gate·on + (1 − gate)·off, both products always computed. */ +function SelectGadget({ labels, altLabels, swap = 0, u, offSpot, opacity }: { labels: [string, string, string]; altLabels: [string, string, string]; swap?: number; u: number; offSpot: number; opacity: number }) { + if (opacity <= 0.01 || u <= 0.01) return null; + const inU = clamp01(u); + const tileU = clamp01(u - 1); + const sumUu = clamp01(u - 2); + const others = 1 - 0.65 * offSpot; + const inY = [250, 285, 320]; + const inCol = [colors.WARM, colors.MUTED, colors.MUTED]; + const vals = [1, 0, 1]; + const tiles = [ + { y: 252, text: 'gate × on', v: 1 }, + { y: 302, text: '(1 − gate) × off', v: 0 }, + ]; + return ( + + {/* wires: gate → both tiles, on → tile 1, off → tile 2 */} + + + + + + + + 0.5 ? colors.WARM : colors.MUTED} strokeWidth={offSpot > 0.5 ? 2 : 1.4} fill="none" opacity={tileU * 0.8} /> + {labels.map((l, i) => ( + + 0.5 ? colors.WARM : inCol[i]} /> + {/* the flip reuses the same gadget: old labels out, then new labels in (never overlapping) */} + + {l} + + + {altLabels[i]} + + + + {vals[i]} + + + ))} + {tiles.map((t, i) => ( + + + + {t.v} + + + ))} + + + + 1 + + + sum + + + + ); +} + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +export function Render({ s }: { s: SceneState }) { + const g = (c: ChannelRef): T => s.get(c); + const cam = g(scene.cam); + const srcIdx = Math.max(0, Math.min(SOURCES.length - 1, Math.round(g(scene.src)))); + const codeIdx = Math.round(g(scene.codeIdx)); + const codeU = g(scene.codeU); + + const worldOp = g(scene.worldOp); + const toggleU = g(scene.toggleU); + const ptrU = g(scene.ptrU); + const ptrPulse = g(scene.ptrPulse); + const wArmed = g(scene.wArmed); + const wOn = g(scene.wOn); + const thumbHome = g(scene.thumbHome); + const offset = thumbOffset(g(scene.thumbT)) * (1 - thumbHome); + const rulerU = g(scene.rulerU); + const rulerOp = g(scene.rulerOp); + + const cellsU = g(scene.cellsU); + const bracketU = g(scene.bracketU); + const spot = Math.round(g(scene.spot)); + const spotU = g(scene.spotU); + const dActive = g(scene.dActive); + const dXY = g(scene.dXY); + const uActive = g(scene.uActive); + const uXY = g(scene.uXY); + const mWritten = g(scene.mWritten); + const tapeOp = g(scene.tapeOp); + const vwFlash = g(scene.vwFlash); + + const rowsOut = g(scene.rowsOut); + const bitsU = scene.bits.map((b) => g(b)); + const flyU = g(scene.flyU); + const andOut = g(scene.andOut); + const plotOp = g(scene.plotOp); + const markX = g(scene.markX); + const offSpot = g(scene.offSpot); + const flipU = g(scene.flipU); + + const springOp = g(scene.springOp); + const springU = g(scene.springU); + const hop = g(scene.hop); + const trailN = g(scene.trailN); + const sq = g(scene.sq); + const closeU = g(scene.closeU); + const listU = g(scene.listU); + const evU = scene.ev.map((e) => g(e)); + + // ---- the matrix on screen (ticks between powers while squaring) ---- + const sqK = Math.min(3, Math.floor(sq)); + const sqF = sq >= 4 ? 1 : sq - sqK; + const matNow = [0, 1, 2, 3].map((i) => lerp(POWERS[sqK][i], POWERS[sqK + 1][i], ease.move(clamp01(sqF)))); + const powerShown = sq >= 4 ? 16 : POWER_N[sqF > 0.5 ? sqK + 1 : sqK]; + const matWrite = M16; + + // ---- tape values ---- + const cellValue = (i: number): string => { + switch (i) { + case 0: return '1280'; + case 1: return '800'; + case 2: return dActive >= 0.5 ? '1' : '0'; + case 3: return dXY > 0.5 ? '640' : '0'; + case 4: return dXY > 0.5 ? '400' : '0'; + case 5: return uActive >= 0.5 ? '1' : '0'; + case 6: return uXY > 0.5 ? '640' : '0'; + case 7: return uXY > 0.5 ? '400' : '0'; + case 11: case 12: case 13: case 14: return mWritten > 0.3 ? fmtCell(matWrite[i - 11]) : '0'; + default: return '0'; + } + }; + const cellLit = (i: number): { u: number; color: string } => { + if (i >= 2 && i <= 4) return { u: dXY * (1 - clamp01(rowsOut * 2)), color: colors.WARM }; + if (i >= 5 && i <= 7) return { u: uXY * (1 - clamp01(rowsOut * 2)), color: colors.WARM }; + if (i >= 11 && i <= 14) return { u: mWritten * springOp, color: colors.TEAL }; + if (i === 0) return { u: vwFlash, color: colors.TEAL }; + return { u: 0, color: colors.GRID }; + }; + + // ---- rows ---- + const rowOp = (i: number): number => { + const u = g(scene.rows[i]); + return u * (1 - 0.65 * g(scene.rowDim[i])) * (1 - rowsOut) * (1 - g(scene.rowsWhisper)); // the select push cuts the rows at the left edge + }; + const rowsShift = -46 * ease.move(rowsOut); + + // anything a camera push carries toward the stage edge fades out before it is cut + const edgeFit = (x0: number, x1: number): number => { + const l = (x0 - cam.x) * cam.k + STAGE_W / 2; + const r = (x1 - cam.x) * cam.k + STAGE_W / 2; + return clamp01((l - 16) / 20) * clamp01((STAGE_W - 16 - r) / 20); + }; + + // ---- phase portrait ---- + const hk = Math.min(15, Math.floor(hop)); + const hf = hop >= 16 ? 1 : hop - hk; + const hopX = PX(lerp(PATH[hk][0], PATH[hk + 1][0], hf)); + const hopY = PY(lerp(PATH[hk][1], PATH[hk + 1][1], hf)) - 9 * Math.sin(Math.PI * hf); + + const chip = `${PIN} · ${SOURCES[srcIdx]}`; + const chipW = chip.length * 7.85 + 20; + + // world toggle placement (B11 moves it to centre-left, larger) + const cu = ease.move(clamp01(closeU)); + const tcx = lerp(PTR.x, CLOSE.x, cu); + const tcy = lerp(THUMB_Y, CLOSE.y, cu); + const tk = lerp(1, CLOSE.k, cu); + const toggleOp = lerp(worldOp, 1, cu); + + const listItems = [ + { text: '2 hit tests', color: colors.WARM }, + { text: '2 selects', color: colors.POSITIVE }, + { text: '1 spring', color: colors.ACCENT }, + ]; + const pulse = evU.reduce((m, p) => Math.max(m, p > 0 && p < 1 ? Math.sin(Math.PI * p) : 0), 0); + + return ( + <> + + + + {/* ================= world panel ================= */} + + + + the switch, at 3× + + {/* rulers (B3): the press against the track edges, toggle.test.ts:9 */} + {rulerU > 0.01 && ( + + + + {[ + { x: XR.x0, lit: bitsU[0], label: '614' }, + { x: XR.x1, lit: bitsU[1], label: '666' }, + ].map((m, i) => ( + + 0.5 ? colors.WARM : colors.MUTED} strokeWidth={m.lit > 0.5 ? 3 : 1.5} /> + 0.5 ? colors.WARM : colors.MUTED} fontSize={13} fontFamily={MONO}> + {m.label} + + + ))} + {[ + { y: YR.y0, lit: bitsU[2], label: '384' }, + { y: YR.y1, lit: bitsU[3], label: '416' }, + ].map((m, i) => ( + + 0.5 ? colors.WARM : colors.MUTED} strokeWidth={m.lit > 0.5 ? 3 : 1.5} /> + 0.5 ? colors.WARM : colors.MUTED} fontSize={13} fontFamily={MONO}> + {m.label} + + + ))} + + + 640 + + + + 400 + + + x + + + y + + + )} + {/* stamps (B0) */} + + + {/* the two state bits the switch shows */} + + + + armed + + + + on + + + + {/* the toggle itself (escapes the panel in B11) */} + + + {ptrU > 0.01 && ( + + + + + )} + + + {/* ================= the tape: 20 source cells ================= */} + + {CELLS.map((c, i) => { + const u = ease.enter(clamp01(cellsU - i)); + if (u <= 0.01) return null; + const spotDim = spotU > 0 && spot !== i ? 1 - 0.65 * spotU : 1; + const lit = cellLit(i); + const x = cellX(i); + const val = cellValue(i); + const fit = edgeFit(x, x + CELL_W); + if (fit <= 0.01) return null; + return ( + + + {lit.u > 0.01 && } + + 4 ? 34 : undefined} lengthAdjust="spacingAndGlyphs"> + {val} + + + ); + })} + {GROUPS.map((gr, k) => { + const u = clamp01(bracketU * 2.2 - k * 0.12); + if (u <= 0.01) return null; + const x0 = cellX(gr.from) + 1; + const x1 = cellX(gr.to) + CELL_W - 1; + const spotDim = spotU > 0 && !(spot >= gr.from && spot <= gr.to) ? 1 - 0.65 * spotU : 1; + const fit = edgeFit(x0, x1); + if (fit <= 0.01) return null; + return ( + + {gr.to > gr.from && } + + {gr.name} + + + ); + })} + {/* counter + kind legend */} + + 20 cells + + + {(['input', 'event', 'state'] as Kind[]).map((k, i) => ( + + + + {k} + + + ))} + + {/* B2 (a): JavaScript writes an input */} + + + JavaScript writes + + {/* B2 (b): JavaScript raises an event, the program resets it */} + + + JavaScript raises + + + + program resets + + {/* spotlit cell name */} + {spotU > 0.01 && spot >= 0 && spot !== I_ON && ( + + {CELLS[spot].name} + + )} + {/* B2 (c): the program computes the next state (looped above the cell, clear of the group labels) */} + {g(scene.arcC) > 0.01 && ( + + + + + program computes next + + + )} + + + {/* ================= derived-wire rows ================= */} + + {/* rows 0–1: the edge wires (small affine operands kept as their own operations) */} + {[ + [{ n: 'trackLeft', v: '614' }, { n: 'trackTop', v: '384' }], + [{ n: 'trackLeft + 52', v: '666' }, { n: 'trackTop + 32', v: '416' }], + ].map((chips, r) => { + const op = rowOp(r); + if (op <= 0.01) return null; + let x = ROW_X + 10; + return ( + + {chips.map((c) => { + const w = (c.n.length + c.v.length + 1) * 7.85 + 18; + const el = ( + + + + {c.n} {c.v} + + + ); + x += w + 10; + return el; + })} + + ); + })} + {/* rows 2–5: four comparisons, two operations each */} + {['x >= trackLeft', 'x < trackLeft + 52', 'y >= trackTop', 'y < trackTop + 32'].map((label, k) => { + const r = k + 2; + const op = rowOp(r); + if (op <= 0.01) return null; + const y = ROW_Y(r); + const flyAway = clamp01(flyU * 1.6 - k * 0.2); + return ( + + + + {label} + + + + + + + 2 + + + + ); + })} + {g(scene.opsTag) > 0.01 && ( + + 2 operations + + )} + {/* B3: why each comparison is 2 operations (tutorial.md:53) */} + {g(scene.gridNote) > 0.01 && ( + + + + pointer on a 1/64 px grid · track edges on a 1/2 px grid → 2 operations each + + + )} + {/* rows 6–11 */} + {[ + { r: 6, name: 'insideTrack(down)', expr: 'all of 4 bits', v: 1 }, + { r: 7, name: 'armedAfterDown', expr: 'select', v: 1 }, + { r: 8, name: 'insideTrack(up)', expr: '', v: 1, mini: true }, + { r: 9, name: 'gate', expr: 'all of 3 bits', v: 1 }, + { r: 10, name: 'on', expr: 'select · next on', v: 1 }, + { r: 11, name: 'armed', expr: 'all of 3 bits · next armed', v: 0 }, + ].map((row) => { + const op = rowOp(row.r); + if (op <= 0.01) return null; + const y = ROW_Y(row.r); + const isState = row.r === 10 || row.r === 11; + return ( + + + + {row.name} + + {row.expr && ( + + {row.expr} + + )} + {row.mini && [0, 1, 2, 3].map((j) => )} + + + ); + })} + + + {/* ================= B4: and = relu(sum − (n − 1)) ================= */} + {flyU > 0.01 && flyU < 1 && + [0, 1, 2, 3].map((k) => { + const u = ease.move(clamp01(flyU * 1.6 - k * 0.2)); + if (u <= 0 || u >= 1) return null; + const x = lerp(BIT_X, SUM.x, u); + const y = lerp(ROW_Y(k + 2), SUM.y, u) - 30 * Math.sin(Math.PI * u); + return ; + })} + + {g(scene.sumU) > 0.01 && ( + + + + 4 + + + sum of bits + + + )} + + + {/* ghost run: one bit is 0 */} + {g(scene.ghostU) > 0.01 && ( + + {[0, 1, 2, 3].map((j) => ( + + ))} + + + if one bit is 0 + + + )} + + {/* the relu plot (shared by B4 and B8) */} + {g(scene.plotU) > 0.01 && ( + + + + + {g(scene.markU) > 0.01 && ( + + 0.5 ? colors.POSITIVE : colors.NEGATIVE} /> + {/* relu = 1 sits below-right of its dot, clear of the y-axis */} + 0.5 ? RX(markX) + 14 : RX(markX) - 12} y={RY(RELU(markX)) + (markX > 0.5 ? 24 : -12)} textAnchor={markX > 0.5 ? 'start' : 'end'} fill={markX > 0.5 ? colors.POSITIVE : colors.NEGATIVE} fontSize={15} fontFamily={MONO} fontWeight={700}> + {markX > 0.5 ? 'relu = 1' : 'relu = 0'} + + + )} + {g(scene.ghostU) > 0.01 && markX > 0.5 && ( + + + + )} + + )} + {/* B8: armed = all(armedAfterDown, not(up.active), not(cancel)) */} + {g(scene.b8U) > 0.01 && ( + + {[ + { n: 'armedAfterDown', v: 1 }, + { n: 'not(up.active)', v: 0 }, + { n: 'not(cancel)', v: 1 }, + ].map((b, i) => ( + + + {b.n} + + + + ))} + + + the switch disarms + + + )} + + {/* ================= B5–B7: the select gadget ================= */} + + + + + {/* instance B: a second input state (no press, armed was 1) — the on side is still computed */} + {g(scene.selB) > 0.01 && ( + + + a second case: no press, armed was 1 · gate = 0 + + + + on is still computed + + + + + 1 + + + off passes + + + )} + + {/* B6: the old cell value feeds the select; the result is a new wire (the curve leaves the + cell's right corner and passes between the "armed" and "springMatrix" labels) */} + {g(scene.curveU) > 0.01 && ( + + )} + + + + cell armed = old value (0) + + + + + + wire armedAfterDown = after the press (1) + + + + {/* ================= B9–B10: the spring as a matrix power ================= */} + {springU > 0.01 && ( + + + spring state, one dot per 4 ms step + + + + velocity, px/s + + + displacement from the destination, px + + {/* trail */} + + {PATH.slice(1).map((p, i) => + i + 1 <= trailN + 1e-6 ? : null, + )} + + {g(scene.startU) > 0.01 && ( + + + {/* right of the dot, below the M¹⁶ jump arrow's path */} + + start (−20, 0) + + + )} + {trailN >= 15.99 && ( + + + 16 steps · 64 ms + + + ({fmt2(PATH[16][0])}, {fmt2(PATH[16][1])}) + + + )} + {hop > 0.001 && } + {/* B10: one jump by M^16 lands on the 16th dot */} + + {g(scene.jumpU) > 0.6 && ( + + )} + {g(scene.jumpU) >= 1 && } + + {/* matrix panel */} + + {g(scene.matU) > 0.01 && ( + + interpolateRgb('#0d1321', matNow[i * 2 + j] < 0 ? colors.NEGATIVE : colors.ACCENT)(0.18 + 0.5 * v)} + /> + {matNow.map((m, i) => ( + + {fmt4(m)} + + ))} + 0.01 ? colors.WARM : colors.TEXT} boxWidth={120} /> + + {POWER_N.map((n, i) => ( + + {`M${['¹', '²', '⁴', '⁸', '¹⁶'][i]}`} + {i < 4 ? ' → ' : ''} + + ))} + {' each a squaring'} + + + stiffness 650 · damping 45 · 4 ms step + + + every entry of Mⁿ, n ≤ 300, stays within ±{BOUND.toFixed(2)} + + + )} + {/* B10: JavaScript writes the power into the four springMatrix cells */} + {g(scene.writeU) > 0.01 && ( + + + + JavaScript writes + + + )} + {g(scene.opsU) > 0.01 && ( + + + + + )} + + )} + + {/* ================= B11: one fixed list ================= */} + {closeU > 0.01 && ( + + + one fixed list, every evaluation + + {listItems.map((it, i) => { + const u = ease.enter(clamp01(listU - i)); + if (u <= 0.01) return null; + const glow = i === 0 ? pulse : pulse * 0.8; + return ( + + + + + {it.text} + + {glow > 0.02 && } + + ); + })} + {/* evaluation ticks: T1 has a press, T2 and T3 do not; the list is the same each time */} + + + evaluations + + + {evU.map((p, i) => { + const x = 1030 + i * 80; + const lit = p > 0.01; + return ( + + + {p > 0 && p < 1 && } + + T{i + 1} + + + {i === 0 ? 'press' : 'no press'} + + + ); + })} + + + )} + + + {/* ================= screen-space: header chips + code chip ================= */} + + + 2 · A Switch of Arithmetic + + + + {chip} + + + + + ); +} + +export const vizScene = () => scene; diff --git a/apps/bookshelf/src/viz/books/handwired/chapter-3.stories.tsx b/apps/bookshelf/src/viz/books/handwired/chapter-3.stories.tsx new file mode 100644 index 00000000..f6baef3d --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired/chapter-3.stories.tsx @@ -0,0 +1,28 @@ +// Handwired — chapter 3 story. Scene authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan. +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Player } from '../../core'; +import { Render, vizScene } from './chapter-3'; + +/** + * The Host Loop — the program is a fixed block in a sandwich of host JavaScript: events queue, advanceFrame + * plans the evaluations, the host writes the table, evaluate runs every operation, next values are copied back + * (or a frame's own evaluation runs on a copy), and a plain-JavaScript lane agrees with every frame. + */ +function Chapter3() { + const scene = vizScene(); + return ( +
+ + {(s) => } + +
+ ); +} + +const meta: Meta = { + title: 'Books/Handwired/Chapter 3', + component: Chapter3, +}; +export default meta; + +export const Scene: StoryObj = {}; diff --git a/apps/bookshelf/src/viz/books/handwired/chapter-3.tsx b/apps/bookshelf/src/viz/books/handwired/chapter-3.tsx new file mode 100644 index 00000000..d79830ca --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired/chapter-3.tsx @@ -0,0 +1,1737 @@ +// The Host Loop +// +// Handwired — chapter 3. Source pin: chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46 (MIT). +// docs/README.md:21 (the host), docs/README.md:9 (runs every operation, can't stop early) +// docs/tutorial.md:13 (the host and the table), 110-112 (event queue, advanceFrame, the callback), +// 143 (write-back policy), 145 (checks; the off-grid throw), 151-157 (DOM writes), 165-169 (the test) +// template/toggle.ts:9-38 (layout, the 20 cells), 86 (ToggleEvent), 92-139 (createToggleMachine: queue, step, +// the callback), 141-158 (mountToggle) +// template/toggle.test.ts:9-39 (scripted trace), 41-61 (the comparison loop and its bounds), 65-69 (off-grid error) +// template/toggle-reference.ts:17-28, 35-53 (plain-JavaScript timing) +// docs/execution-model.md:166-228 (springs and time; which evaluations write state back) +// src/runtime/host-timing.ts:36-40 (4 ms step, 16-step settle checks, 300-step cap), 314-406 (advanceFrame) +// src/runtime/cells.ts:27-32 (cell kinds; inputs are kept, events reset) +// docs/behavior-choices.md:44 (the planner; a spring not at rest settles only on a settle check), 48 (stored +// tables bit-identical at 10, 30, 60 and 120 fps in the toast stack), 52 (window-manager.test.ts replays a +// session at several frame rates); the four frame-rate lanes on screen are an illustration of that rule +// +// Authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan. +// +// Every per-frame number on screen (on, armed, thumb offsets, the 38 animating frames) is computed at module +// scope by a line-for-line port of toggle-reference.ts over the test's scripted trace, and checked against the +// test's own annotations; the module throws if they ever disagree. One correction to the plan: the evaluation +// at 1061 ms runs while the spring clock is stopped, so advanceFrame plans it with settleCheck = true +// (host-timing.ts:388-389), and the host writes 1 into settleCheck, not 0. +import { CAMERA_HOME, Camera, MathLabel, STAGE_H, STAGE_W, Timeline, cameraInterp, colors, ease, mulberry32 } from '../../core'; +import type { CameraState, SceneState } from '../../core'; + +const clamp01 = (u: number): number => (u < 0 ? 0 : u > 1 ? 1 : u); +const lerp = (a: number, b: number, u: number): number => a + (b - a) * u; +/** 0 → 1 → 0 bump over [a, b] of u */ +const win = (u: number, a: number, b: number): number => clamp01((u - a) / Math.max(1e-6, b - a)); +const MONO = colors.font.mono; +const UI = colors.font.ui; +const CH14 = 8.43; // mono advance at 14 px +const CH13 = 7.83; // mono advance at 13 px +const fmt = (v: number): string => (v < 0 ? `−${Math.abs(v)}` : `${v}`); + +// --------------------------------------------------------------------------- +// The scripted trace and the plain-JavaScript reference (module scope, exact) +// --------------------------------------------------------------------------- + +type Ev = { kind: 'down' | 'up' | 'cancel'; x: number; y: number; timeStamp: number }; +type TraceFrame = { timeMs: number; events: Ev[]; expected?: { on: number; armed: number } }; + +/** toggle.test.ts:11-39, verbatim in behaviour. */ +function scriptedTrace(): TraceFrame[] { + const frames: TraceFrame[] = []; + let timeMs = 1000; + const idle = (count: number, gapMs = 16) => { + for (let i = 0; i < count; i++) frames.push({ timeMs: (timeMs += gapMs), events: [] }); + }; + const pointer = (kind: Ev['kind'], x: number, y: number, on: number, armed: number) => { + timeMs += 16; + frames.push({ timeMs, events: [{ kind, x, y, timeStamp: timeMs - 3 }], expected: { on, armed } }); + }; + idle(2); + pointer('down', 640, 400, 0, 1); + pointer('up', 640, 400, 1, 0); + idle(3); + pointer('down', 630.25, 399.5, 1, 1); + pointer('up', 630.25, 399.5, 0, 0); + idle(60); + pointer('down', 613.984375, 400, 0, 0); + pointer('down', 614, 384, 0, 1); + pointer('up', 666, 400, 0, 0); + pointer('down', 640, 400, 0, 1); + pointer('cancel', 0, 0, 0, 0); + pointer('up', 640, 400, 0, 0); + pointer('down', 665.984375, 415.984375, 0, 1); + pointer('up', 665.984375, 415.984375, 1, 0); + idle(1, 500); + idle(60); + return frames; +} + +type RefFrame = { timeMs: number; on: number; armed: number; thumb: number; animating: boolean; hasEvent: boolean }; + +/** toggle-reference.ts:10-58 at 1280 × 800 (one event per frame in this trace). */ +function runReference(frames: TraceFrame[]): RefFrame[] { + const st = { on: 0, armed: 0, thumb: 0, velocity: 0, animatedUntilMs: null as number | null, checkOriginMs: 0 }; + const destination = () => st.on * 20; + const within = () => Math.abs(st.thumb - destination()) <= Math.fround(0.01) && Math.abs(st.velocity) <= Math.fround(0.01); + const q = (t: number) => Math.round(t * 64) / 64 + 0; + const stepTo = (timeMs: number) => { + while (st.animatedUntilMs !== null && st.animatedUntilMs + 4 <= timeMs) { + st.velocity += (-650 * (st.thumb - destination()) - 45 * st.velocity) * 0.004; + st.thumb += st.velocity * 0.004; + st.animatedUntilMs += 4; + if ((st.animatedUntilMs - st.checkOriginMs) % 64 === 0 && within()) { + st.thumb = destination(); + st.velocity = 0; + st.animatedUntilMs = null; + } + } + }; + const trackLeft = 1280 / 2 - 52 / 2; + const trackTop = 800 / 2 - 32 / 2; + const inside = (e: Ev) => e.x >= trackLeft && e.x < trackLeft + 52 && e.y >= trackTop && e.y < trackTop + 32; + return frames.map((f) => { + for (const e of f.events) { + const eventMs = q(e.timeStamp); + stepTo(eventMs); + if (e.kind === 'down') st.armed = inside(e) ? 1 : 0; + if (e.kind === 'up' && st.armed === 1 && inside(e)) st.on = 1 - st.on; + if (e.kind === 'up' || e.kind === 'cancel') st.armed = 0; + if (st.animatedUntilMs === null && !within()) { + st.animatedUntilMs = eventMs; + st.checkOriginMs = eventMs; + } + } + stepTo(q(f.timeMs)); + return { timeMs: f.timeMs, on: st.on, armed: st.armed, thumb: st.thumb, animating: st.animatedUntilMs !== null, hasEvent: f.events.length > 0 }; + }); +} + +const TRACE = scriptedTrace(); +const REF = runReference(TRACE); +const N_FRAMES = REF.length; +const ANIMATING = REF.filter((r) => r.animating).length; +// The test's own assertions (toggle.test.ts:52, 58): every annotated frame, and 38 animating frames. +TRACE.forEach((f, i) => { + if (f.expected && (f.expected.on !== REF[i].on || f.expected.armed !== REF[i].armed)) { + throw new Error(`handwired ch3: reference disagrees with the test annotation at frame ${i}`); + } +}); +if (ANIMATING !== 38) throw new Error(`handwired ch3: expected 38 animating frames, got ${ANIMATING}`); +const THUMB_MAX = Math.max(...REF.map((r) => r.thumb)); +const FOCUS_FRAME = 10; // the thumb is mid-flight here (after the second click flips it back) +// Outputs of the frame evaluations after the release (1064, 1080, 1096, 1112 ms). +const OUT_FRAMES = [3, 4, 5, 6].map((i) => REF[i]); + +// --------------------------------------------------------------------------- +// The 20-cell table (toggleCells order, toggle.ts:18-38) +// --------------------------------------------------------------------------- + +type Kind = 'input' | 'event' | 'state'; +const KIND_COLOR: Record = { input: colors.ACCENT, event: colors.WARM, state: colors.POSITIVE }; +const CELLS: { name: string; kind: Kind }[] = [ + { name: 'viewportWidth', kind: 'input' }, + { name: 'viewportHeight', kind: 'input' }, + { name: 'down.active', kind: 'event' }, + { name: 'down.x', kind: 'input' }, + { name: 'down.y', kind: 'input' }, + { name: 'up.active', kind: 'event' }, + { name: 'up.x', kind: 'input' }, + { name: 'up.y', kind: 'input' }, + { name: 'cancel', kind: 'event' }, + { name: 'on', kind: 'state' }, + { name: 'armed', kind: 'state' }, + { name: 'springMatrix[0]', kind: 'input' }, + { name: 'springMatrix[1]', kind: 'input' }, + { name: 'springMatrix[2]', kind: 'input' }, + { name: 'springMatrix[3]', kind: 'input' }, + { name: 'settleCheck', kind: 'input' }, + { name: 'snap', kind: 'input' }, + { name: 'thumb.displacement', kind: 'state' }, + { name: 'thumb.velocity', kind: 'state' }, + { name: 'thumb.destination', kind: 'state' }, +]; +// settleCheck ends just inside its cell's right edge, so it and snap (centred) sit side by side under their own +// cells instead of overlapping. +const GROUPS: { label: string; from: number; to: number; align?: 'end' }[] = [ + { label: 'viewport', from: 0, to: 1 }, + { label: 'down', from: 2, to: 4 }, + { label: 'up', from: 5, to: 7 }, + { label: 'cancel', from: 8, to: 8 }, + { label: 'on', from: 9, to: 9 }, + { label: 'armed', from: 10, to: 10 }, + { label: 'springMatrix', from: 11, to: 14 }, + { label: 'settleCheck', from: 15, to: 15, align: 'end' }, + { label: 'snap', from: 16, to: 16 }, + { label: 'thumb', from: 17, to: 19 }, +]; + +// Stored table after the press at 1045 (written back) and the frame at 1048 (on a copy): derived. +const T_BEFORE = [1280, 800, 0, 640, 400, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0]; +// The host's writes for the evaluation at 1061 ms, in toggle.ts:115-126 order (0 steps → identity matrix; +// a stopped clock → settleCheck 1; no snap; viewport; the release and its coordinates). +const WRITES: { cells: number[]; values: number[]; label: string }[] = [ + { cells: [11, 12, 13, 14], values: [1, 0, 0, 1], label: 'springMatrix' }, + { cells: [15], values: [1], label: 'settleCheck' }, + { cells: [16], values: [0], label: 'snap' }, + { cells: [0, 1], values: [1280, 800], label: 'viewport' }, + { cells: [5], values: [1], label: 'up.active' }, + { cells: [6], values: [640], label: 'up.x' }, + { cells: [7], values: [400], label: 'up.y' }, +]; +const WRITE_GROUP: number[] = CELLS.map((_, i) => WRITES.findIndex((w) => w.cells.includes(i))); +const T_WRITTEN = T_BEFORE.map((v, i) => { + const g = WRITE_GROUP[i]; + return g < 0 ? v : WRITES[g].values[WRITES[g].cells.indexOf(i)]; +}); +// next values (derived by hand from execution-model.md:178-190 and cells.ts:27-32): inputs kept, events reset, +// on flips, armed clears, the thumb's spring is 20 px from its new destination with 0 steps run. +const NEXT = T_WRITTEN.map((v, i) => { + const n = CELLS[i].name; + if (CELLS[i].kind === 'event') return 0; + if (n === 'on') return 1; + if (n === 'armed') return 0; + if (n === 'thumb.displacement') return -20; + if (n === 'thumb.velocity') return 0; + if (n === 'thumb.destination') return 20; + return v; +}); +const CHANGED = NEXT.map((v, i) => v !== T_WRITTEN[i]); + +// --------------------------------------------------------------------------- +// Layout (stage 1280×720; y ≥ 630 is the caption band) +// --------------------------------------------------------------------------- + +const TOP = { x: 40, y: 80, w: 1200, h: 142 }; // host band: the planner's timeline +const BOT = { x: 40, y: 502, w: 1200, h: 120 }; // host band: queue + page +const CELL_W = 48; +const CELL_H = 28; +const PITCH = 56; +const CX0 = 70; +const cellX = (i: number): number => CX0 + i * PITCH; +const TABLE_Y = 244; +const NEXT_Y = 458; +const SLOT = { x: 200, y: 316, w: 880, h: 120 }; +const BIG = { x: 340, y: 150, w: 600, h: 320 }; +const RING = { x: 120, y: 104, w: 1040, h: 430 }; +const HEADER_H = 76; // the screen-space title band + +// timeline: x 60 ↔ 1032 ms … x 1220 ↔ 1144 ms +const TX = (ms: number): number => 60 + ((ms - 1032) * 1160) / 112; +const AXIS_Y = 128; +const EVAL_Y = 168; +const FRAME_MS = [1032, 1048, 1064, 1080, 1096, 1112, 1128, 1144]; +const DIM_AFTER = 1125; // frames 7–8 are drawn dim and unlabelled + +// program block layer stack (schematic): seeded column heights +const STACK = (() => { + const rand = mulberry32(31); + return Array.from({ length: 22 }, () => 2 + Math.floor(rand() * 4)); +})(); + +// bottom band +const QUEUE = { x: 130, y: 530, w: 440, h: 44 }; +const TRACK = { x: 740, y: 532, w: 104, h: 64 }; // the mini toggle at 2× +const OUT_CHIP = { x: 862, y: 514, w: 176, h: 24, dx: 186, dy: 30 }; + +// comparator +const CMP_X0 = 100; +const CMP_X1 = 1220; +const FW = (CMP_X1 - CMP_X0) / N_FRAMES; +const fx = (i: number): number => CMP_X0 + i * FW; +const LANE_A = 150; +const LANE_B = 240; +const CURVE_Y0 = 446; // 0 px +const CURVE_Y1 = 336; // 20 px +const cy = (px: number): number => CURVE_Y0 + ((CURVE_Y1 - CURVE_Y0) * px) / 20; +const WINDOW = { x: 790, y: 470, w: 430, h: 104 }; + +// pixel rulers (1/64 px = 96 world px) +const RL = { cx: 400, y: 470 }; +const rlx = (v: number): number => RL.cx + (v - 614) * 64 * 96; +const RC = { cx: 900, cy: 420 }; +const rcx = (v: number): number => RC.cx + (v - 666) * 64 * 96; +const rcy = (v: number): number => RC.cy + (v - 416) * 64 * 96; + +const CAM_QUEUE: CameraState = { x: 620, y: 560, k: 1.25 }; +const CAM_TL: CameraState = { x: 640, y: 344, k: 0.92 }; // card, code chip above it and the queue band all fit +const CAM_TL_ZOOM: CameraState = { x: 300, y: 165, k: 1.75 }; +const CAM_TL_PAN: CameraState = { x: 740, y: 190, k: 1.4 }; // the release at 1061 through the settle box, both labels clear of the edges +const CAM_TABLE: CameraState = { x: 655, y: 300, k: 1.08 }; +const CAM_LOWER: CameraState = { x: 640, y: 372, k: 1.04 }; +const CAM_RATES: CameraState = { x: 640, y: 392, k: 1.05 }; +const CAM_PAGE: CameraState = { x: 860, y: 480, k: 1.3 }; +const CAM_CMP: CameraState = { x: 640, y: 372, k: 1.02 }; +const CAM_RULE_L: CameraState = { x: 400, y: 430, k: 1.8 }; +const CAM_RULE_R: CameraState = { x: 900, y: 400, k: 1.6 }; + +const SOURCES = [ + 'docs/README.md:21', + 'template/toggle.ts:106-108 · docs/tutorial.md:110', + 'docs/tutorial.md:110 · template/toggle.ts:111', + 'toggle.test.ts:9 · toggle-reference.ts:17-28, 47-50 (derived)', + 'template/toggle.ts:115-126 · toggle.test.ts:68', + 'template/toggle.ts:129-130 · execution-model.md:178-190 (derived)', + 'toggle.ts:112-113 · execution-model.md:226 · behavior-choices.md:48, 52', + 'template/toggle.ts:141-158 · docs/tutorial.md:151', + 'template/toggle.test.ts:1, 41-56', + 'docs/tutorial.md:167 · template/toggle.test.ts:58-61', + 'template/toggle.test.ts:28-35 · docs/tutorial.md:169', + 'docs/tutorial.md:13 · docs/README.md:9', +]; + +// code chip bank (verbatim excerpts; long lines wrapped) +const C3A = ['queue(event: ToggleEvent) {', ' queued.push(event)', '}']; +const C3B = ['machine.clock = advanceFrame(machine.clock, frameTime, queued, evaluation => {']; +const C3C = [ + 'cells.springMatrix.forEach((wire, index) => {', + ' values[wire] = matrix[index]! })', + 'values[cells.settleCheck] = evaluation.settleCheck ? 1 : 0', +]; +const C3D = ['const result = evaluate(values)', 'if (evaluation.writesBack) source.set(result.nextValues)']; +const C3E = [ + 'const values = evaluation.writesBack ? source : copy', + 'if (!evaluation.writesBack) copy.set(source)', + "writeBack: WriteBack = 'events and settle checks'", +]; +const C3F = ['track.style.transform =', ' `translate3d(${output.trackLeft}px,${output.trackTop}px,0)`']; +const C3G = ['expect(largestThumbError).toBeLessThan(1e-5)']; +const C3H = [ + "pointer('down', 613.984375, 400, 0, 0, false) // 1/64 px left of the track", + "pointer('down', 614, 384, 0, 1) // exactly on the left and top edges", + "pointer('up', 666, 400, 0, 0) // exactly on the right edge, which is outside, so it only disarms", +]; + +// --------------------------------------------------------------------------- +// Timeline — 112.6 s, twelve fixed captions +// --------------------------------------------------------------------------- + +export function buildScene() { + const tl = new Timeline(); + const cam = tl.channel('cam', { x: 640, y: 330, k: 1.08 }, cameraInterp); + const src = tl.channel('src', 0); + + // layer opacities (the sandwich) + const opTL = tl.channel('opTL', 1); + const opTable = tl.channel('opTable', 1); + const opProg = tl.channel('opProg', 1); + const opNext = tl.channel('opNext', 1); + const opBottom = tl.channel('opBottom', 1); + const opTop = tl.channel('opTop', 1); // the top host band, empty once the timeline and the writes are gone + const cmpMode = tl.channel('cmpMode', 0); // sandwich whispers while the comparator runs + + // B0 + const introU = tl.channel('introU', 0); + const crossU = tl.channel('crossU', 0); + const hostIconsU = tl.channel('hostIconsU', 0); + const sandU = tl.channel('sandU', 0); + const tableU = tl.channel('tableU', 0); + // B1 + const pageU = tl.channel('pageU', 0); + const pressU = tl.channel('pressU', 0); // 0 none · 1 press · 2 release + const q1U = tl.channel('q1U', 0); + const q2U = tl.channel('q2U', 0); + const drainU = tl.channel('drainU', 0); + const codeA = tl.channel('codeA', 0); + // B2 + const axisU = tl.channel('axisU', 0); + const framesU = tl.channel('framesU', 0); + const eventsU = tl.channel('eventsU', 0); + const afU = tl.channel('afU', 0); + const codeB = tl.channel('codeB', 0); + const leg1 = tl.channel('leg1', 0); + const leg2 = tl.channel('leg2', 0); + const leg3 = tl.channel('leg3', 0); + const legOp = tl.channel('legOp', 1); + // B3 + const b3U = tl.channel('b3U', 0); + const combU = tl.channel('combU', 0); + const settleU = tl.channel('settleU', 0); + // B4 + const pickU = tl.channel('pickU', 0); + const writeU = tl.channel('writeU', 0); // 0..7 groups + const codeC = tl.channel('codeC', 0); + const errU = tl.channel('errU', 0); + // B5 + const sweepU = tl.channel('sweepU', 0); + const nextU = tl.channel('nextU', 0); + const copyU = tl.channel('copyU', 0); + const gateU = tl.channel('gateU', 0); // the writesBack? gate on the copy-back arrow + const codeD = tl.channel('codeD', 0); + // B6 + const ghostU = tl.channel('ghostU', 0); + const ghostSweepU = tl.channel('ghostSweepU', 0); + const ghostOutU = tl.channel('ghostOutU', 0); + const crumbleU = tl.channel('crumbleU', 0); + const keepU = tl.channel('keepU', 0); + const codeE = tl.channel('codeE', 0); + const ratesU = tl.channel('ratesU', 0); + const barsU = tl.channel('barsU', 0); + const eqU = tl.channel('eqU', 0); + // B7 + const outU = tl.channel('outU', 0); + const tickU = tl.channel('tickU', 0); // 0..3 → frames 1064..1112 + const codeF = tl.channel('codeF', 0); + const backU = tl.channel('backU', 0); + // B8–B9 + const cmpU = tl.channel('cmpU', 0); + const scanU = tl.channel('scanU', -1); // frame index of the scan line + const exactU = tl.channel('exactU', 0); + const curveU = tl.channel('curveU', 0); + const loupeU = tl.channel('loupeU', 0); + const chipsU = tl.channel('chipsU', 0); + const b9Op = tl.channel('b9Op', 1); + const lanesOp = tl.channel('lanesOp', 1); + // B10 + const rulerU = tl.channel('rulerU', 0); + const dot1U = tl.channel('dot1U', 0); + const dot2U = tl.channel('dot2U', 0); + const cornerU = tl.channel('cornerU', 0); + const dot3U = tl.channel('dot3U', 0); + const dot4U = tl.channel('dot4U', 0); + const codeH = tl.channel('codeH', 0); + const rulersOp = tl.channel('rulersOp', 1); + // B11 + const finalU = tl.channel('finalU', 0); + + const caps = [ + { at: 0.4, dur: 8.3 }, + { at: 9.1, dur: 7.2 }, + { at: 16.7, dur: 10.9 }, + { at: 28, dur: 9.1 }, + { at: 37.5, dur: 8.3 }, + { at: 46.2, dur: 8.8 }, + { at: 55.4, dur: 11.8 }, + { at: 67.6, dur: 6.1 }, + { at: 74.1, dur: 8.3 }, + { at: 82.8, dur: 8.7 }, + { at: 91.9, dur: 9.1 }, + { at: 101.4, dur: 8.7 }, + ]; + caps.forEach((c, i) => tl.set(src, i, Math.max(0, c.at - 0.1))); + + // — B0 · a program can't wait, listen or loop; the host can — + tl.caption({ ...caps[0], text: "A program can't wait, listen or loop. The host can: ordinary Javascript around it that feeds the table and reads the answers." }); + tl.tween(introU, 1, { at: 0.5, dur: 0.8, ease: ease.enter }); + tl.tween(crossU, 1, { at: 1.3, dur: 0.9, ease: ease.draw }); + tl.tween(hostIconsU, 1, { at: 3.3, dur: 0.8, ease: ease.enter }); + tl.tween(cam, CAMERA_HOME, { at: 4.8, dur: 1.4, ease: ease.move }); + tl.tween(sandU, 1, { at: 5.0, dur: 1.4, ease: ease.move }); + tl.tween(tableU, 1, { at: 5.9, dur: 1.0, ease: ease.enter }); + tl.hold(8.7, 0.4); + + // — B1 · handlers only queue — + tl.caption({ ...caps[1], text: 'Event handlers do almost nothing: a press or a release is queued with its timestamp for the next frame.' }); + tl.tween(cam, CAM_QUEUE, { at: 9.1, dur: 1.2, ease: ease.move }); + tl.tween(crossU, 0, { at: 9.1, dur: 0.6, ease: ease.move }); + tl.tween(opTable, 0.3, { at: 9.1, dur: 0.6, ease: ease.move }); + tl.tween(opProg, 0.3, { at: 9.1, dur: 0.6, ease: ease.move }); + tl.tween(pageU, 1, { at: 9.4, dur: 0.6, ease: ease.enter }); + tl.tween(pressU, 1, { at: 10.3, dur: 0.6, ease: ease.move }); + tl.tween(q1U, 1, { at: 10.9, dur: 0.7, ease: ease.enter }); + tl.tween(pressU, 2, { at: 12.0, dur: 0.6, ease: ease.move }); + tl.tween(q2U, 1, { at: 12.6, dur: 0.7, ease: ease.enter }); + tl.tween(codeA, 1, { at: 13.6, dur: 0.6, ease: ease.enter }); + tl.hold(16.3, 0.4); + + // — B2 · advanceFrame plans the frame — + tl.caption({ ...caps[2], text: 'Each frame calls a planner, advance frame: one evaluation per event time, one at the frame, and a settle check every sixty-four milliseconds, where a resting spring may stop.' }); + tl.tween(cam, CAM_TL, { at: 16.7, dur: 1.3, ease: ease.move }); + tl.tween(codeA, 0, { at: 16.7, dur: 0.5, ease: ease.move }); + tl.tween(opTable, 0.12, { at: 16.7, dur: 0.6, ease: ease.move }); + tl.tween(opProg, 0.12, { at: 16.7, dur: 0.6, ease: ease.move }); + tl.tween(axisU, 1, { at: 17.0, dur: 1.1, ease: ease.draw }); + tl.tween(framesU, 1, { at: 17.4, dur: 1.0, ease: ease.linear }); + tl.tween(eventsU, 1, { at: 18.0, dur: 0.6, ease: ease.pop }); + tl.tween(drainU, 1, { at: 18.0, dur: 0.6, ease: ease.move }); + tl.tween(afU, 1, { at: 18.5, dur: 0.7, ease: ease.draw }); + tl.tween(codeB, 1, { at: 18.8, dur: 0.6, ease: ease.enter }); + tl.tween(leg1, 1, { at: 19.6, dur: 0.6, ease: ease.pop }); + tl.tween(leg3, 1, { at: 21.3, dur: 0.6, ease: ease.pop }); + tl.tween(leg2, 1, { at: 23.0, dur: 0.6, ease: ease.pop }); + tl.hold(27.6, 0.4); + + // — B3 · two evaluations in an event frame; the first settle check — + tl.caption({ ...caps[3], text: 'In the test, events land three milliseconds before their frame, so that frame gets two evaluations: one at the event, one at the frame.' }); + tl.tween(cam, CAM_TL_ZOOM, { at: 28, dur: 1.2, ease: ease.move }); + tl.tween(codeB, 0, { at: 28, dur: 0.5, ease: ease.move }); + tl.tween(legOp, 0, { at: 28, dur: 0.5, ease: ease.move }); + tl.tween(afU, 0, { at: 28, dur: 0.5, ease: ease.move }); + tl.tween(b3U, 1, { at: 29, dur: 0.8, ease: ease.enter }); + tl.tween(cam, CAM_TL_PAN, { at: 32.2, dur: 1.4, ease: ease.move }); + tl.tween(combU, 1, { at: 32.8, dur: 1.2, ease: ease.draw }); + tl.tween(settleU, 1, { at: 34.2, dur: 0.7, ease: ease.enter }); + tl.hold(37.1, 0.4); + + // — B4 · the host writes the table — + tl.caption({ ...caps[4], text: 'Before each evaluation the host writes the table: the spring matrix, settle and snap bits, viewport, and any event with its coordinates.' }); + tl.tween(cam, CAM_TABLE, { at: 37.5, dur: 1.3, ease: ease.move }); + tl.tween(b3U, 0, { at: 37.5, dur: 0.5, ease: ease.move }); + tl.tween(combU, 0, { at: 37.5, dur: 0.5, ease: ease.move }); + tl.tween(settleU, 0, { at: 37.5, dur: 0.5, ease: ease.move }); + tl.tween(opTable, 1, { at: 37.5, dur: 0.7, ease: ease.move }); + tl.tween(opBottom, 0.15, { at: 37.5, dur: 0.6, ease: ease.move }); + tl.tween(pickU, 1, { at: 38.1, dur: 0.6, ease: ease.pop }); + tl.tween(codeC, 1, { at: 38.6, dur: 0.6, ease: ease.enter }); + tl.tween(opTL, 0, { at: 39.2, dur: 0.6, ease: ease.move }); + tl.tween(writeU, WRITES.length + 1, { at: 39.8, dur: (WRITES.length + 1) * 0.55, ease: ease.linear }); + tl.tween(errU, 1, { at: 43.7, dur: 0.3, ease: ease.enter }); + tl.tween(errU, 0, { at: 45.1, dur: 0.4, ease: ease.move }); + tl.hold(45.8, 0.4); + + // — B5 · evaluate, then copy the next values back — + tl.caption({ ...caps[5], text: 'Then evaluate runs every operation and returns next values and outputs. When an evaluation writes back, the host copies the next values over the table.' }); + tl.tween(cam, CAM_LOWER, { at: 46.2, dur: 1.2, ease: ease.move }); + tl.tween(pickU, 0, { at: 46.2, dur: 0.5, ease: ease.move }); + tl.tween(codeC, 0, { at: 46.2, dur: 0.5, ease: ease.move }); + tl.tween(opProg, 1, { at: 46.2, dur: 0.6, ease: ease.move }); + tl.tween(opTop, 0, { at: 46.2, dur: 0.6, ease: ease.move }); // pushed up, its label would sit under the header chip + tl.tween(sweepU, 1, { at: 46.8, dur: 1.6, ease: ease.linear }); + tl.tween(codeD, 1, { at: 47.2, dur: 0.6, ease: ease.enter }); + tl.tween(nextU, 1, { at: 48.2, dur: 0.9, ease: ease.linear }); + tl.tween(gateU, 1, { at: 49.6, dur: 0.6, ease: ease.pop }); + tl.tween(copyU, 1, { at: 50.5, dur: 2.6, ease: ease.linear }); + tl.hold(55, 0.4); + + // — B6 · a frame's own evaluation runs on a copy; four frame rates agree — + tl.caption({ ...caps[6], text: "A frame's own evaluation runs on a copy, so stored state follows events, not frame rate. The repo's replay tests find stored tables identical, bit for bit, at every frame rate tried." }); + tl.tween(codeD, 0, { at: 55.4, dur: 0.5, ease: ease.move }); + tl.tween(nextU, 0, { at: 55.4, dur: 0.5, ease: ease.move }); + tl.tween(sweepU, 0, { at: 55.4, dur: 0.4, ease: ease.move }); + tl.tween(gateU, 0, { at: 55.4, dur: 0.4, ease: ease.move }); + tl.tween(codeE, 1, { at: 55.7, dur: 0.6, ease: ease.enter }); + tl.tween(ghostU, 1, { at: 56, dur: 0.8, ease: ease.move }); + tl.tween(ghostSweepU, 1, { at: 57, dur: 1.0, ease: ease.linear }); + tl.tween(ghostOutU, 1, { at: 57.9, dur: 0.6, ease: ease.move }); + tl.tween(opBottom, 0.7, { at: 57.9, dur: 0.6, ease: ease.move }); + tl.tween(crumbleU, 1, { at: 58.7, dur: 0.9, ease: ease.move }); + tl.tween(keepU, 1, { at: 58.9, dur: 0.6, ease: ease.enter }); + tl.tween(cam, CAM_RATES, { at: 60.8, dur: 1.2, ease: ease.move }); + tl.tween(codeE, 0, { at: 60.8, dur: 0.5, ease: ease.move }); + tl.tween(keepU, 0, { at: 60.8, dur: 0.5, ease: ease.move }); + tl.tween(ghostOutU, 0, { at: 60.8, dur: 0.5, ease: ease.move }); + tl.tween(opBottom, 0.15, { at: 60.8, dur: 0.6, ease: ease.move }); + tl.tween(opTable, 0.3, { at: 60.8, dur: 0.6, ease: ease.move }); + tl.tween(ratesU, 1, { at: 61.2, dur: 0.9, ease: ease.enter }); + tl.tween(barsU, 1, { at: 62.6, dur: 1.0, ease: ease.linear }); + tl.tween(eqU, 1, { at: 64, dur: 0.6, ease: ease.pop }); + tl.hold(67.2, 0.4); + + // — B7 · the page draws from outputs only — + tl.caption({ ...caps[7], text: "The page draws from output numbers only: the track's position, the on flag, the thumb's offset." }); + tl.tween(ratesU, 0, { at: 67.6, dur: 0.6, ease: ease.move }); + tl.tween(barsU, 0, { at: 67.6, dur: 0.6, ease: ease.move }); + tl.tween(eqU, 0, { at: 67.6, dur: 0.6, ease: ease.move }); + tl.tween(cam, CAM_PAGE, { at: 67.6, dur: 1.2, ease: ease.move }); + tl.tween(opTable, 0.25, { at: 67.6, dur: 0.6, ease: ease.move }); + tl.tween(opProg, 0.45, { at: 67.6, dur: 0.6, ease: ease.move }); + tl.tween(opBottom, 1, { at: 67.6, dur: 0.6, ease: ease.move }); + tl.tween(ghostSweepU, 0, { at: 67.6, dur: 0.5, ease: ease.move }); + tl.tween(outU, 1, { at: 68.1, dur: 1.0, ease: ease.move }); + tl.tween(codeF, 1, { at: 68.9, dur: 0.6, ease: ease.enter }); + tl.tween(tickU, 3, { at: 69.3, dur: 2.4, ease: ease.linear }); + tl.tween(backU, 1, { at: 71.5, dur: 0.8, ease: ease.draw }); + tl.hold(73.7, 0.4); + + // — B8 · the two-lane comparator — + tl.caption({ ...caps[8], text: 'To check it, the repo runs the same switch in plain Javascript and compares both on a scripted trace, frame by frame.' }); + tl.tween(cam, CAM_CMP, { at: 74.1, dur: 1.2, ease: ease.move }); + tl.tween(codeF, 0, { at: 74.1, dur: 0.5, ease: ease.move }); + tl.tween(backU, 0, { at: 74.1, dur: 0.5, ease: ease.move }); + tl.tween(cmpMode, 1, { at: 74.1, dur: 0.8, ease: ease.move }); + tl.tween(cmpU, 1, { at: 74.7, dur: 0.9, ease: ease.enter }); + tl.tween(scanU, N_FRAMES - 1, { at: 76.3, dur: 5.2, ease: ease.linear }); + tl.hold(82.4, 0.4); + + // — B9 · decisions exact; the thumb within 1e-5 px — + tl.caption({ ...caps[9], text: 'Decisions must match exactly: on, armed, track position. The thumb may differ, but by under a hundred-thousandth of a pixel, across the trace.' }); + tl.tween(exactU, 1, { at: 83, dur: 0.6, ease: ease.enter }); + tl.tween(curveU, 1, { at: 84.9, dur: 1.3, ease: ease.draw }); + tl.tween(loupeU, 1, { at: 86.5, dur: 0.7, ease: ease.pop }); + tl.tween(chipsU, 1, { at: 87.9, dur: 0.9, ease: ease.enter }); + tl.hold(91.5, 0.4); + + // — B10 · the edge presses at 1/64 px — + tl.caption({ ...caps[10], text: 'The trace presses where decisions are hardest: one sixty-fourth of a pixel outside the track, exactly on its edge, and just inside a corner.' }); + tl.tween(b9Op, 0, { at: 91.9, dur: 0.6, ease: ease.move }); + tl.tween(lanesOp, 0.04, { at: 91.9, dur: 0.6, ease: ease.move }); + tl.tween(cam, CAM_RULE_L, { at: 92, dur: 1.3, ease: ease.move }); + tl.tween(rulerU, 1, { at: 92.5, dur: 1.0, ease: ease.draw }); + tl.tween(codeH, 1, { at: 93.1, dur: 0.6, ease: ease.enter }); + tl.tween(dot1U, 1, { at: 94.3, dur: 0.6, ease: ease.pop }); + tl.tween(dot2U, 1, { at: 95.5, dur: 0.6, ease: ease.pop }); + tl.tween(cam, CAM_RULE_R, { at: 96.5, dur: 1.3, ease: ease.move }); + tl.tween(cornerU, 1, { at: 96.8, dur: 1.0, ease: ease.draw }); + tl.tween(dot3U, 1, { at: 97.9, dur: 0.6, ease: ease.pop }); + tl.tween(dot4U, 1, { at: 99.1, dur: 0.6, ease: ease.pop }); + tl.hold(101, 0.4); + + // — B11 · waiting, listening and looping stay out here — + tl.caption({ ...caps[11], text: 'Waiting, listening and looping all stay out here. Inside, there is no loop and no early exit, so loops must become something else.' }); + tl.tween(codeH, 0, { at: 101.4, dur: 0.6, ease: ease.move }); + tl.tween(rulersOp, 0, { at: 101.4, dur: 0.7, ease: ease.move }); + tl.tween(cmpU, 0, { at: 101.4, dur: 0.7, ease: ease.move }); + tl.tween(cam, CAMERA_HOME, { at: 101.4, dur: 1.4, ease: ease.move }); + tl.tween(cmpMode, 0, { at: 101.8, dur: 1.0, ease: ease.move }); + tl.tween(opTable, 0.1, { at: 101.4, dur: 0.8, ease: ease.move }); + tl.tween(opBottom, 0.1, { at: 101.4, dur: 0.8, ease: ease.move }); + tl.tween(opNext, 0.1, { at: 101.4, dur: 0.8, ease: ease.move }); + tl.tween(opProg, 1, { at: 101.4, dur: 0.8, ease: ease.move }); + tl.tween(opTop, 1, { at: 101.4, dur: 0.8, ease: ease.move }); + tl.tween(outU, 0, { at: 101.4, dur: 0.6, ease: ease.move }); + tl.tween(crossU, 0.9, { at: 102.5, dur: 0.9, ease: ease.draw }); + tl.tween(finalU, 1, { at: 103.3, dur: 0.9, ease: ease.enter }); + tl.hold(110.1, 2.5); + + return { + tl, cam, src, opTL, opTable, opProg, opNext, opBottom, opTop, cmpMode, + introU, crossU, hostIconsU, sandU, tableU, + pageU, pressU, q1U, q2U, drainU, codeA, + axisU, framesU, eventsU, afU, codeB, leg1, leg2, leg3, legOp, + b3U, combU, settleU, + pickU, writeU, codeC, errU, + sweepU, nextU, copyU, gateU, codeD, + ghostU, ghostSweepU, ghostOutU, crumbleU, keepU, codeE, ratesU, barsU, eqU, + outU, tickU, codeF, backU, + cmpU, scanU, exactU, curveU, loupeU, chipsU, b9Op, lanesOp, + rulerU, dot1U, dot2U, cornerU, dot3U, dot4U, codeH, rulersOp, + finalU, + }; +} + +const scene = buildScene(); + +// --------------------------------------------------------------------------- +// Glyph kit (local copies, per the book-wide spec) +// --------------------------------------------------------------------------- + +function CellChip({ x, y, value, kind, flash = 0, dashed = false, opacity = 1, name, nameOp = 0, scale = 1 }: { + x: number; y: number; value?: number; kind: Kind; flash?: number; dashed?: boolean; opacity?: number; + name?: string; nameOp?: number; scale?: number; +}) { + if (opacity <= 0.005) return null; + const w = CELL_W * scale; + const h = CELL_H * scale; + const ox = x + (CELL_W - w) / 2; + const oy = y + (CELL_H - h) / 2; + return ( + + 0.01 ? colors.TEAL : colors.GRID} + strokeWidth={1 + 1.5 * flash} strokeDasharray={dashed ? '4 3' : undefined} /> + {flash > 0.01 && } + {!dashed && } + {value !== undefined && ( + + {fmt(value)} + + )} + {name && nameOp > 0.01 && ( + + {name} + + )} + + ); +} + +function CodeChip({ x, y, lines, stripe = colors.TEAL, opacity = 1, fs = 14, note }: { + x: number; y: number; lines: string[]; stripe?: string; opacity?: number; fs?: number; note?: string; +}) { + if (opacity <= 0.005) return null; + const adv = fs === 14 ? CH14 : CH13; + const lh = fs + 4; + const w = Math.max(...lines.map((l) => l.length)) * adv + 26; + const h = lines.length * lh + 12; + return ( + + + + {lines.map((l, i) => ( + + {l.replace(/^ +/, (m) => ' '.repeat(m.length))} + + ))} + {note && ( + + {note} + + )} + + ); +} + +function BitDisc({ x, y, b, r = 11, opacity = 1 }: { x: number; y: number; b: number; r?: number; opacity?: number }) { + if (opacity <= 0.005) return null; + return ( + + = 1 ? colors.WARM : 'none'} stroke={colors.WARM} strokeWidth={2} /> + = 1 ? '#0a0e1a' : colors.WARM} fontSize={13} fontFamily={MONO} fontWeight={700}> + {b} + + + ); +} + +function LensRing({ x, y, r, opacity = 1, leader }: { x: number; y: number; r: number; opacity?: number; leader?: [number, number] }) { + if (opacity <= 0.005) return null; + const s = 0.7 + 0.3 * opacity; + return ( + + + {leader && } + + ); +} + +function ClockIcon({ x, y, s, color }: { x: number; y: number; s: number; color: string }) { + return ( + + + + + + ); +} +function RippleIcon({ x, y, s, color }: { x: number; y: number; s: number; color: string }) { + return ( + + + + + + + ); +} +function LoopIcon({ x, y, s, color }: { x: number; y: number; s: number; color: string }) { + const r = 13 * s; + const a0 = -Math.PI / 2 + 0.5; + const a1 = -Math.PI / 2 + 2 * Math.PI - 0.35; + const p0 = [x + r * Math.cos(a0), y + r * Math.sin(a0)]; + const p1 = [x + r * Math.cos(a1), y + r * Math.sin(a1)]; + return ( + + + + + ); +} +const ICONS = [ClockIcon, RippleIcon, LoopIcon]; +const ICON_WORDS = ['wait', 'listen', 'loop']; + +function Cross({ x, y, r, u }: { x: number; y: number; r: number; u: number }) { + if (u <= 0.01) return null; + const a = clamp01(u * 2); + const b = clamp01(u * 2 - 1); + return ( + + + {b > 0 && } + + ); +} +function Tick({ x, y, s, opacity = 1 }: { x: number; y: number; s: number; opacity?: number }) { + return ( + + ); +} + +/** ▲ event evaluation · ● frame evaluation · ◆ settle check */ +function EvalMark({ kind, x, y, r = 6, color, opacity = 1 }: { kind: 'event' | 'frame' | 'settle'; x: number; y: number; r?: number; color?: string; opacity?: number }) { + if (opacity <= 0.005) return null; + if (kind === 'frame') return ; + if (kind === 'settle') + return ; + return ; +} + +function Chip({ x, y, text, color, opacity = 1, fs = 13, anchor = 'start', mono = false }: { + x: number; y: number; text: string; color: string; opacity?: number; fs?: number; anchor?: 'start' | 'middle' | 'end'; mono?: boolean; +}) { + if (opacity <= 0.005) return null; + const w = text.length * (mono ? (fs === 14 ? CH14 : CH13) : fs * 0.56) + 20; + const x0 = anchor === 'start' ? x : anchor === 'middle' ? x - w / 2 : x - w; + return ( + + + + {text} + + + ); +} + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +export function Render({ s }: { s: SceneState }) { + const cam = s.get(scene.cam); + const srcIdx = Math.max(0, Math.min(SOURCES.length - 1, Math.round(s.get(scene.src)))); + + const opTL = s.get(scene.opTL); + const opTable = s.get(scene.opTable); + const opProg = s.get(scene.opProg); + const opNext = s.get(scene.opNext); + const opBottom = s.get(scene.opBottom); + const rulerFocus = s.get(scene.rulerU) * s.get(scene.rulersOp); + + // anything a camera push carries toward a stage edge (or up under the header) fades out before it is cut + const edgeFit = (x0: number, x1: number, y0: number, y1: number): number => { + const sx = (x: number) => (x - cam.x) * cam.k + STAGE_W / 2; + const sy = (y: number) => (y - cam.y) * cam.k + STAGE_H / 2; + const r = 6; + return clamp01(sx(x0) / r) * clamp01((STAGE_W - sx(x1)) / r) * clamp01((sy(y0) - HEADER_H) / r) * clamp01((STAGE_H - sy(y1)) / r); + }; + const sandOp = (1 - 0.93 * s.get(scene.cmpMode)) * (1 - 0.6 * rulerFocus); + + const introU = s.get(scene.introU); + const crossU = s.get(scene.crossU); + const hostIconsU = s.get(scene.hostIconsU); + const sandU = s.get(scene.sandU); + const tableU = s.get(scene.tableU); + const pageU = s.get(scene.pageU); + const pressU = s.get(scene.pressU); + const q1U = s.get(scene.q1U); + const q2U = s.get(scene.q2U); + const drainU = s.get(scene.drainU); + const axisU = s.get(scene.axisU); + const framesU = s.get(scene.framesU); + const eventsU = s.get(scene.eventsU); + const afU = s.get(scene.afU); + const legOp = s.get(scene.legOp); + const marks = [s.get(scene.leg1), s.get(scene.leg2), s.get(scene.leg3)]; // marker reveal (stays on) + const legs = marks.map((v) => v * legOp); // legend chips (fade in B3) + const b3U = s.get(scene.b3U); + const combU = s.get(scene.combU); + const settleU = s.get(scene.settleU); + const pickU = s.get(scene.pickU); + const writeU = s.get(scene.writeU); + const errU = s.get(scene.errU); + const sweepU = s.get(scene.sweepU); + const nextU = s.get(scene.nextU); + const copyU = s.get(scene.copyU); + const gateU = s.get(scene.gateU); + const ghostU = s.get(scene.ghostU); + const ghostSweepU = s.get(scene.ghostSweepU); + const ghostOutU = s.get(scene.ghostOutU); + const crumbleU = s.get(scene.crumbleU); + const keepU = s.get(scene.keepU); + const ratesU = s.get(scene.ratesU); + const barsU = s.get(scene.barsU); + const eqU = s.get(scene.eqU); + const outU = s.get(scene.outU); + const tickU = s.get(scene.tickU); + const backU = s.get(scene.backU); + const cmpU = s.get(scene.cmpU); + const scanU = s.get(scene.scanU); + const exactU = s.get(scene.exactU); + const curveU = s.get(scene.curveU); + const loupeU = s.get(scene.loupeU); + const chipsU = s.get(scene.chipsU); + const b9Op = s.get(scene.b9Op); + const lanesOp = s.get(scene.lanesOp); + const rulerU = s.get(scene.rulerU); + const dot1U = s.get(scene.dot1U); + const dot2U = s.get(scene.dot2U); + const cornerU = s.get(scene.cornerU); + const dot3U = s.get(scene.dot3U); + const dot4U = s.get(scene.dot4U); + const codeH = s.get(scene.codeH); + const rulersOp = s.get(scene.rulersOp); + const finalU = s.get(scene.finalU); + + // ---- table values: before → host writes (per group) → copied next values (per cell) ---- + const copyLand = (i: number): number => clamp01(copyU * 2 - (i / 19) * 0.8); // per-cell flight 0..1 + const tableValue = (i: number): number => { + if (copyLand(i) >= 1) return NEXT[i]; + const gi = WRITE_GROUP[i]; + if (gi >= 0 && writeU >= gi + 0.35) return T_WRITTEN[i]; + return T_BEFORE[i]; + }; + const writeFlash = (i: number): number => { + const gi = WRITE_GROUP[i]; + if (gi < 0) return 0; + const d = writeU - (gi + 0.35); + return d < 0 ? 0 : Math.max(0, 1 - d / 1.2); + }; + const copyFlash = (i: number): number => { + const l = copyLand(i); + if (l < 1) return 0; + const landAt = (1 + (0.8 * i) / 19) / 2; + return clamp01(1 - (copyU - landAt) * 4) * (1 - ghostU) * (CHANGED[i] ? 1 : 0.35); + }; + + // ---- program block geometry: BIG (intro) → SLOT (sandwich) ---- + const pb = { + x: lerp(BIG.x, SLOT.x, sandU), + y: lerp(BIG.y, SLOT.y, sandU), + w: lerp(BIG.w, SLOT.w, sandU), + h: lerp(BIG.h, SLOT.h, sandU), + }; + const pbc = { x: pb.x + pb.w / 2, y: pb.y + pb.h / 2 }; + const iconScale = lerp(1.5, 1, sandU); + const iconOp = crossU; + const stackOp = lerp(0.3, 1, clamp01(1 - crossU)) * (crossU > 0.05 ? 0.35 : 1); + + // the whole program block also whispers in comparator mode, but returns in the finale + const progOp = introU * opProg; + + // ---- mini toggle state (page): only outputs move it ---- + const tickIdx = Math.min(3, Math.floor(tickU + 1e-6)); + const tickFrac = clamp01((tickU - tickIdx) * 3); + const thumbPx = outU > 0.5 ? lerp(OUT_FRAMES[Math.max(0, tickIdx - 1)]?.thumb ?? 0, OUT_FRAMES[tickIdx].thumb, tickIdx === 0 ? 1 : tickFrac) : 0; + const pageOn = outU > 0.5 ? 1 : 0; + const pointer = (() => { + // press at (790, 568), lift, release at the same place + if (pressU <= 0) return null; + const p1 = clamp01(pressU); + const p2 = clamp01(pressU - 1); + const px = lerp(900, 792, p1); + const py = lerp(470, 566, p1); + const ring = pressU < 1.02 ? p1 : p2; + return { x: px, y: py, ring, up: pressU > 1.5 }; + })(); + + // the code chips at the bottom left (C3c–C3e) sit over the queue: hide it rather than ghost it behind them + const codeCover = Math.max(s.get(scene.codeC), s.get(scene.codeD), s.get(scene.codeE)); + + const queueFit = edgeFit(QUEUE.x, QUEUE.x + QUEUE.w, QUEUE.y - 22, QUEUE.y + QUEUE.h); + + // ---- the 1/64 px rulers: each fades whole once the other push would cut it ---- + const leftFit = edgeFit(rlx(613.96875) - 30, rlx(614.03125) + 40, 348, RL.y + 30); + const cornerFit = edgeFit(600, 1220, 240, RL.y + 30); + + // ---- comparator scan ---- + const scanI = Math.floor(scanU); + + const chip = SOURCES[srcIdx]; + + return ( + <> + + + {/* ======================== B0 · the host ring (before the sandwich) ======================== */} + {introU > 0.01 && sandU < 0.99 && ( + + + + host: JavaScript around the program + + {ICONS.map((Icon, k) => { + const x = 230; + const y = 214 + k * 110; + return ( + + + + + {ICON_WORDS[k]} + + + ); + })} + + )} + + + {/* ======================== host bands ======================== */} + {sandU > 0.01 && ( + + {[TOP, BOT].map((b, k) => ( + + + + host · JavaScript + + + ))} + + )} + + {/* ======================== top band: the planner's timeline ======================== */} + {axisU > 0.01 && ( + + + {FRAME_MS.map((ms, k) => { + const u = clamp01(framesU * FRAME_MS.length - k); + if (u <= 0) return null; + const dim = ms > DIM_AFTER ? 0.3 : 1; + const x = TX(ms); + return ( + + + {ms <= 1144 && ( + + {ms > DIM_AFTER ? '' : `${ms}`} + + )} + {/* ● frame evaluation */} + + + ); + })} + {/* events ▼ */} + {[ + { ms: 1045, label: 'down 1045' }, + { ms: 1061, label: 'up 1061' }, + { ms: 1125, label: '' }, + { ms: 1141, label: '' }, + ].map((e) => { + const dim = e.ms > DIM_AFTER - 1 ? 0.3 : 1; + const x = TX(e.ms); + const yy = lerp(AXIS_Y - 40, AXIS_Y - 14, eventsU); + return ( + + + {e.label && ( + + {e.label} + + )} + + ); + })} + {/* advanceFrame bracket over the frame at 1048 */} + {afU > 0.01 && ( + + + + advanceFrame plans the frame at 1048 + + + )} + {/* ▲ event evaluations · ◆ settle check */} + {[1045, 1061].map((ms) => ( + + ))} + + + + {/* legend */} + + + + + {/* B3: 3 ms brackets + two evaluations */} + {b3U > 0.01 && + [ + [1045, 1048], + [1061, 1064], + ].map(([a, b]) => ( + + + + 3 ms · 2 evaluations + + + ))} + {/* B3: 4 ms spring steps from the release; first settle check */} + {combU > 0.01 && ( + + {Array.from({ length: 17 }, (_, k) => 1061 + 4 * k).map((ms, k) => + k / 16 <= combU ? : null, + )} + + 4 ms spring steps from the release · 4 per frame (derived) + + + )} + {settleU > 0.01 && ( + + + + first settle check, 64 ms after the release (derived) + + + )} + + )} + + {/* B4: the evaluation we follow */} + {pickU > 0.01 && ( + + + + + the evaluation at 1061 ms + + + )} + + {/* ======================== the table ======================== */} + {tableU > 0.01 && ( + + {CELLS.map((c, i) => { + const u = clamp01(tableU * 1.6 - (i / 19) * 0.6); + return ( + + ); + })} + {GROUPS.map((gr) => { + const x0 = cellX(gr.from); + const x1 = cellX(gr.to) + CELL_W; + const yb = TABLE_Y + CELL_H + 8; + return ( + + {gr.to > gr.from && } + + {gr.label} + + + ); + })} + + table + + {/* B6: the table is untouched by the frame's own evaluation */} + {keepU > 0.01 && ( + + + + + )} + + )} + + {/* B4: write arrows from the host band into the cells */} + {writeU > 0.01 && writeU < WRITES.length + 2.5 && ( + + {WRITES.map((w, gi) => { + const u = win(writeU, gi, gi + 0.35); + const fade = 1 - win(writeU, gi + 1.2, gi + 1.8); + if (u <= 0 || fade <= 0) return null; + const cxs = w.cells.map((ci) => cellX(ci) + CELL_W / 2); + const mid = (Math.min(...cxs) + Math.max(...cxs)) / 2; + const callOp = win(writeU, gi + 0.05, gi + 0.3) * (1 - win(writeU, gi + 0.85, gi + 1.0)); + const label = ( + + ); + return [label, ...w.cells.map((ci) => { + const x = cellX(ci) + CELL_W / 2; + return ( + + + {u > 0.9 && } + + ); + })]; + })} + + )} + + {/* ======================== the program block ======================== */} + {progOp > 0.01 && ( + + + + program · a fixed list of operations (layers, schematic) + + {/* layer stack */} + + {STACK.map((n, k) => { + const x = pb.x + 40 + (k * (pb.w - 80)) / (STACK.length - 1); + const sweepX = pb.x + 20 + (pb.w - 40) * Math.max(sweepU, ghostSweepU); + const lit = (sweepU > 0.001 || ghostSweepU > 0.001) && x < sweepX; + const sc = lerp(1.3, 1, sandU); + return Array.from({ length: n }, (_, j) => ( + + )); + })} + {(sweepU > 0.001 && sweepU < 0.999) || (ghostSweepU > 0.001 && ghostSweepU < 0.999) ? ( + + ) : null} + + {sweepU > 0.01 && ( + + evaluate(values): every operation, every time + + )} + {ghostSweepU > 0.01 && ghostSweepU < 1.5 && ( + + the frame at 1064: evaluated on the copy + + )} + {/* crossed icons inside */} + {iconOp > 0.01 && ( + + {ICONS.map((Icon, k) => { + const x = pbc.x + (k - 1) * 150 * iconScale; + const y = pbc.y - 2 * iconScale; + return ( + + + + + + ); + })} + + {finalU > 0.5 ? 'inside: no loop · no early exit' : 'inside: no waiting · no listening · no loop'} + + + )} + + )} + + {/* B4: the grid refusal */} + {errU > 0.01 && ( + + + + + down.x must be a multiple of 0.015625 + + + off the 1/64 px grid is refused, not rounded + + + )} + + {/* ======================== next values slot ======================== */} + {sandU > 0.01 && ( + + {CELLS.map((c, i) => { + const u = clamp01(nextU * 1.6 - (i / 19) * 0.6); + return ( + + + {u > 0.01 && } + + ); + })} + {ghostU > 0.5 && crumbleU < 0.5 ? ( + + copy + + ) : ( + <> + + next + + + values + + + )} + {nextU > 0.3 && ( + + {[5, 9, 10].map((i) => ( + + {CELLS[i].name} + + ))} + + )} + + )} + {nextU > 0.5 && copyU < 0.02 && ( + + )} + + {/* B5: copy the next values back over the table */} + {copyU > 0.001 && copyU < 0.999 && ( + + {CELLS.map((c, i) => { + const l = copyLand(i); + if (!CHANGED[i] || l <= 0 || l >= 1) return null; + const y = lerp(NEXT_Y, TABLE_Y, ease.move(l)); + return ; + })} + + source.set(result.nextValues) + + + )} + {/* B5: the copy-back arrow, gated on evaluation.writesBack (toggle.ts:130); 1061 is an event evaluation */} + {gateU > 0.01 && ( + + + + + + an event evaluation + + + )} + + {/* B6: the ghost copy (copy.set(source)) — evaluated, then discarded */} + {ghostU > 0.01 && crumbleU < 0.999 && ( + + {CELLS.map((c, i) => { + const fall = clamp01(crumbleU * 1.5 - ((i * 7) % 20) / 40); + const y = lerp(TABLE_Y, NEXT_Y, ease.move(clamp01(ghostU * 1.4 - (i / 19) * 0.4))) + fall * 26; + return ( + + ); + })} + + )} + {ghostOutU > 0.01 && ( + + + + outputs → page + + + )} + + {/* ======================== bottom band: queue + page ======================== */} + {sandU > 0.01 && ( + + + + queue + + + {[ + { u: q1U, x: QUEUE.x + 10, text: 'down · 640, 400 · 1045' }, + { u: q2U, x: QUEUE.x + 226, text: 'up · 640, 400 · 1061' }, + ].map((q, k) => + q.u > 0.01 ? ( + + + + {q.text} + + + ) : null, + )} + {drainU > 0.5 && ( + + queued = [] (handed to advanceFrame) + + )} + + {/* the page */} + {pageU > 0.01 && ( + + + page + + + + + {pointer && drainU < 0.99 && ( + + + {pointer.ring > 0 && pointer.ring < 1 && ( + + )} + + {pointer.up ? 'release' : 'press'} + + + )} + {pressU > 1.6 && drainU < 0.99 && ( + + nothing changes yet + + )} + + )} + + )} + + {/* B7: six outputs slide out of the program into the page */} + {outU > 0.01 && ( + + {[ + { name: 'trackLeft', v: '614' }, + { name: 'thumbOffset', v: (outU > 0.5 ? OUT_FRAMES[tickIdx].thumb : 0).toFixed(2) }, + { name: 'trackTop', v: '384' }, + { name: 'on', v: '1' }, + { name: 'active', v: '1' }, + { name: 'armed', v: '0' }, + ].map((o, k) => { + const col = k % 2; + const row = Math.floor(k / 2); + const tx = OUT_CHIP.x + col * OUT_CHIP.dx; + const ty = OUT_CHIP.y + row * OUT_CHIP.dy; + const u = ease.move(clamp01(outU * 1.5 - k * 0.1)); + const x = lerp(SLOT.x + SLOT.w - 130, tx, u); + const y = lerp(SLOT.y + 50 + k * 8, ty, u); + const live = k === 1 && tickU > 0.01; + return ( + + + + {o.name} + {o.v} + + + ); + })} + + frame {OUT_FRAMES[tickIdx].timeMs} ms · thumb offset per frame (derived) + + + )} + {backU > 0.01 && ( + + + {backU > 0.9 && } + + events only + + {/* the page push fades the queue box (it would be cut at the left edge): name the arrow's target instead */} + + queue + + + )} + + {/* B11: the host's three abilities, back in the bands */} + {finalU > 0.01 && ( + + {[ + { k: 0, x: 470, y: TOP.y + 64 }, + { k: 2, x: 810, y: TOP.y + 64 }, + { k: 1, x: 640, y: BOT.y + 56 }, + ].map(({ k, x, y }) => { + const Icon = ICONS[k]; + return ( + + + + + + {ICON_WORDS[k]} + + + ); + })} + + )} + + {/* ======================== B6 part 2: four frame rates, one stored table ======================== */} + {ratesU > 0.01 && ( + + + + illustration: same events, four frame rates + + {' · '}the rule of behavior-choices.md:48; replay-tested in window-manager.test.ts + + + + stored table (20 cells) + + {[120, 60, 30, 10].map((fps, k) => { + const y = 368 + k * 34; + const X = (ms: number) => 270 + ((ms - 1000) * 500) / 180; + const pitch = 1000 / fps; + const ticks = Array.from({ length: Math.floor(180 / pitch) + 1 }, (_, j) => 1000 + j * pitch); + const lu = clamp01(ratesU * 2 - 0.4 - k * 0.15); + return ( + + + {fps} fps + + + {ticks.map((ms) => ( + + ))} + {[1045, 1061].map((ms) => ( + + ))} + {/* barcode of the stored table */} + {CELLS.map((c, i) => { + const bu = clamp01(barsU * 20 - i); + if (bu <= 0) return null; + const v = NEXT[i]; + return ; + })} + {k > 0 && eqU > 0.01 && ( + + = + + )} + + ); + })} + + ▼ events at 1045 and 1061 ms · teal: frame ticks · barcode lit = non-zero cell + + + )} + + {/* B3: the settle-check interval (above the dimmed table) */} + {settleU > 0.01 && ( + + + + + derived: stepMs 4 × settleCheckSteps 16 + + + )} + + {/* ======================== world code chips ======================== */} + + + + + + + + + {/* ======================== B8–B9 · the two-lane comparator ======================== */} + {cmpU > 0.01 && ( + + + {[ + { y: LANE_A, title: 'toggle.ts — the program, 32-bit floats', color: colors.TEXT }, + { y: LANE_B, title: 'toggle-reference.ts — plain JavaScript, 64-bit floats', color: colors.MUTED }, + ].map((lane, li) => ( + + + {lane.title} + + {['on', 'armed'].map((row, ri) => ( + + + {row} + + + {REF.map((r, i) => { + const b = ri === 0 ? r.on : r.armed; + if (!b) return null; + return ; + })} + + ))} + {li === 0 && + REF.map((r, i) => + r.hasEvent ? : null, + )} + + ))} + {/* green agreement ticks as the scan passes */} + {REF.map((_, i) => + i <= scanI ? : null, + )} + {scanU >= 0 && scanU < N_FRAMES - 1 + 0.5 && ( + + = N_FRAMES - 1 ? 0 : 0.9} /> + + )} + + scripted trace, frame by frame →{scanI >= N_FRAMES - 1 ? ' every frame equal' : ''} + + {exactU > 0.01 && ( + + + exactly equal every frame: on · armed · trackLeft · trackTop + + + )} + + + {/* B9: the thumb curve and the loupe */} + {curveU > 0.01 && ( + + + thumbOffset per frame, from toggle-reference.ts (derived) · one line stands for both lanes + + + + + 0 + + + 20 + + `${i ? 'L' : 'M'} ${fx(i) + FW / 2} ${cy(r.thumb)}`) + .join(' ')} + fill="none" stroke={colors.WARM} strokeWidth={2.4} strokeLinejoin="round" + /> + {loupeU > 0.01 && ( + + + + + + + + + {'< 0.00001 px'} + + + program (FP32) + + + reference (float64) + + + magnified, schematic: the gap is too thin to draw + + + + )} + {chipsU > 0.01 && ( + + + + + + + )} + + )} + + {/* B10: the 1/64 px rulers */} + {rulerU > 0.01 && ( + + {/* left edge */} + + + the left edge, x = 614 · ticks every 1/64 px + + + + track + + + {[-2, -1, 0, 1, 2].map((k) => { + const v = 614 + k / 64; + const x = rlx(v); + const u = clamp01(rulerU * 5 - (k + 2)); + return ( + + + + {v} + + + ); + })} + + + {dot1U > 0.01 && ( + + + + 613.984375 + + + no arm · armed + + + + )} + {dot2U > 0.01 && ( + + + + 614, 384 + + + arms · armed + + + + )} + + {/* bottom-right corner */} + + + the bottom right corner · x < 666 and y < 416 are inside + + + + + + track + + + {[-2, -1, 0, 1, 2].map((k) => { + const v = 666 + k / 64; + const x = rcx(v); + return ( + + + + {v} + + + ); + })} + + x = 666 · y = 416 + + + {dot3U > 0.01 && ( + + + + 666, 400 + + + on the right edge: outside · only disarms + + + (y = 400 lies far above this window) + + + )} + {dot4U > 0.01 && ( + + + + 665.984375, 415.984375 + + + 1/64 px inside the corner · flips · on + + + + )} + + )} + + )} + + + {/* ======================== screen-space: C3h, title, source chip ======================== */} + {codeH > 0.01 && ( + + + + )} + + + 3 · The Host Loop + + + + + {chip} + + + + ); +} + +export const vizScene = () => scene; + +// keep THUMB_MAX referenced for the module-scope sanity check (the travel never exceeds the 40 px state bound) +if (THUMB_MAX > 40) throw new Error('handwired ch3: thumb offset out of the declared bounds'); diff --git a/apps/bookshelf/src/viz/books/handwired/chapter-4.stories.tsx b/apps/bookshelf/src/viz/books/handwired/chapter-4.stories.tsx new file mode 100644 index 00000000..c3b71af7 --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired/chapter-4.stories.tsx @@ -0,0 +1,28 @@ +// Handwired — chapter 4 story. Scene authored by Claude Opus 5.5 (claude-opus-5-5). +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Player } from '../../core'; +import { Render, vizScene } from './chapter-4'; + +/** + * Loops That Cannot Stop — a findIndex cursor stops early while the program tests every row at + * once; the same row list becomes a count, a chain then a tree, a rank matrix and a table read, + * while a log–log scatter of operations against layers records each trade. + */ +function Chapter4() { + const scene = vizScene(); + return ( +
+ + {(s) => } + +
+ ); +} + +const meta: Meta = { + title: 'Books/Handwired/Chapter 4', + component: Chapter4, +}; +export default meta; + +export const Scene: StoryObj = {}; diff --git a/apps/bookshelf/src/viz/books/handwired/chapter-4.tsx b/apps/bookshelf/src/viz/books/handwired/chapter-4.tsx new file mode 100644 index 00000000..ab20369b --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired/chapter-4.tsx @@ -0,0 +1,1343 @@ +// Loops That Cannot Stop +// +// Handwired — chapter 4. Source pin: chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46 (MIT, Cheng Lou) +// docs/README.md:9 a loop becomes a count, a tree, a table, or the same work on every item +// docs/patterns.md:3, 7 every operation runs on every frame; CPU time follows operations +// docs/patterns.md:34, 39 when a tree gives the loop's answer; integers below 2^24 (≈ 16.8 million) add exactly in any grouping +// docs/patterns.md:50 prefix-network table (Brent-Kung: 2,036 combines in 18 levels) +// docs/patterns.md:54-55 CPU executor vs GPU dispatch per tree level +// docs/patterns.md:65 a count or a tree picks one item +// docs/patterns.md:69-82 findIndex (code 72, bits 75-77, cost 81, monotone caveat 82) +// docs/patterns.md:175-194 running totals (diagram 184-190, Brent-Kung network 192, cost 193) +// src/runtime/prefix-networks.ts:30 brentKungPrefix (every running total; 2,036 operations, 18 layers at 1,024 rows) +// docs/patterns.md:356-374 sort as ranks (code 361, rank table 366-371, cost 374: keys on a grid) +// docs/patterns.md:456-470 tables JavaScript writes (code 461-462, table 465-466, cost 470) +// +// Authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan. Captions are the fixed +// narration contract (verbatim). Every number below is computed at module scope from the doc's own +// examples and asserted against the doc's printed values; scatter points are the sizes the repo quotes. +import type { ReactNode } from 'react'; +import { CAMERA_HOME, Camera, MathLabel, STAGE_H, STAGE_W, Timeline, cameraInterp, colors, ease } from '../../core'; +import type { CameraState, ChannelRef, SceneState } from '../../core'; +import { MatrixGrid } from '../../primitives'; + +const clamp01 = (u: number): number => (u < 0 ? 0 : u > 1 ? 1 : u); +const lerp = (a: number, b: number, u: number): number => a + (b - a) * u; +const MONO = colors.font.mono; +const UI = colors.font.ui; +const INK = '#e6edf6'; +const DARK = '#08101f'; + +function assert(cond: boolean, msg: string): void { + if (!cond) throw new Error(`handwired ch4: ${msg}`); +} + +// --------------------------------------------------------------------------- +// The arithmetic (module scope; checked against docs/patterns.md) +// --------------------------------------------------------------------------- + +// findIndex (patterns.md:72-77) +const HEIGHTS = [40, 40, 0, 60, 40]; +const BOTTOMS = HEIGHTS.reduce((acc, h) => [...acc, (acc.length ? acc[acc.length - 1] : 0) + h], []); +const TOPS = BOTTOMS.map((b, i) => b - HEIGHTS[i]); +const POINTER_Y = 90; +const BITS = BOTTOMS.map((b) => (b <= POINTER_Y ? 1 : 0)); +const COUNT: number = BITS.reduce((p, c) => p + c, 0); +const LOOP_INDEX = BOTTOMS.findIndex((rowBottom) => POINTER_Y < rowBottom); +assert(BOTTOMS.join(' ') === '40 80 80 140 180', 'row bottoms'); +assert(BITS.join(' ') === '1 1 1 0 0', 'findIndex bits'); +assert(COUNT === 3 && LOOP_INDEX === 3, 'count equals loop index'); + +// the monotone caveat (patterns.md:82): a height of −10 px +const EX_HEIGHTS = [40, -10, 50]; +const EX_BOTTOMS = EX_HEIGHTS.reduce((acc, h) => [...acc, (acc.length ? acc[acc.length - 1] : 0) + h], []); +const EX_TOPS = EX_BOTTOMS.map((b, i) => b - EX_HEIGHTS[i]); +const EX_Y = 35; +const EX_BITS = EX_BOTTOMS.map((b) => (b <= EX_Y ? 1 : 0)); +const EX_COUNT: number = EX_BITS.reduce((p, c) => p + c, 0); +const EX_LOOP = EX_BOTTOMS.findIndex((b) => EX_Y < b); +assert(EX_BOTTOMS.join(' ') === '40 30 80' && EX_BITS.join(' ') === '0 1 0', 'caveat bits'); +assert(EX_COUNT === 1 && EX_LOOP === 0, 'caveat: count 1, loop 0'); + +// running totals (patterns.md:181-189): the first four heights +const H4 = HEIGHTS.slice(0, 4); +const CHAIN = H4.reduce((acc, h) => [...acc, (acc.length ? acc[acc.length - 1] : 0) + h], []); +const PAIR01 = H4[0] + H4[1]; +const PAIR23 = H4[2] + H4[3]; +const TREE = [H4[0], PAIR01, PAIR01 + H4[2], PAIR01 + PAIR23]; +assert(CHAIN.join(' ') === '40 80 80 140' && TREE.join(' ') === CHAIN.join(' '), 'chain and tree totals agree'); +assert(CHAIN.join(' ') === BOTTOMS.slice(0, 4).join(' '), 'totals are the first four row bottoms'); + +// ranks (patterns.md:361-371) +const KEYS = [30, 10, 30, 20]; +const before = (i: number, j: number): number => (j !== i && (KEYS[j] < KEYS[i] || (KEYS[j] === KEYS[i] && j < i)) ? 1 : 0); +const RANK_BITS = KEYS.map((_, i) => KEYS.map((__, j) => before(i, j))); +const RANKS = RANK_BITS.map((row) => row.reduce((p, c) => p + c, 0)); +const ORDER = KEYS.map((_, index) => index).sort((left, right) => KEYS[left] - KEYS[right] || left - right); +assert(RANK_BITS.map((r) => r.join('')).join(' ') === '0101 0000 1101 0100', 'rank table'); +assert(RANKS.join(' ') === '2 0 3 1' && ORDER.join(' ') === '1 3 0 2', 'ranks and sorted order'); +assert(ORDER.every((item, r) => RANKS[item] === r), 'ranks place each item'); + +// the table JavaScript writes (patterns.md:461-466) +const DEPTHS = [0, 1, 2, 1, 2, 3]; +const LEVELS = 4; +const ANCESTOR = DEPTHS.map((_, row) => + Array.from({ length: LEVELS }, (__, level) => { + let ancestor = row; + while (ancestor > 0 && DEPTHS[ancestor] > level) ancestor--; + return ancestor; + }), +); +const READ = { row: 5, level: 2 }; +const READ_CELL = READ.row * LEVELS + READ.level; +assert(ANCESTOR[3].join(' ') === '0 3 3 3' && ANCESTOR[5].join(' ') === '0 3 4 5', 'ancestor rows 3 and 5'); +assert(READ_CELL === 22 && ANCESTOR.flat()[READ_CELL] === 4, 'read row 5, level 2 = cell 22 = 4'); + +// --------------------------------------------------------------------------- +// Layout (stage 1280×720; y ≥ 630 is the caption band) +// --------------------------------------------------------------------------- + +const CODE = { x: 40, y: 82 }; + +// row list (vertical ruler, 1.6 px per unit: 0..180 → y 170..458) +const RULER_X = 150; +const yOf = (u: number): number => 170 + u * 1.6; +const BLK = { x: 160, w: 130 }; +const ROW_CY = BOTTOMS.map((b, i) => yOf((b + TOPS[i]) / 2)); +const NEEDLE_Y = yOf(POINTER_Y); +const CURSOR_X = 62; + +// program panel +const PANEL = { x: 340, y: 150, w: 430, h: 336 }; +const TILE = { x: 360, w: 180, h: 26 }; +const DISC_X = 566; +const SIGMA = { x: 705, y: 236 }; + +// menu bar (four answers) +const MENU = [ + { x: 40, w: 150, label: 'count' }, + { x: 210, w: 150, label: 'tree' }, + { x: 380, w: 150, label: 'table' }, + { x: 550, w: 230, label: 'same work on every item' }, +]; +const MENU_Y = 572; + +// running totals: bars and nodes +const COL_X = [200, 320, 440, 560]; +const BAR_BASE = 262; +const BAR_SCALE = 1.2; +const BAR_W = 64; +const LAYER_Y = [318, 368, 418, 468]; +const NODE = { w: 104, h: 30 }; + +// ranks +const RG = { x: 200, y: 214, cell: 52, gap: 8 }; +const RG_PITCH = RG.cell + RG.gap; +const CARD = { w: 52, h: 44, y: 146 }; +const SORTED_X = 640; + +// ancestor table +const AT = { x: 340, y: 192, cell: 32, pitch: 38 }; +const LIST = { x: 60, y: 192, pitch: 38, h: 30, w: 130, indent: 22 }; + +// the work-vs-waiting scatter (log–log): x 850..1210, y 130..540 +const SC = { x0: 850, x1: 1210, y0: 540, y1: 130 }; +const LX = [-0.2, 4.3]; +const LY = [-0.2, 3.3]; +const sx = (ops: number): number => SC.x0 + ((Math.log10(ops) - LX[0]) / (LX[1] - LX[0])) * (SC.x1 - SC.x0); +const sy = (layers: number): number => SC.y0 - ((Math.log10(layers) - LY[0]) / (LY[1] - LY[0])) * (SC.y0 - SC.y1); +type PointId = 'T' | 'C' | 'H' | 'B' | 'S' | 'R'; +interface ScatterPoint { + id: PointId; + ops: number; + layers: number; + name: string; + counter: string; + solid: boolean; + lx: number; + ly: number; + anchor: 'start' | 'middle' | 'end'; +} +const POINT_DATA: Array & { dx: number; dy: number; anchor: ScatterPoint['anchor'] }> = [ + { id: 'T', ops: 447, layers: 34, name: 'tree, 64 rows', counter: '447 · 34', solid: false, dx: -12, dy: -4, anchor: 'end' }, + { id: 'C', ops: 195, layers: 3, name: 'count, 64 rows', counter: '195 · 3', solid: true, dx: -10, dy: -24, anchor: 'end' }, + { id: 'H', ops: 1023, layers: 1023, name: 'chain, 1,024 rows', counter: '1,023 · 1,023', solid: false, dx: -12, dy: -4, anchor: 'end' }, + { id: 'B', ops: 2036, layers: 18, name: 'Brent-Kung, 1,024 rows', counter: '2,036 · 18', solid: true, dx: 0, dy: 23, anchor: 'middle' }, + { id: 'S', ops: 4096, layers: 3, name: 'rank sums, 64 items', counter: '4,096 · 3', solid: true, dx: -4, dy: -24, anchor: 'middle' }, + { id: 'R', ops: 1, layers: 1, name: 'one table read', counter: '1 · 1', solid: true, dx: 14, dy: -4, anchor: 'start' }, +]; +const POINTS: Record = Object.fromEntries( + POINT_DATA.map((p) => { + const px = sx(p.ops); + const py = sy(p.layers); + return [p.id, { ...p, px, py, lx: px + p.dx, ly: py + p.dy }]; + }), +) as unknown as Record; +for (const p of Object.values(POINTS)) { + assert(p.px > SC.x0 && p.px < SC.x1 && p.py < SC.y0 && p.py > SC.y1, `point ${p.id} inside the box`); +} +assert(POINTS.H.py < POINTS.T.py && POINTS.T.py < POINTS.B.py && POINTS.B.py < POINTS.C.py, 'layer order H > T > B > C'); +assert(Math.abs(POINTS.C.py - POINTS.S.py) < 1e-9 && POINTS.R.px < POINTS.C.px && POINTS.C.px < POINTS.T.px, 'x order'); +const X_TICKS = [1, 10, 100, 1000, 10000]; +const Y_TICKS = [1, 10, 100, 1000]; +const tickLabel = (v: number): string => v.toLocaleString('en-US'); + +// cameras +const CAM_ROWS: CameraState = { x: 410, y: 300, k: 1.22 }; +const CAM_TREE: CameraState = { x: 400, y: 309, k: 1.22 }; +const CAM_RANK: CameraState = { x: 420, y: 318, k: 1.18 }; +const CAM_TABLE: CameraState = { x: 410, y: 314, k: 1.2 }; +const CAM_SCATTER: CameraState = { x: 995, y: 388, k: 1.25 }; + +const SOURCES = [ + 'docs/patterns.md:3, 69, 72', + 'docs/README.md:9 · docs/patterns.md:3', + 'docs/patterns.md:72-76', + 'docs/patterns.md:69, 80', + 'docs/patterns.md:81', + 'docs/patterns.md:82', + 'docs/patterns.md:177-182, 193', + 'docs/patterns.md:34, 50, 185-193 · prefix-networks.ts:30', + 'docs/patterns.md:34, 39', + 'docs/patterns.md:358-374', + 'docs/patterns.md:458-470', + 'docs/patterns.md:7, 54-55', + 'docs/patterns.md:65', +]; + +// verbatim code chips (≤ 3 lines) +const C4A = ['const pointedRow = rowBottoms.findIndex(rowBottom => pointerY < rowBottom)']; +const C4B = [ + 'rowBottom 40 80 80 140 180 pointerY = 90', + '[rowBottom <= pointerY] 1 1 1 0 0 their count, 3, is the pointed row', +]; +const C4C = ['let top = 0', 'for (const height of heights) { rowTops.push(top); top += height }']; +const C4E = [ + 'const order = items.map((_, index) => index)', + ' .sort((left, right) => keys[left] - keys[right] || left - right)', +]; +const C4F = ['let ancestor = row', 'while (ancestor > 0 && depths[ancestor] > level) ancestor--']; +const C4G = ['ancestor = ancestorAtLevel[row × 4 + level], 1 operation']; + +// --------------------------------------------------------------------------- +// Timeline — 13 fixed captions, ≈ 123 s +// --------------------------------------------------------------------------- + +export function buildScene() { + const tl = new Timeline(); + const cam = tl.channel('cam', CAMERA_HOME, cameraInterp); + const src = tl.channel('src', 0); + const titleU = tl.channel('titleU', 0); + + // code chips + const codeA = tl.channel('codeA', 0); + const codeB = tl.channel('codeB', 0); + const codeC = tl.channel('codeC', 0); + const codeE = tl.channel('codeE', 0); + const codeF = tl.channel('codeF', 0); + const codeG = tl.channel('codeG', 0); + + // findIndex machine + const rowsU = tl.channel('rowsU', 0); // blocks + ruler draw on + const blocksOp = tl.channel('blocksOp', 1); + const fiOp = tl.channel('fiOp', 1); // needle, cursor, panel, readout + const needleU = tl.channel('needleU', 0); + const curU = tl.channel('curU', 0); + const curRow = tl.channel('curRow', 0); + const checkIdx = tl.channel('checkIdx', 0); + const stopU = tl.channel('stopU', 0); + const row4Dim = tl.channel('row4Dim', 0); + const panelU = tl.channel('panelU', 0); + const flashU = tl.channel('flashU', 0); + const everyU = tl.channel('everyU', 0); + const testU = tl.channel('testU', 0); + const bitsW = tl.channel('bitsW', 0); + const sumU = tl.channel('sumU', 0); + const sigmaU = tl.channel('sigmaU', 0); + const ringU = tl.channel('ringU', 0); + const sameU = tl.channel('sameU', 0); + const flipU = tl.channel('flipU', 0); + const idxTexU = tl.channel('idxTexU', 0); + + // menu + const menuU = tl.channel('menuU', 0); + const m0 = tl.channel('menuCount', 0.35); + const m1 = tl.channel('menuTree', 0.35); + const m2 = tl.channel('menuTable', 0.35); + const m3 = tl.channel('menuSame', 0.35); + + // scatter + const scatU = tl.channel('scatU', 0); + const scatOp = tl.channel('scatOp', 1); + const pC = tl.channel('pC', 0); + const pT = tl.channel('pT', 0); + const pH = tl.channel('pH', 0); + const pB = tl.channel('pB', 0); + const pS = tl.channel('pS', 0); + const pR = tl.channel('pR', 0); + const arrTC = tl.channel('arrTC', 0); + const arrHB = tl.channel('arrHB', 0); + const xGlow = tl.channel('xGlow', 0); + const yGlow = tl.channel('yGlow', 0); + + // caveat example + const exU = tl.channel('exU', 0); + const exBits = tl.channel('exBits', 0); + const exSigma = tl.channel('exSigma', 0); + const exLoop = tl.channel('exLoop', 0); + const exFlag = tl.channel('exFlag', 0); + + // running totals + const barsU = tl.channel('barsU', 0); + const chainU = tl.channel('chainU', 0); + const chainTag = tl.channel('chainTag', 0); + const treeU = tl.channel('treeU', 0); + const exactU = tl.channel('exactU', 0); + const matchU = tl.channel('matchU', 0); // 0..1 tree row, 1..2 loop row + equals + const totalsOp = tl.channel('totalsOp', 1); + + // ranks + const rankU = tl.channel('rankU', 0); + const rankFill = tl.channel('rankFill', 0); + const rankSums = tl.channel('rankSums', 0); + const rankTex = tl.channel('rankTex', 0); + const tieU = tl.channel('tieU', 0); + const fallU = tl.channel('fallU', 0); + const rankOp = tl.channel('rankOp', 1); + + // table + const listU = tl.channel('listU', 0); + const hostU = tl.channel('hostU', 0); + const fillU = tl.channel('fillU', 0); + const readU = tl.channel('readU', 0); + const readTex = tl.channel('readTex', 0); + const glowU = tl.channel('glowU', 0); + const opChipU = tl.channel('opChipU', 0); + + // close + const leftOp = tl.channel('leftOp', 1); + const cardU = tl.channel('cardU', 0); + + const CAPTION_AT = [0.4, 9.9, 18.3, 28.5, 36.5, 46.4, 52.9, 61.6, 71.2, 78.6, 90.2, 100.8, 113.8]; + CAPTION_AT.forEach((at, i) => tl.set(src, i, Math.max(0, at - 0.1))); + const out = (ch: ChannelRef, at: number, dur = 0.5) => + tl.tween(ch, 0, { at, dur, ease: ease.move }); + + // — B0 · the loop stops; the program can't — + tl.caption({ at: 0.4, dur: 9.1, text: 'A Javascript loop can stop early: find the first row that fails, and quit. The program can\'t, because every operation runs on every evaluation.' }); + tl.tween(titleU, 1, { at: 0.4, dur: 0.6, ease: ease.enter }); + tl.tween(codeA, 1, { at: 0.6, dur: 0.6, ease: ease.enter }); + tl.tween(rowsU, 1, { at: 1.0, dur: 1.4, ease: ease.draw }); + tl.tween(cam, CAM_ROWS, { at: 1.4, dur: 1.3, ease: ease.move }); + tl.tween(needleU, 1, { at: 2.2, dur: 0.9, ease: ease.move }); + tl.tween(curU, 1, { at: 2.7, dur: 0.5, ease: ease.enter }); + [1, 2, 3].forEach((r, i) => { + const at = 3.4 + i * 0.7; + tl.tween(curRow, r, { at, dur: 0.4, ease: ease.move }); + tl.set(checkIdx, r, at + 0.4); + }); + tl.tween(stopU, 1, { at: 5.2, dur: 0.5, ease: ease.pop }); + tl.tween(row4Dim, 1, { at: 5.6, dur: 0.6, ease: ease.enter }); + tl.tween(panelU, 1, { at: 6.3, dur: 0.6, ease: ease.enter }); + tl.tween(flashU, 1, { at: 7.2, dur: 0.3, ease: ease.pop }); + tl.tween(flashU, 0, { at: 7.6, dur: 0.6, ease: ease.move }); + tl.tween(everyU, 1, { at: 7.3, dur: 0.5, ease: ease.enter }); + tl.hold(9.5, 0.4); + + // — B1 · four answers — + tl.caption({ at: 9.9, dur: 8, text: 'So a loop becomes one of four things: a count, a tree, a table, or the same work on every item.' }); + tl.tween(cam, CAMERA_HOME, { at: 9.9, dur: 1.1, ease: ease.move }); + tl.tween(fiOp, 0.35, { at: 10.0, dur: 0.6, ease: ease.move }); + tl.tween(blocksOp, 0.35, { at: 10.0, dur: 0.6, ease: ease.move }); + tl.tween(codeA, 0.35, { at: 10.0, dur: 0.6, ease: ease.move }); + tl.tween(menuU, 1, { at: 10.4, dur: 0.7, ease: ease.enter }); + [m0, m1, m2, m3].forEach((m, i) => { + const at = [13.2, 13.9, 14.5, 15.2][i]; + tl.tween(m, 1, { at, dur: 0.3, ease: ease.pop }); + tl.tween(m, 0.35, { at: at + 0.9, dur: 0.5, ease: ease.move }); + }); + tl.hold(17.9, 0.4); + + // — B2 · test every row at once — + tl.caption({ at: 18.3, dur: 9.8, text: 'Which row holds the pointer? Test every row at once, bottom at or above the pointer. At ninety the bits read one, one, one, zero, zero.' }); + tl.tween(cam, CAM_ROWS, { at: 18.3, dur: 1.2, ease: ease.move }); + out(menuU, 18.3, 0.5); + tl.tween(fiOp, 1, { at: 18.4, dur: 0.6, ease: ease.move }); + tl.tween(blocksOp, 1, { at: 18.4, dur: 0.6, ease: ease.move }); + tl.tween(m0, 1, { at: 18.4, dur: 0.6, ease: ease.move }); + out(codeA, 18.5, 0.4); + tl.tween(codeB, 1, { at: 18.8, dur: 0.6, ease: ease.enter }); + tl.tween(curU, 0.25, { at: 18.6, dur: 0.6, ease: ease.move }); + tl.tween(stopU, 0.25, { at: 18.6, dur: 0.6, ease: ease.move }); + tl.tween(everyU, 0, { at: 18.6, dur: 0.5, ease: ease.move }); + tl.tween(testU, 1, { at: 20.6, dur: 0.7, ease: ease.move }); + tl.tween(bitsW, 5.6, { at: 24.0, dur: 0.9, ease: ease.linear }); + tl.hold(28.1, 0.4); + + // — B3 · count the ones — + tl.caption({ at: 28.5, dur: 7.6, text: 'Count the ones: three. The loop returns index three too, because the bits never go back from zero to one.' }); + tl.tween(sumU, 1, { at: 28.8, dur: 1.0, ease: ease.move }); + tl.tween(sigmaU, 1, { at: 29.6, dur: 0.5, ease: ease.pop }); + tl.tween(curU, 1, { at: 30.4, dur: 0.5, ease: ease.move }); + tl.tween(stopU, 1, { at: 30.4, dur: 0.5, ease: ease.move }); + tl.tween(ringU, 1, { at: 30.6, dur: 0.6, ease: ease.enter }); + tl.set(checkIdx, 4, 31.4); + tl.tween(sameU, 1, { at: 31.4, dur: 0.5, ease: ease.pop }); + tl.tween(flipU, 1, { at: 32.8, dur: 0.8, ease: ease.draw }); + tl.tween(idxTexU, 1, { at: 33.6, dur: 0.6, ease: ease.enter }); + tl.hold(36.1, 0.4); + + // — B4 · the scatter: count beats tree — + tl.caption({ at: 36.5, dur: 9.5, text: 'At sixty-four rows, that is a hundred ninety-five operations in three layers. A tree for the first hit takes four hundred forty-seven, in thirty-four layers.' }); + tl.tween(cam, CAMERA_HOME, { at: 36.5, dur: 1.2, ease: ease.move }); + tl.tween(fiOp, 0.35, { at: 36.6, dur: 0.6, ease: ease.move }); + tl.tween(blocksOp, 0.35, { at: 36.6, dur: 0.6, ease: ease.move }); + tl.tween(codeB, 0.35, { at: 36.6, dur: 0.6, ease: ease.move }); + tl.tween(scatU, 1, { at: 37.2, dur: 1.4, ease: ease.draw }); + tl.tween(menuU, 1, { at: 36.6, dur: 0.6, ease: ease.move }); + tl.tween(pC, 1, { at: 39.2, dur: 0.5, ease: ease.pop }); + tl.tween(pT, 1, { at: 42.6, dur: 0.5, ease: ease.pop }); + tl.tween(arrTC, 1, { at: 43.6, dur: 1.1, ease: ease.move }); + tl.hold(46.0, 0.4); + + // — B5 · the monotone caveat — + tl.caption({ at: 46.4, dur: 6.1, text: 'It only works while the bits flip once. Unsorted row bottoms give a wrong row, silently.' }); + tl.tween(scatOp, 0.35, { at: 46.4, dur: 0.6, ease: ease.move }); + out(codeB, 46.4, 0.5); + tl.tween(exU, 1, { at: 46.6, dur: 0.6, ease: ease.enter }); + tl.tween(exBits, 1, { at: 47.3, dur: 0.6, ease: ease.enter }); + tl.tween(exSigma, 1, { at: 47.9, dur: 0.5, ease: ease.pop }); + tl.tween(exLoop, 1, { at: 48.5, dur: 0.6, ease: ease.enter }); + tl.tween(exFlag, 1, { at: 49.3, dur: 0.5, ease: ease.pop }); + out(exU, 51.6, 0.7); + tl.hold(52.5, 0.4); + + // — B6 · running totals are a chain — + tl.caption({ at: 52.9, dur: 8.7, text: 'Running totals are a chain: each total reads the one before it, so at 1,024 rows the chain is 1,023 layers deep.' }); + out(fiOp, 52.6, 0.6); + tl.tween(blocksOp, 1, { at: 52.6, dur: 0.6, ease: ease.move }); + tl.tween(m0, 0.35, { at: 52.9, dur: 0.5, ease: ease.move }); + tl.tween(m1, 1, { at: 52.9, dur: 0.5, ease: ease.move }); + tl.tween(codeC, 1, { at: 53.2, dur: 0.6, ease: ease.enter }); + tl.tween(cam, CAM_TREE, { at: 53.0, dur: 1.2, ease: ease.move }); + out(menuU, 53.0, 0.5); + tl.tween(barsU, 1, { at: 53.5, dur: 1.3, ease: ease.move }); + tl.tween(chainU, 4, { at: 55.0, dur: 2.8, ease: ease.linear }); + tl.tween(chainTag, 1, { at: 57.6, dur: 0.5, ease: ease.enter }); + tl.tween(cam, CAMERA_HOME, { at: 58.3, dur: 1.1, ease: ease.move }); + tl.tween(menuU, 1, { at: 58.4, dur: 0.6, ease: ease.move }); + tl.tween(scatOp, 1, { at: 58.8, dur: 0.6, ease: ease.move }); + tl.tween(pH, 1, { at: 59.5, dur: 0.5, ease: ease.pop }); + tl.hold(61.2, 0.4); + + // — B7 · a Brent-Kung network — + tl.caption({ at: 61.6, dur: 9.2, text: 'Combine in pairs, then pairs of pairs: a layout called a Brent-Kung network gives every total in eighteen layers, for two thousand thirty-six operations.' }); + tl.tween(treeU, 1, { at: 61.9, dur: 1.4, ease: ease.move }); + tl.tween(arrHB, 1, { at: 64.6, dur: 1.1, ease: ease.move }); + tl.tween(pB, 1, { at: 65.5, dur: 0.5, ease: ease.pop }); + tl.hold(70.8, 0.4); + + // — B7b · exact in any grouping: the tree's totals are the loop's — + tl.caption({ at: 71.2, dur: 7.0, text: 'Whole numbers below about sixteen million add exactly in any grouping, so the tree\'s totals match the loop\'s.' }); + tl.tween(exactU, 1, { at: 71.4, dur: 0.6, ease: ease.enter }); + tl.tween(matchU, 1, { at: 74.4, dur: 0.7, ease: ease.enter }); + tl.tween(matchU, 2, { at: 75.4, dur: 0.7, ease: ease.enter }); + tl.hold(78.2, 0.4); + + // — B8 · a sort needs no sorting — + tl.caption({ at: 78.6, dur: 11.2, text: 'A sort needs no sorting. With keys on a grid, one comparison bit per pair, summed per item, gives each rank: at sixty-four items, 4,096 operations in three layers.' }); + out(totalsOp, 78.5, 0.6); + out(blocksOp, 78.5, 0.6); + out(codeC, 78.5, 0.5); + tl.tween(scatOp, 0.35, { at: 78.5, dur: 0.6, ease: ease.move }); + tl.tween(m1, 0.35, { at: 78.6, dur: 0.5, ease: ease.move }); + tl.tween(m3, 1, { at: 78.6, dur: 0.5, ease: ease.move }); + tl.tween(cam, CAM_RANK, { at: 78.8, dur: 1.2, ease: ease.move }); + out(menuU, 78.7, 0.5); + tl.tween(codeE, 1, { at: 79.0, dur: 0.6, ease: ease.enter }); + tl.tween(rankU, 1, { at: 79.3, dur: 0.7, ease: ease.enter }); + tl.tween(rankFill, 1, { at: 81.3, dur: 1.6, ease: ease.linear }); + tl.tween(rankSums, 1, { at: 83.1, dur: 0.5, ease: ease.pop }); + tl.tween(rankTex, 1, { at: 83.6, dur: 0.6, ease: ease.enter }); + tl.tween(tieU, 1, { at: 84.1, dur: 0.6, ease: ease.enter }); + tl.tween(fallU, 1, { at: 84.9, dur: 1.2, ease: ease.move }); + tl.tween(cam, CAMERA_HOME, { at: 86.8, dur: 1.1, ease: ease.move }); + tl.tween(menuU, 1, { at: 86.9, dur: 0.6, ease: ease.move }); + tl.tween(scatOp, 1, { at: 87.2, dur: 0.6, ease: ease.move }); + tl.tween(pS, 1, { at: 87.9, dur: 0.5, ease: ease.pop }); + tl.hold(89.8, 0.4); + + // — B9 · a table JavaScript writes — + tl.caption({ at: 90.2, dur: 10.2, text: 'If data rarely changes, let Javascript run the loop and write the answers into cells. The program reads one in a single operation, at any table size.' }); + out(rankOp, 90.1, 0.6); + out(codeE, 90.1, 0.5); + tl.tween(scatOp, 0.35, { at: 90.1, dur: 0.6, ease: ease.move }); + tl.tween(m3, 0.35, { at: 90.2, dur: 0.5, ease: ease.move }); + tl.tween(m2, 1, { at: 90.2, dur: 0.5, ease: ease.move }); + tl.tween(cam, CAM_TABLE, { at: 90.4, dur: 1.2, ease: ease.move }); + out(menuU, 90.3, 0.5); + tl.tween(codeF, 1, { at: 90.6, dur: 0.6, ease: ease.enter }); + tl.tween(listU, 1, { at: 90.9, dur: 0.7, ease: ease.enter }); + tl.tween(hostU, 1, { at: 91.5, dur: 0.6, ease: ease.enter }); + tl.tween(fillU, 24, { at: 92.0, dur: 2.6, ease: ease.linear }); + tl.tween(readU, 1, { at: 95.4, dur: 0.6, ease: ease.enter }); + tl.tween(readTex, 1, { at: 96.0, dur: 0.6, ease: ease.enter }); + tl.tween(glowU, 1, { at: 96.6, dur: 0.5, ease: ease.pop }); + tl.tween(codeG, 1, { at: 97.0, dur: 0.6, ease: ease.enter }); + tl.tween(opChipU, 1, { at: 97.3, dur: 0.5, ease: ease.pop }); + tl.tween(cam, CAMERA_HOME, { at: 98.1, dur: 1.1, ease: ease.move }); + tl.tween(menuU, 1, { at: 98.2, dur: 0.6, ease: ease.move }); + tl.tween(scatOp, 1, { at: 98.4, dur: 0.6, ease: ease.move }); + tl.tween(pR, 1, { at: 99.2, dur: 0.5, ease: ease.pop }); + tl.hold(100.4, 0.4); + + // — B10 · work vs waiting (everything but the scatter leaves, so nothing hangs clipped at the edge) — + tl.caption({ at: 100.8, dur: 12.6, text: 'Fewer layers is not always faster: on a processor, time follows operations, and the tree can lose. On a graphics card, a whole level of the tree runs as one batch of parallel work.' }); + out(leftOp, 100.8, 0.7); + out(codeF, 100.8, 0.6); + out(codeG, 100.8, 0.6); + out(titleU, 100.8, 0.5); + out(menuU, 100.8, 0.8); + tl.tween(cam, CAM_SCATTER, { at: 100.9, dur: 1.3, ease: ease.move }); + tl.set(arrTC, 0, 102.5); + tl.tween(arrTC, 1, { at: 102.5, dur: 1.0, ease: ease.move }); + tl.set(arrHB, 0, 102.9); + tl.tween(arrHB, 1, { at: 102.9, dur: 1.0, ease: ease.move }); + tl.tween(xGlow, 1, { at: 104.4, dur: 0.6, ease: ease.enter }); + tl.tween(yGlow, 1, { at: 107.4, dur: 0.6, ease: ease.enter }); + tl.hold(113.4, 0.4); + + // — B11 · close: every test runs, a count picks — + tl.caption({ at: 113.8, dur: 6.9, text: 'So a loop ends up like the branch did: every test runs, and a count picks the winner.' }); + tl.tween(cam, CAMERA_HOME, { at: 113.8, dur: 1.2, ease: ease.move }); + tl.tween(scatOp, 0.15, { at: 113.8, dur: 0.8, ease: ease.move }); + out(xGlow, 113.8, 0.6); + out(yGlow, 113.8, 0.6); + tl.tween(titleU, 1, { at: 114.5, dur: 0.6, ease: ease.enter }); + tl.tween(cardU, 1, { at: 114.6, dur: 0.8, ease: ease.enter }); + tl.hold(120.7, 2.5); + + return { + tl, cam, src, titleU, codeA, codeB, codeC, codeE, codeF, codeG, + rowsU, blocksOp, fiOp, needleU, curU, curRow, checkIdx, stopU, row4Dim, panelU, flashU, everyU, testU, bitsW, + sumU, sigmaU, ringU, sameU, flipU, idxTexU, + menuU, m0, m1, m2, m3, + scatU, scatOp, pC, pT, pH, pB, pS, pR, arrTC, arrHB, xGlow, yGlow, + exU, exBits, exSigma, exLoop, exFlag, + barsU, chainU, chainTag, treeU, exactU, matchU, totalsOp, + rankU, rankFill, rankSums, rankTex, tieU, fallU, rankOp, + listU, hostU, fillU, readU, readTex, glowU, opChipU, + leftOp, cardU, + }; +} + +const scene = buildScene(); +type Scene = typeof scene; + +// --------------------------------------------------------------------------- +// Glyph kit (book-wide specs, local copy) +// --------------------------------------------------------------------------- + +/** The bit: solid amber disc when 1, hollow ring when 0. */ +function BitDisc({ x, y, b, r = 11, opacity = 1 }: { x: number; y: number; b: number; r?: number; opacity?: number }) { + if (opacity <= 0.002) return null; + return ( + + = 0.5 ? colors.WARM : 'none'} stroke={colors.WARM} strokeWidth={2} /> + = 0.5 ? DARK : colors.WARM}> + {b >= 0.5 ? '1' : '0'} + + + ); +} + +/** Verbatim code chip: mono 14 px, panel fill, 1 px grid border, role stripe on the left. */ +function CodeChip({ x, y, lines, opacity, stripe = colors.MUTED, size = 14 }: { x: number; y: number; lines: string[]; opacity: number; stripe?: string; size?: number }) { + if (opacity <= 0.002) return null; + const lh = size + 6; + const w = Math.max(...lines.map((l) => l.length)) * size * 0.602 + 30; + const h = lines.length * lh + 12; + return ( + + + + {lines.map((l, i) => ( + + {l} + + ))} + + ); +} + +function Label({ x, y, children, size = 13, fill = colors.MUTED, anchor = 'start', mono = false, weight = 400, opacity = 1 }: { + x: number; y: number; children: ReactNode; size?: number; fill?: string; anchor?: 'start' | 'middle' | 'end'; mono?: boolean; weight?: number; opacity?: number; +}) { + if (opacity <= 0.002) return null; + return ( + + {children} + + ); +} + +/** Quadratic curve from a to b bent by `bend`, drawn up to fraction u, with a head at the tip. */ +function GrowArrow({ a, b, bend, u, color, width = 2.2, opacity = 1 }: { a: [number, number]; b: [number, number]; bend: number; u: number; color: string; width?: number; opacity?: number }) { + if (u <= 0.002 || opacity <= 0.002) return null; + const mx = (a[0] + b[0]) / 2; + const my = (a[1] + b[1]) / 2; + const dx = b[0] - a[0]; + const dy = b[1] - a[1]; + const len = Math.hypot(dx, dy) || 1; + const c: [number, number] = [mx - (dy / len) * bend, my + (dx / len) * bend]; + const at = (t: number): [number, number] => [ + (1 - t) * (1 - t) * a[0] + 2 * (1 - t) * t * c[0] + t * t * b[0], + (1 - t) * (1 - t) * a[1] + 2 * (1 - t) * t * c[1] + t * t * b[1], + ]; + const n = 28; + const pts: string[] = []; + for (let i = 0; i <= n; i++) { + const p = at((u * i) / n); + pts.push(`${p[0].toFixed(1)},${p[1].toFixed(1)}`); + } + const tip = at(u); + const back = at(Math.max(0, u - 0.04)); + const ang = Math.atan2(tip[1] - back[1], tip[0] - back[0]); + const hl = 9; + const head = [ + [tip[0], tip[1]], + [tip[0] - hl * Math.cos(ang - 0.45), tip[1] - hl * Math.sin(ang - 0.45)], + [tip[0] - hl * Math.cos(ang + 0.45), tip[1] - hl * Math.sin(ang + 0.45)], + ].map((p) => p.map((v) => v.toFixed(1)).join(',')).join(' '); + return ( + + + + + ); +} + +// --------------------------------------------------------------------------- +// Pieces +// --------------------------------------------------------------------------- + +const CHECKS: Array<{ text: string; color: string }> = [ + { text: 'JavaScript, row 0: is 90 < 40? no, keep going', color: colors.NEGATIVE }, + { text: 'JavaScript, row 1: is 90 < 80? no, keep going', color: colors.NEGATIVE }, + { text: 'JavaScript, row 2: is 90 < 80? no, keep going', color: colors.NEGATIVE }, + { text: 'JavaScript, row 3: is 90 < 140? yes: stop, return 3', color: colors.POSITIVE }, + { text: 'the loop returns 3 · the count is 3 ✓ same answer', color: colors.POSITIVE }, +]; + +function RowList({ s, sc }: { s: SceneState; sc: Scene }) { + const op = s.get(sc.blocksOp) * s.get(sc.leftOp) * s.get(sc.totalsOp); + if (op <= 0.002) return null; + const rowsU = s.get(sc.rowsU); + const barsU = s.get(sc.barsU); + const row4Dim = s.get(sc.row4Dim); + const ringU = s.get(sc.ringU); + const listLabels = 1 - clamp01(barsU * 2); + const barLabels = clamp01(barsU * 2 - 1); + return ( + + {/* ruler with the row bottoms as ticks */} + + + {[0, 40, 80, 140, 180].map((v) => ( + + + + + ))} + + + {HEIGHTS.map((h, i) => { + const enter = clamp01(rowsU * 5 - i); + if (enter <= 0) return null; + const isLast = i === 4; + // list geometry → bar geometry (rows 0..3 become h0..h3; row 4 leaves) + const u = isLast ? 0 : barsU; + const x0 = lerp(BLK.x, COL_X[Math.min(i, 3)] - BAR_W / 2, u); + const w = lerp(BLK.w, BAR_W, u); + const y0 = lerp(yOf(TOPS[i]), BAR_BASE - h * BAR_SCALE, u); + const y1 = lerp(yOf(BOTTOMS[i]), BAR_BASE, u); + const rowOp = isLast ? (1 - 0.6 * row4Dim) * (1 - barsU) : 1; + if (rowOp <= 0.002) return null; + const hh = Math.max(0, y1 - y0); + return ( + + {hh > 0.5 ? ( + + ) : ( + + )} + {/* list labels */} + + {i === 2 ? ( + + ) : ( + + )} + {i === 2 && } + {isLast && } + + {/* bar labels */} + {!isLast && ( + + )} + + ); + })} + {/* row 3 holds the pointer: the count's answer */} + {ringU > 0.002 && listLabels > 0.002 && ( + + + + + )} + + ); +} + +function FindIndex({ s, sc }: { s: SceneState; sc: Scene }) { + const op = s.get(sc.fiOp) * s.get(sc.leftOp); + if (op <= 0.002) return null; + const needleU = s.get(sc.needleU); + const curU = s.get(sc.curU); + const curRow = s.get(sc.curRow); + const checkIdx = Math.round(s.get(sc.checkIdx)); + const stopU = s.get(sc.stopU); + const panelU = s.get(sc.panelU); + const flashU = s.get(sc.flashU); + const everyU = s.get(sc.everyU); + const testU = s.get(sc.testU); + const bitsW = s.get(sc.bitsW); + const sumU = s.get(sc.sumU); + const sigmaU = s.get(sc.sigmaU); + const sameU = s.get(sc.sameU); + const flipU = s.get(sc.flipU); + const idxTexU = s.get(sc.idxTexU); + const r0 = Math.floor(curRow); + const r1 = Math.min(4, r0 + 1); + const cy = lerp(ROW_CY[r0], ROW_CY[r1], curRow - r0); + const check = CHECKS[Math.max(0, Math.min(CHECKS.length - 1, checkIdx))]; + const showSame = checkIdx >= 4 ? sameU : 1; + return ( + + {/* needle: pointerY = 90 */} + {needleU > 0.002 && ( + + + + + + )} + {/* the JavaScript cursor */} + {curU > 0.002 && ( + + + + + {[0, 1, 2, 3].map((r) => { + const seen = clamp01((curRow - r) * 3 + 1) * (checkIdx >= r ? 1 : 0); + if (seen <= 0) return null; + const ok = r === 3; + return ( + + ); + })} + {/* stop sign beside row 3 */} + {stopU > 0.002 && ( + + { + const a = Math.PI / 8 + (k * Math.PI) / 4; + return `${(19 * Math.cos(a)).toFixed(1)},${(19 * Math.sin(a)).toFixed(1)}`; + }).join(' ')} + fill={colors.NEGATIVE} + /> + stop + + )} + + + )} + {/* the program: five tests, no stop */} + {panelU > 0.002 && ( + + + + + {BOTTOMS.map((b, i) => { + const y = ROW_CY[i] - TILE.h / 2; + const bitU = clamp01(bitsW - i); + return ( + + 0.01 ? colors.WARM : colors.MUTED} strokeOpacity={0.5 + 0.5 * flashU} strokeWidth={1 + 1.5 * flashU} /> + + + + + ); + })} + {/* the ones slide into the sum */} + {sumU > 0.002 && + [0, 1, 2].map((i) => { + const u = sumU; + const x = lerp(DISC_X, SIGMA.x - 18 + i * 18, u); + const y = lerp(ROW_CY[i], SIGMA.y + 6, u); + return ; + })} + {sigmaU > 0.002 && ( + + + Σ + {COUNT} + + )} + {/* the bits flip once and never back */} + {flipU > 0.002 && ( + + + + + + + )} + + + )} + + ); +} + +function MenuBar({ s, sc }: { s: SceneState; sc: Scene }) { + const menuU = s.get(sc.menuU); + if (menuU <= 0.002) return null; + const lit = [s.get(sc.m0), s.get(sc.m1), s.get(sc.m2), s.get(sc.m3)]; + const slide = (1 - clamp01(menuU)) * 24; + return ( + + {MENU.map((m, i) => { + const L = lit[i]; + const gx = m.x + 14; + const gy = MENU_Y + 24; + const glyph = (() => { + const st = { stroke: colors.WARM, strokeWidth: 1.6, fill: 'none' } as const; + if (i === 0) + return ( + + {[0, 1, 2, 3, 4].map((k) => ( + + ))} + + ); + if (i === 1) + return ( + + + + + + ); + if (i === 2) + return ( + + {[0, 1, 2].map((r) => + [0, 1, 2].map((c) => ( + + )), + )} + + ); + return ( + + {[0, 1, 2].map((k) => ( + + + + + ))} + + ); + })(); + return ( + + 0.6 ? colors.WARM : colors.MUTED} strokeOpacity={0.8} strokeWidth={L > 0.6 ? 2 : 1} /> + {glyph} + + + ); + })} + + ); +} + +function Caveat({ s, sc }: { s: SceneState; sc: Scene }) { + const exU = s.get(sc.exU); + if (exU <= 0.002) return null; + const bitsU = s.get(sc.exBits); + const sigU = s.get(sc.exSigma); + const loopU = s.get(sc.exLoop); + const flagU = s.get(sc.exFlag); + const ey = (u: number): number => 204 + u * 2.6; + const LANE_X = [150, 222, 294]; + const TILE_Y = [228, 278, 328]; + return ( + + + + {/* ruler */} + + {[0, 30, 40, 80].map((v) => ( + + + + + ))} + {EX_HEIGHTS.map((h, i) => { + const a = Math.min(EX_TOPS[i], EX_BOTTOMS[i]); + const b = Math.max(EX_TOPS[i], EX_BOTTOMS[i]); + const neg = h < 0; + return ( + + + + + + ); + })} + + {/* the program's tests */} + {EX_BOTTOMS.map((b, i) => ( + + + + + + ))} + + {sigU > 0.002 && ( + + + Σ + {EX_COUNT} + + )} + + + {flagU > 0.002 && ( + + + wrong row, no error + + )} + + ); +} + +type NodeId = 't0' | 't1' | 't2' | 't3' | 'u'; +const NODE_LABEL: Record = { + t0: `0..0 · ${TREE[0]}`, + t1: `0..1 · ${TREE[1]}`, + t2: `0..2 · ${TREE[2]}`, + t3: `0..3 · ${TREE[3]}`, + u: `2..3 · ${PAIR23}`, +}; + +function Totals({ s, sc }: { s: SceneState; sc: Scene }) { + const op = s.get(sc.totalsOp) * s.get(sc.leftOp); + const chainU = s.get(sc.chainU); + if (op <= 0.002 || chainU <= 0.002) return null; + const treeU = s.get(sc.treeU); + const tagU = s.get(sc.chainTag); + const exactU = s.get(sc.exactU); + const matchU = s.get(sc.matchU); + const treeRow = clamp01(matchU); + const loopRow = clamp01(matchU - 1); + const pos: Record = { + t0: [COL_X[0], LAYER_Y[0]], + t1: [COL_X[1], LAYER_Y[1]], + t2: [COL_X[2], LAYER_Y[2]], + t3: [COL_X[3], lerp(LAYER_Y[3], LAYER_Y[2], treeU)], + u: [COL_X[3], LAYER_Y[1]], + }; + const nodeOp: Record = { + t0: clamp01(chainU * 2), + t1: clamp01((chainU - 1) * 2), + t2: clamp01((chainU - 2) * 2), + t3: clamp01((chainU - 3) * 2), + u: treeU, + }; + const barFoot = (i: number): [number, number] => [COL_X[i], BAR_BASE + 26]; + const top = (n: NodeId): [number, number] => [pos[n][0], pos[n][1] - NODE.h / 2]; + const bottom = (n: NodeId): [number, number] => [pos[n][0], pos[n][1] + NODE.h / 2]; + const chainOnly = 1 - treeU; + const edges: Array<{ a: [number, number]; b: [number, number]; o: number; main: boolean }> = [ + { a: barFoot(0), b: top('t0'), o: nodeOp.t0, main: false }, + { a: bottom('t0'), b: top('t1'), o: nodeOp.t1, main: true }, + { a: barFoot(1), b: top('t1'), o: nodeOp.t1, main: false }, + { a: bottom('t1'), b: top('t2'), o: nodeOp.t2, main: true }, + { a: barFoot(2), b: top('t2'), o: nodeOp.t2, main: false }, + { a: bottom('t2'), b: top('t3'), o: nodeOp.t3 * chainOnly, main: true }, + { a: barFoot(3), b: top('t3'), o: nodeOp.t3 * chainOnly, main: false }, + { a: barFoot(2), b: top('u'), o: treeU, main: false }, + { a: barFoot(3), b: top('u'), o: treeU, main: false }, + { a: [pos.t1[0] + NODE.w / 2, pos.t1[1] + 6], b: [pos.t3[0] - NODE.w / 2 + 10, pos.t3[1] - NODE.h / 2], o: treeU, main: true }, + { a: bottom('u'), b: top('t3'), o: treeU, main: true }, + ]; + const tagA = 1 - treeU; + const tagB = treeU * (1 - treeRow); + return ( + + {/* depth axis */} + {[1, 2, 3].map((L) => ( + + + + + ))} + {edges.map((e, i) => + e.o > 0.002 ? ( + + ) : null, + )} + {(Object.keys(pos) as NodeId[]).map((n) => { + const o = nodeOp[n]; + if (o <= 0.002) return null; + const [x, y] = pos[n]; + const isNew = n === 'u'; + return ( + + + {NODE_LABEL[n]} + {!isNew && treeRow > 0.002 && ( + + )} + + ); + })} + {/* the depth tag */} + + + + {/* the structure behind 2,036 · 18: brentKungPrefix (patterns.md:50, 193) */} + + + + {/* the tree's totals against the loop's, column by column (patterns.md:34, 39) */} + {treeRow > 0.002 && ( + + + + {TREE.map((v, i) => ( + + ))} + + + {CHAIN.map((_, i) => ( + + ))} + + {CHAIN.map((v, i) => ( + + ))} + + + + + )} + {exactU > 0.002 && ( + + + + + )} + + ); +} + +function Ranks({ s, sc }: { s: SceneState; sc: Scene }) { + const op = s.get(sc.rankOp) * s.get(sc.leftOp); + const rankU = s.get(sc.rankU); + if (op <= 0.002 || rankU <= 0.002) return null; + const fill = s.get(sc.rankFill); + const sums = s.get(sc.rankSums); + const tex = s.get(sc.rankTex); + const tie = s.get(sc.tieU); + const fall = s.get(sc.fallU); + const offIdx = (i: number, j: number): number => i * 3 + (j > i ? j - 1 : j); + const values = RANK_BITS.map((row, i) => row.map((v, j) => (i === j ? 0.5 : v))); + const colCx = (j: number): number => RG.x + j * RG_PITCH + RG.cell / 2; + const rowCy = (i: number): number => RG.y + i * RG_PITCH + RG.cell / 2; + return ( + + (i === j ? 1 : clamp01(fill * 12 - offIdx(i, j)))} + fill={(v, i, j) => (i === j ? '#141b2b' : v > 0.5 ? colors.WARM : '#1b2436')} + showValues={(v) => (v === 0.5 ? '·' : v > 0.5 ? '1' : '0')} + highlight={{ cell: [2, 0], color: colors.POSITIVE, u: tie }} + /> + {KEYS.map((_, i) => ( + + ))} + + {RANKS.map((r, i) => ( + + ))} + + {[0, 1, 2, 3].map((r) => ( + + ))} + {/* the cards: above their column, then fall to their rank */} + {KEYS.map((k, j) => { + const x = lerp(colCx(j), SORTED_X, fall); + const y = lerp(CARD.y + CARD.h / 2, rowCy(RANKS[j]), fall); + return ( + + + {k} + {`item ${j}`} + + ); + })} + + + + ); +} + +function Table({ s, sc }: { s: SceneState; sc: Scene }) { + const op = s.get(sc.leftOp); + const listU = s.get(sc.listU); + if (op <= 0.002 || listU <= 0.002) return null; + const hostU = s.get(sc.hostU); + const fillU = s.get(sc.fillU); + const readU = s.get(sc.readU); + const readTex = s.get(sc.readTex); + const glowU = s.get(sc.glowU); + const opChipU = s.get(sc.opChipU); + const hostRow = fillU > 0 && fillU < 24 ? Math.floor(fillU / LEVELS) : -1; + const cellX = (l: number): number => AT.x + l * AT.pitch; + const cellY = (r: number): number => AT.y + r * AT.pitch; + const target: [number, number] = [cellX(READ.level) + AT.cell / 2, cellY(READ.row) + AT.cell / 2]; + return ( + + {/* the host lane */} + + + + + + {DEPTHS.map((d, r) => { + const y = LIST.y + r * LIST.pitch; + const on = r === hostRow; + return ( + + + + + ); + })} + + {[0, 1, 2, 3].map((l) => ( + + ))} + {ANCESTOR.map((row, r) => + row.map((v, l) => { + const k = r * LEVELS + l; + const u = clamp01(fillU - k); + const isRead = k === READ_CELL; + const g = isRead ? glowU : 0; + return ( + + 0.5 ? colors.WARM : colors.TEAL} fillOpacity={g > 0.5 ? 0.95 : 0.06 + 0.16 * u} stroke={g > 0.01 ? colors.WARM : colors.GRID} strokeWidth={g > 0.01 ? 2.5 : 1} /> + 0.5 ? 700 : 400} fill={g > 0.5 ? DARK : INK} opacity={u}>{v} + + ); + }), + )} + {/* the program's one read */} + {readU > 0.002 && ( + + + {[ + { y: 178, text: `row ${READ.row}` }, + { y: 220, text: `level ${READ.level}` }, + ].map((c) => ( + + + + + + ))} + + + + + )} + {opChipU > 0.002 && ( + + + + + )} + + ); +} + +function Scatter({ s, sc }: { s: SceneState; sc: Scene }) { + const scatU = s.get(sc.scatU); + if (scatU <= 0.002) return null; + const op = s.get(sc.scatOp); + const xGlow = s.get(sc.xGlow); + const yGlow = s.get(sc.yGlow); + const pop: Record = { + T: s.get(sc.pT), + C: s.get(sc.pC), + H: s.get(sc.pH), + B: s.get(sc.pB), + S: s.get(sc.pS), + R: s.get(sc.pR), + }; + const axisX = lerp(SC.x0, SC.x1, scatU); + const axisY = lerp(SC.y0, SC.y1, scatU); + const ticksU = clamp01(scatU * 2 - 1); + const xCol = xGlow > 0.01 ? colors.TEAL : colors.MUTED; + const yCol = yGlow > 0.01 ? colors.WARM : colors.MUTED; + const yTitleX = 798; + const yTagX = 778; + const midY = (SC.y0 + SC.y1) / 2; + const arrow = (from: PointId, to: PointId, u: number, bend: number) => { + const a = POINTS[from]; + const b = POINTS[to]; + const dx = b.px - a.px; + const dy = b.py - a.py; + const L = Math.hypot(dx, dy); + return ( + + ); + }; + return ( + + {/* decade gridlines */} + + {X_TICKS.map((v) => ( + + ))} + {Y_TICKS.map((v) => ( + + ))} + + + + {/* axes */} + + + + {X_TICKS.map((v) => ( + + + + + ))} + {Y_TICKS.map((v) => ( + + + + + ))} + + 0.01 ? colors.WARM : INK}> + layers (waiting) → + + + {yGlow > 0.002 && ( + + graphics card: each layer at once + + )} + + {arrow('T', 'C', s.get(sc.arrTC), 18)} + {arrow('H', 'B', s.get(sc.arrHB), -26)} + {(Object.keys(POINTS) as PointId[]).map((id) => { + const u = pop[id]; + if (u <= 0.002) return null; + const p = POINTS[id]; + const sc2 = Math.max(0, u); + return ( + + + + + + + + + + ); + })} + + ); +} + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +export function Render({ s }: { s: SceneState }) { + const sc = scene; + const cam = s.get(sc.cam); + const srcIdx = Math.max(0, Math.min(SOURCES.length - 1, Math.round(s.get(sc.src)))); + const leftOp = s.get(sc.leftOp); + const cardU = s.get(sc.cardU); + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + {cardU > 0.002 && ( + + + + + + + )} + + + ); +} + +export const vizScene = () => scene; diff --git a/apps/bookshelf/src/viz/books/handwired/chapter-5.stories.tsx b/apps/bookshelf/src/viz/books/handwired/chapter-5.stories.tsx new file mode 100644 index 00000000..d754fee2 --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired/chapter-5.stories.tsx @@ -0,0 +1,29 @@ +// Handwired — chapter 5 story. Scene authored by Claude Opus 5.5 (claude-opus-5-5). +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Player } from '../../core'; +import { Render, vizScene } from './chapter-5'; + +/** + * Exact, Trainable, Wide — the chapter-1 program stack read four ways: its comparison bits equal + * JavaScript's, a gradient runs back through it and stops at a rounding bit unless straight-through + * is chosen, its weights are frozen in the desktop demo and trained in the learned gallery, and one + * pass runs it over 100,000 documents. + */ +function Chapter5() { + const scene = vizScene(); + return ( +
+ + {(s) => } + +
+ ); +} + +const meta: Meta = { + title: 'Books/Handwired/Chapter 5', + component: Chapter5, +}; +export default meta; + +export const Scene: StoryObj = {}; diff --git a/apps/bookshelf/src/viz/books/handwired/chapter-5.tsx b/apps/bookshelf/src/viz/books/handwired/chapter-5.tsx new file mode 100644 index 00000000..9521f6d8 --- /dev/null +++ b/apps/bookshelf/src/viz/books/handwired/chapter-5.tsx @@ -0,0 +1,1750 @@ +// Exact, Trainable, Wide +// +// Handwired — chapter 5. Source pin: chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46 (MIT, Cheng Lou) +// exact: docs/execution-model.md:118 (the guarantee), 123 (integer bits: relu(1 - relu(-D))), 126, +// 130-136 (cost table), 143 (gradient of a cascade), 257 (what exact means); +// docs/tutorial.md:53, 167 +// gradients: README.md:29, docs/execution-model.md:21-36 (the instruction set: affine, reglu, boolean, index), 112, +// src/runtime/numeric-ui-core.ts:658, 718-722 (booleanGradient, the throw, the pass-through) +// desktop: src/handwiredos/README.md:3, 13, 42 +// learned: src/learned-gallery/README.md:3, 15, 17, 28 +// JSON search: src/json-search/README.md:3, 30, 42, 47, 53 +// +// Authored by Claude Opus 5.5 (claude-opus-5-5) from the Sonnet 5.5 visual plan. Captions are the +// planner's fixed narration (verbatim). +// +// Persistent object: the program stack from chapter 1 (four MatrixGrid layer tiles, violet relu gates, +// the combine and the pick), faint behind every beat, the subject of the gradient beats, and alone +// (≤ 0.12) behind the closing tiles. Derived numbers (result(a), its slope) are computed here and +// labelled "derived from the README formulas"; the 15-chip staircase, the arcs, the desktop and +// photo sketches are labelled schematic or placeholder. Nothing here is a screenshot of the runtime. +import type { ReactNode } from 'react'; +import { scaleLinear, interpolateRgb } from 'd3'; +import { + CAMERA_HOME, + Camera, + MathLabel, + STAGE_H, + STAGE_W, + Timeline, + cameraInterp, + colors, + ease, + gaussian, + mulberry32, +} from '../../core'; +import type { CameraState, SceneState } from '../../core'; +import { FunctionPlot, MatrixGrid, Vec } from '../../primitives'; + +const clamp01 = (u: number): number => (u < 0 ? 0 : u > 1 ? 1 : u); +const lerp = (a: number, b: number, u: number): number => a + (b - a) * u; +const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace'; +const COPPER = '#d98a56'; +const DIM_CELL = '#141b2b'; +const relu = (x: number): number => (x > 0 ? x : 0); +const commas = (n: number): string => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ','); +const fmtNum = (v: number): string => { + const r = Math.round(v * 100) / 100; + const s = Number.isInteger(r) ? String(r) : r.toFixed(2).replace(/0$/, ''); + return s.replace('-', '−'); +}; +function check(cond: boolean, msg: string): void { + if (!cond) throw new Error(`handwired ch5: ${msg}`); +} + +// --------------------------------------------------------------------------- +// The arithmetic (module scope) +// --------------------------------------------------------------------------- + +// B0 — execution-model.md:123: on a grid, a >= b is relu(1 - relu(-D)), equal to relu(D+1) - relu(D) at every integer +const BIT = (D: number): number => relu(1 - relu(-D)); +const RAMP_DIFF = (D: number): number => relu(D + 1) - relu(D); +const P1F = (D: number): number => relu(-D); +const P2F = (D: number): number => 1 - relu(-D); +const D_INTS = [-4, -3, -2, -1, 0, 1, 2, 3, 4]; +const D_BITS = D_INTS.map(BIT); +check(D_BITS.join('') === '000011111', 'built bit on D = -4..4'); +check(D_INTS.every((d) => RAMP_DIFF(d) === BIT(d) && BIT(d) === (d >= 0 ? 1 : 0)), 'ramp difference and JS comparison'); + +// B1 — twelve samples, the same bit strip from both builders (execution-model.md:134-136, tutorial.md:53) +const STRIP_D = [-2, 3, 0, -1, 5, -4, 1, 0, -3, 2, -1, 4]; +const STRIP_BITS = STRIP_D.map((d) => (d >= 0 ? 1 : 0)); +check(STRIP_D.every((d, i) => BIT(d) === STRIP_BITS[i]), 'strip bits'); + +// B3 — derived from the README formulas: result = above4·10 + (1 − above4)·12 = 12 − 2·(relu(a−4) − relu(a−5)) +const G1 = (a: number): number => relu(a - 4); +const G2 = (a: number): number => relu(a - 5); +const ABOVE4 = (a: number): number => G1(a) - G2(a); +const RESULT = (a: number): number => ABOVE4(a) * 10 + (1 - ABOVE4(a)) * 12; +const SLOPE1 = (a: number): number => (a > 4 ? 1 : 0); +const SLOPE2 = (a: number): number => (a > 5 ? 1 : 0); +// backward pass: d result/d p10 = d result/d p12 = 1; d/d above4 = 10 − 12 = −2; d/d g1 = −2, d/d g2 = +2 +const GRAD = { res: 1, p10: 1, p12: 1, ab: 10 - 12, g1: (10 - 12) * 1, g2: (10 - 12) * -1 }; +const DRESULT_DA = (a: number): number => GRAD.g1 * SLOPE1(a) + GRAD.g2 * SLOPE2(a); +check(DRESULT_DA(4.5) === -2 && DRESULT_DA(3) === 0 && DRESULT_DA(6) === 0, 'd result / d a at 4.5, 3, 6'); +check(RESULT(4.5) === 11 && RESULT(3) === 12 && RESULT(6) === 10, 'result values'); +const HARD = (a: number): number => (a >= 4.5 ? 1 : 0); // a rounding bit: rounds a score at one half + +// B5 — handwiredos/README.md:13 +const DESKTOP = { cells: 234, ops: 4646, layers: 130, weights: 9909 }; +// B6 — learned-gallery/README.md:3, 17 +const GALLERY = { cells: 244, stepsPerSecond: 600, fitLo: 630, fitHi: 750, shown: 700 }; +// B7/B8 — json-search/README.md:3, 30, 42, 53 +const DOCS = 100_000; +const MATCHES = 10_559; +const BANDS = [ + { lo: 0.011, hi: 0.015, color: COPPER, label: 'this program, graphics card: 11 to 15 ms', side: 'right' as const }, + { lo: 0.008, hi: 0.028, color: colors.ACCENT, label: 'regular expressions, ordinary patterns: 8 to 28 ms', side: 'right' as const }, + { lo: 2.1, hi: 2.1, color: colors.NEGATIVE, label: 'regular expressions, backtracking ([a-z]+)+x: 2.1 s', side: 'left' as const }, + { lo: 13, hi: 13, color: colors.MUTED, label: "same program on a processor: 13 s (smallest program, the search's own time)", side: 'left' as const }, +]; +check( + BANDS[0].lo === 0.011 && BANDS[0].hi === 0.015 && BANDS[1].lo === 0.008 && BANDS[1].hi === 0.028 && BANDS[2].lo === 2.1 && BANDS[3].lo === 13, + 'ruler bands', +); +check(BANDS[1].lo < BANDS[0].lo && BANDS[0].hi < BANDS[1].hi, 'search band sits inside the RegExp band (overlap)'); + +// --------------------------------------------------------------------------- +// Layout (stage 1280×720; y ≥ 630 is the caption band) +// --------------------------------------------------------------------------- + +// B0 — three small plots, the D-line, the two lanes +const PLOTS = [ + { x0: 210, x1: 430, dom: [-0.5, 4.5] as [number, number], f: P1F, color: colors.SECONDARY, tex: '\\mathrm{relu}(-D)' }, + { x0: 530, x1: 750, dom: [-3.5, 1.5] as [number, number], f: P2F, color: colors.SECONDARY, tex: '1-\\mathrm{relu}(-D)' }, + { x0: 850, x1: 1070, dom: [-0.4, 1.4] as [number, number], f: BIT, color: colors.WARM, tex: '\\mathrm{bit}=\\mathrm{relu}\\big(1-\\mathrm{relu}(-D)\\big)' }, +].map((p) => ({ ...p, X: scaleLinear([-4.5, 4.5], [p.x0, p.x1]), Y: scaleLinear(p.dom, [230, 120]) })); +// the chapter-1 ramp difference over plot 3, sampled finely (it coincides with the bit) +const RAMP_DIFF_PATH = Array.from({ length: 181 }, (_, i) => { + const D = -4.5 + (9 * i) / 180; + return `${i === 0 ? 'M' : 'L'}${PLOTS[2].X(D).toFixed(1)} ${PLOTS[2].Y(RAMP_DIFF(D)).toFixed(1)}`; +}).join(''); +const DLINE_Y = 300; +const DX = (D: number): number => 640 + D * 100; +const JS_Y = 432; +const BUILT_Y = 492; + +// B1 — the two builders +const STRIP = { x0: 430, pitch: 36, yA: 456, yB: 528 }; +const STAIR = Array.from({ length: 15 }, (_, i) => { + // schematic: 15 operations over 14 layers — the first layer holds two + const col = i === 0 ? 0 : i - 1; + return { x: 628 + col * 38, y: i === 0 ? 206 : 226 + (i - 1) * 11 }; +}); + +// B3–B4 — the stack (local coordinates; the stackPos channel moves it) +const COLS = [175, 310, 445, 580]; +const NODES = { + a: { x: 70, y: 405 }, + g1: { x: 175, y: 340 }, + g2: { x: 175, y: 470 }, + ab: { x: 310, y: 405 }, + p10: { x: 445, y: 340 }, + p12: { x: 445, y: 470 }, + res: { x: 580, y: 405 }, + out: { x: 722, y: 405 }, +}; +type NodeKey = keyof typeof NODES; +const CHIP_W = 120; +const CHIP_H = 40; +const EDGES: Array<[NodeKey, NodeKey, number]> = [ + ['a', 'g1', 0], + ['a', 'g2', 0], + ['g1', 'ab', 1], + ['g2', 'ab', 1], + ['ab', 'p10', 2], + ['ab', 'p12', 2], + ['p10', 'res', 3], + ['p12', 'res', 3], + ['res', 'out', 4], +]; +const DEPTH: Record = { a: 0, g1: 1, g2: 1, ab: 2, p10: 3, p12: 3, res: 4, out: 5 }; +const edgeEnds = (from: NodeKey, to: NodeKey) => { + const A = NODES[from]; + const B = NODES[to]; + const ax = from === 'a' ? A.x + 20 : A.x + CHIP_W / 2; + const bx = to === 'out' ? B.x : B.x - CHIP_W / 2; + return { x1: ax, y1: A.y, x2: bx, y2: B.y }; +}; +// layer tiles: 6×6 sparse matrices, seeded +const TILES = (() => { + const rnd = mulberry32(5); + return COLS.map((_, l) => + Array.from({ length: 6 }, () => Array.from({ length: 6 }, () => (rnd() < (l === 0 ? 0.2 : 0.16) ? 1 : 0))), + ); +})(); +const TILE = { y: 168, cell: 14, gap: 4 }; +const TILE_SIZE = 6 * TILE.cell + 5 * TILE.gap; +const STACK_CENTER: CameraState = { x: 265, y: 20, k: 1 }; // stackPos offset that centres the stack on the stage +const STACK_HOME: CameraState = { x: 0, y: 0, k: 1 }; + +// the result plot (B3) that becomes a hard bit (B4) +const RX = scaleLinear([2, 7], [742, 1200]); +const PY_RES = scaleLinear([9.4, 12.6], [400, 170]); +const PY_BIT = scaleLinear([-0.25, 1.25], [400, 170]); +const ID = scaleLinear([0, 1], [0, 1]); +const RES_PX = (a: number): number => PY_RES(RESULT(a)); +const BIT_PX = (a: number): number => PY_BIT(HARD(a)); +const GATE = { x: 668, y: 405 }; +const WALL_X = 702; +const GDOT_FROM = { x: RX(4.5), y: (PY_BIT(0) + PY_BIT(1)) / 2 }; + +// B5 — 9,909 placeholder arcs (seeded), one path per chunk +const ARC_CHUNKS = (() => { + const rnd = mulberry32(9909); + const g = gaussian(mulberry32(130)); + const step = 1400 / DESKTOP.layers; + const chunks: string[][] = Array.from({ length: 6 }, () => []); + for (let i = 0; i < DESKTOP.weights; i++) { + const L = Math.floor(rnd() * DESKTOP.layers); + const x1 = -60 + L * step; + const x2 = x1 + (1 + Math.floor(rnd() * 5)) * step; + const y1 = 20 + rnd() * 680; + const y2 = Math.max(10, Math.min(710, y1 + g() * 46)); + const cx = (x1 + x2) / 2; + const cy = Math.min(y1, y2) - 6 - rnd() * 34; + chunks[i % 6].push(`M${x1.toFixed(0)} ${y1.toFixed(0)}Q${cx.toFixed(0)} ${cy.toFixed(0)} ${x2.toFixed(0)} ${y2.toFixed(0)}`); + } + check(chunks.reduce((n, c) => n + c.length, 0) === DESKTOP.weights, '9,909 arcs'); + return chunks.map((c) => c.join('')); +})(); + +// B6 — the learned gallery's network and its 244 weights +const NET = (() => { + const col = (x: number, n: number, y0: number, y1: number) => + Array.from({ length: n }, (_, i) => ({ x, y: n === 1 ? (y0 + y1) / 2 : y0 + ((y1 - y0) * i) / (n - 1) })); + const layers = [col(90, 9, 186, 386), col(215, 6, 206, 366), col(340, 6, 206, 366), col(462, 4, 236, 336)]; + const paths = [0, 1, 2].map((l) => { + let d = ''; + for (const a of layers[l]) for (const b of layers[l + 1]) d += `M${a.x} ${a.y.toFixed(1)}L${b.x} ${b.y.toFixed(1)}`; + return d; + }); + return { layers, paths }; +})(); +const WEIGHT_DOTS = (() => { + const rnd = mulberry32(244); + return Array.from({ length: GALLERY.cells }, (_, i) => ({ + x: 196 + (i % 21) * 9, + y: 440 + Math.floor(i / 21) * 9, + ph: rnd(), + })); +})(); +check(WEIGHT_DOTS.length === 244 && Math.floor(243 / 21) === 11, '244 weight dots, 12 rows (last partial)'); +const ACCENT_TO_VIOLET = interpolateRgb(colors.ACCENT, colors.SECONDARY); +// the sketch: a 5-wide, 4-tall stage (Story preset: 10 rectangles), schematic target layout +const SK = { x: 690, y: 176, w: 420, h: 336 }; +const SK_UNIT = SK.w / 5; +check(Math.abs(SK.h / SK_UNIT - 4) < 1e-9, 'sketch stage is 5:4'); +const TARGETS = [ + [0, 0, 2, 1.6], + [2, 0, 1.5, 1.6], + [3.5, 0, 1.5, 0.8], + [3.5, 0.8, 1.5, 0.8], + [0, 1.6, 1.2, 1.2], + [0, 2.8, 1.2, 1.2], + [1.2, 1.6, 2.2, 2.4], + [3.4, 1.6, 1.6, 1.0], + [3.4, 2.6, 0.8, 1.4], + [4.2, 2.6, 0.8, 1.4], +].map(([x, y, w, h]) => ({ x: SK.x + x * SK_UNIT + 4, y: SK.y + y * SK_UNIT + 4, w: w * SK_UNIT - 8, h: h * SK_UNIT - 8 })); +const PHOTO_HUES = ['#5b7fa6', '#7a9a6b', '#a77f5b', '#8c6fa6', '#5f9b9b', '#a6687a', '#6b7fa6', '#9a8f5b', '#6b9a84', '#9a6b5b']; +const PILE = (() => { + const rnd = mulberry32(28); + return TARGETS.map((_, i) => ({ + cx: SK.x + SK.w / 2 + (rnd() - 0.5) * 50, + cy: SK.y + SK.h / 2 + (rnd() - 0.5) * 40, + rot: (rnd() - 0.5) * 40, + delay: 0.05 * i + rnd() * 0.12, + hue: PHOTO_HUES[i], + })); +})(); +check(TARGETS.length === 10, 'ten target rectangles'); + +// B7 — the 100 × 100 dot map: one dot per 10 documents, a seeded 10,559 / 100,000 share is copper +const MAP = { x: 70, y: 130, pitch: 4.4, n: 100, sq: 3.2 }; +const MAP_PATHS = (() => { + const total = MAP.n * MAP.n; + const nMatch = Math.round((total * MATCHES) / DOCS); + const rnd = mulberry32(10559); + const order = Array.from({ length: total }, (_, i) => i); + for (let i = total - 1; i > 0; i--) { + const j = Math.floor(rnd() * (i + 1)); + [order[i], order[j]] = [order[j], order[i]]; + } + const isMatch = new Uint8Array(total); + for (let i = 0; i < nMatch; i++) isMatch[order[i]] = 1; + let base = ''; + let copper = ''; + for (let i = 0; i < total; i++) { + const x = MAP.x + (i % MAP.n) * MAP.pitch; + const y = MAP.y + Math.floor(i / MAP.n) * MAP.pitch; + const d = `M${x.toFixed(1)} ${y.toFixed(1)}h${MAP.sq}v${MAP.sq}h-${MAP.sq}z`; + if (isMatch[i]) copper += d; + else base += d; + } + return { base, copper, nMatch, total }; +})(); +check(MAP_PATHS.total === 10_000 && MAP_PATHS.nMatch === 1056, '10,000 dots, 1,056 copper (10.6 %)'); +const ONE_DOT = { x: MAP.x + 50 * MAP.pitch + MAP.sq / 2, y: MAP.y + 50 * MAP.pitch + MAP.sq / 2 }; + +// B8 — log-time ruler, 1 ms … 100 s +const LX = (sec: number): number => 580 + 124 * Math.log10(sec / 0.001); +const RULER = { rows: [352, 394, 436, 478], axisY: 516 }; +check(LX(0.008) < LX(0.011) && LX(0.015) < LX(0.028) && LX(0.028) < LX(2.1) && LX(2.1) < LX(13) && LX(100) === 1200, 'log ruler order'); + +// B9 — five recap panels +const PANEL_W = 210; +const PANEL_H = 170; +const PANEL_Y = 250; +const PANEL_X = (i: number): number => 71 + i * 232; +const PANEL_TITLES = ['Both Branches Run', 'A Switch of Arithmetic', 'The Host Loop', 'Loops That Cannot Stop', 'Exact, Trainable, Wide']; + +// camera stops +const CAM_DLINE: CameraState = { x: 650, y: 240, k: 1.35 }; +const CAM_STACK: CameraState = { x: 640, y: 360, k: 0.95 }; +const CAM_WALL: CameraState = { x: 640, y: 385, k: 1.0 }; // wide enough that the input cell a stays on stage +const CAM_WIDE: CameraState = { x: 640, y: 360, k: 0.9 }; +const CAM_NET: CameraState = { x: 330, y: 360, k: 1.3 }; +const CAM_MAP: CameraState = { x: 400, y: 360, k: 1.2 }; +const CAM_RULER: CameraState = { x: 880, y: 410, k: 1.25 }; + +const PIN = 'chenglou/handwired@4ea7fcb'; +const SOURCES = [ + 'docs/execution-model.md:118, 123', + 'docs/execution-model.md:134-136 · docs/tutorial.md:53', + 'docs/execution-model.md:257 · docs/tutorial.md:167', + 'README.md:29 · docs/execution-model.md:21-36, 112', + 'src/runtime/numeric-ui-core.ts:719-722 · docs/execution-model.md:36, 143', + 'src/handwiredos/README.md:3, 13, 42', + 'src/learned-gallery/README.md:3, 15, 17, 28', + 'src/json-search/README.md:3, 30, 47', + 'src/json-search/README.md:42, 53', + 'README.md:20 · docs/README.md:9', + 'README.md:20 · docs/README.md:9', + 'docs/expressions.md:11 · docs/execution-model.md:264', +]; + +// --------------------------------------------------------------------------- +// Timeline — twelve fixed captions, ≈ 116.6 s +// --------------------------------------------------------------------------- + +export function buildScene() { + const tl = new Timeline(); + const cam = tl.channel('cam', CAMERA_HOME, cameraInterp); + const src = tl.channel('src', 0); + const chromeU = tl.channel('chromeU', 1); + + // the persistent stack + const stackOp = tl.channel('stackOp', 0.03); + const stackTextU = tl.channel('stackTextU', 1); + const tileDim = tl.channel('tileDim', 0); + const stackPos = tl.channel('stackPos', STACK_CENTER); + + // B0 + const b0Op = tl.channel('b0Op', 1); + const dlineU = tl.channel('dlineU', 0); + const plot1U = tl.channel('plot1U', 0); + const plot2U = tl.channel('plot2U', 0); + const plot3U = tl.channel('plot3U', 0); + const dropU = tl.channel('dropU', 0); + const overlayU = tl.channel('overlayU', 0); + const lanesU = tl.channel('lanesU', 0); + const eqTickU = tl.channel('eqTickU', 0); + const quote0U = tl.channel('quote0U', 0); + // B1 + const b1Op = tl.channel('b1Op', 1); + const gridBU = tl.channel('gridBU', 0); + const stripAU = tl.channel('stripAU', 0); + const cascU = tl.channel('cascU', 0); + const stripBU = tl.channel('stripBU', 0); + const combU = tl.channel('combU', 0); + // B2 + const b2Op = tl.channel('b2Op', 1); + const decU = tl.channel('decU', 0); + const posU = tl.channel('posU', 0); + const loupeU = tl.channel('loupeU', 0); + const quote2U = tl.channel('quote2U', 0); + // B3–B4 + const b34Op = tl.channel('b34Op', 1); + const plotU = tl.channel('plotU', 0); + const eq3U = tl.channel('eq3U', 0); + const fwdU = tl.channel('fwdU', 0); + const markU = tl.channel('markU', 0); + const aMark = tl.channel('aMark', 2.5); + const bwdU = tl.channel('bwdU', 0); + const lampU = tl.channel('lampU', 0); + const readU = tl.channel('readU', 0); + const chipInfoU = tl.channel('chipInfoU', 1); + const hardU = tl.channel('hardU', 0); + const gateU = tl.channel('gateU', 0); + const gdotU = tl.channel('gdotU', 0); + const wallU = tl.channel('wallU', 0); + const errU = tl.channel('errU', 0); + const stU = tl.channel('stU', 0); + const passU = tl.channel('passU', 0); + const noteU = tl.channel('noteU', 0); + // B5 + const arcsU = tl.channel('arcsU', 0); + const mockU = tl.channel('mockU', 0); + const countU = tl.channel('countU', 0); + const b5TextU = tl.channel('b5TextU', 0); + const lock5U = tl.channel('lock5U', 0); + // B6 + const b6Op = tl.channel('b6Op', 1); + const netU = tl.channel('netU', 0); + const wdotU = tl.channel('wdotU', 0); + const unlockU = tl.channel('unlockU', 0); + const rectU = tl.channel('rectU', 0); + const pileU = tl.channel('pileU', 0); + const fitU = tl.channel('fitU', 0); + const b6TagU = tl.channel('b6TagU', 0); + // B7–B8 + const b78Op = tl.channel('b78Op', 1); + const mapU = tl.channel('mapU', 0); + const oneU = tl.channel('oneU', 0); + const barU = tl.channel('barU', 0); + const mapLit = tl.channel('mapLit', 0); + const flashU = tl.channel('flashU', 0); + const matchU = tl.channel('matchU', 0); + const mapTagU = tl.channel('mapTagU', 0); + const cardU = tl.channel('cardU', 0); + const mapDim = tl.channel('mapDim', 0); + const rulerU = tl.channel('rulerU', 0); + const band1 = tl.channel('band1', 0); + const band2 = tl.channel('band2', 0); + const band3 = tl.channel('band3', 0); + const band4 = tl.channel('band4', 0); + const overlapU = tl.channel('overlapU', 0); + // B9–B10 + const panelsU = tl.channel('panelsU', 0); + const lit1 = tl.channel('lit1', 0); + const lit2 = tl.channel('lit2', 0); + const lit3 = tl.channel('lit3', 0); + const lit4 = tl.channel('lit4', 0); + const panelsOut = tl.channel('panelsOut', 0); + const closeU = tl.channel('closeU', 0); + const closeEqU = tl.channel('closeEqU', 0); + + const CAP = [0.4, 10.6, 18.2, 28.5, 39.9, 52.1, 61.6, 71.5, 83.2, 94.4, 99.5, 105.4]; + CAP.forEach((at, i) => tl.set(src, i, at - 0.1)); + + // — B0 · the exact comparison bit on the D-line — + tl.caption({ at: 0.4, dur: 9.8, text: "Chapter one's ramps held only for whole numbers. The real comparison is exact: for every pair of values in the declared ranges, its bit equals Javascript's." }); + tl.tween(cam, CAM_DLINE, { at: 0.4, dur: 1.3, ease: ease.move }); + tl.tween(dlineU, 1, { at: 0.6, dur: 1.2, ease: ease.draw }); + tl.tween(plot1U, 1, { at: 1.4, dur: 1.0, ease: ease.draw }); + tl.tween(plot2U, 1, { at: 2.4, dur: 1.0, ease: ease.draw }); + tl.tween(plot3U, 1, { at: 3.4, dur: 1.0, ease: ease.draw }); + tl.tween(dropU, 1, { at: 4.4, dur: 1.1, ease: ease.move }); + tl.tween(overlayU, 1, { at: 5.0, dur: 0.9, ease: ease.draw }); + tl.tween(cam, CAMERA_HOME, { at: 5.8, dur: 1.2, ease: ease.move }); + tl.tween(lanesU, 1, { at: 6.4, dur: 1.2, ease: ease.linear }); + tl.tween(eqTickU, 1, { at: 7.6, dur: 0.9, ease: ease.linear }); + tl.tween(quote0U, 1, { at: 8.3, dur: 0.7, ease: ease.enter }); + tl.hold(10.2, 0.4); + + // — B1 · two operations on a grid, fifteen without — + tl.caption({ at: 10.6, dur: 7.2, text: 'On a declared grid the comparison is two operations. Without a grid it takes fifteen, with the same bits.' }); + tl.tween(b0Op, 0, { at: 10.4, dur: 0.6, ease: ease.move }); + tl.tween(stackOp, 0, { at: 9.2, dur: 0.6, ease: ease.move }); + tl.tween(gridBU, 1, { at: 10.9, dur: 0.8, ease: ease.enter }); + tl.tween(stripAU, 1, { at: 11.8, dur: 1.2, ease: ease.linear }); + tl.tween(cascU, 1, { at: 13.6, dur: 1.4, ease: ease.draw }); + tl.tween(stripBU, 1, { at: 15.1, dur: 1.2, ease: ease.linear }); + tl.tween(combU, 1, { at: 16.4, dur: 0.6, ease: ease.enter }); + tl.hold(17.8, 0.4); + + // — B2 · exact bits, positions within a stated bound — + tl.caption({ at: 18.2, dur: 9.9, text: "Exact means exact bits. Positions can still differ from sixty-four-bit floats, by at most 1.3 millionths of a pixel across the toggle's travel." }); + tl.tween(b1Op, 0, { at: 18.0, dur: 0.6, ease: ease.move }); + tl.tween(decU, 1, { at: 18.6, dur: 0.8, ease: ease.enter }); + tl.tween(posU, 1, { at: 20.2, dur: 1.4, ease: ease.draw }); + tl.tween(loupeU, 1, { at: 21.8, dur: 0.9, ease: ease.enter }); + tl.tween(quote2U, 1, { at: 23.4, dur: 0.7, ease: ease.enter }); + tl.hold(28.1, 0.4); + + // — B3 · forward, then the gradient back through the sums, products and relus — + tl.caption({ at: 28.5, dur: 11.0, text: 'Almost every operation is a sum, a product or a relu, so the program can run backwards. Backpropagation sends a gradient, how much each number should change, back through them.' }); + tl.tween(b2Op, 0, { at: 28.3, dur: 0.6, ease: ease.move }); + tl.tween(cam, CAM_STACK, { at: 28.5, dur: 1.2, ease: ease.move }); + tl.tween(stackPos, STACK_HOME, { at: 28.5, dur: 1.2, ease: ease.move }); + tl.tween(stackOp, 1, { at: 28.6, dur: 0.9, ease: ease.enter }); + tl.tween(plotU, 1, { at: 29.2, dur: 1.2, ease: ease.draw }); + tl.tween(markU, 1, { at: 29.6, dur: 0.5, ease: ease.enter }); + tl.tween(aMark, 4.5, { at: 29.8, dur: 0.9, ease: ease.move }); + tl.tween(eq3U, 1, { at: 30, dur: 0.6, ease: ease.enter }); + tl.tween(fwdU, 1, { at: 31.2, dur: 2.2, ease: ease.linear }); + tl.tween(bwdU, 1, { at: 34.6, dur: 2.0, ease: ease.linear }); + tl.tween(lampU, 1, { at: 35.8, dur: 0.5, ease: ease.enter }); + tl.tween(readU, 1, { at: 36.5, dur: 0.5, ease: ease.enter }); + tl.tween(aMark, 3, { at: 37.3, dur: 0.7, ease: ease.move }); + tl.tween(aMark, 6, { at: 38.3, dur: 1.0, ease: ease.move }); + tl.hold(39.5, 0.4); + + // — B4 · a hard decision has no slope; straight-through passes it on — + tl.caption({ at: 39.9, dur: 11.8, text: 'But a hard decision has no slope. At a score rounded to zero or one, the backward pass throws, unless you pick a stand-in called straight-through, which passes the gradient along unchanged.' }); + tl.tween(cam, CAM_WALL, { at: 39.9, dur: 1.2, ease: ease.move }); + tl.tween(readU, 0, { at: 39.9, dur: 0.5, ease: ease.move }); + tl.tween(eq3U, 0, { at: 39.9, dur: 0.5, ease: ease.move }); + tl.tween(markU, 0, { at: 39.9, dur: 0.5, ease: ease.move }); + tl.tween(lampU, 0, { at: 39.9, dur: 0.5, ease: ease.move }); + tl.tween(chipInfoU, 0, { at: 39.9, dur: 0.5, ease: ease.move }); + tl.tween(tileDim, 1, { at: 39.9, dur: 0.6, ease: ease.move }); + tl.tween(hardU, 1, { at: 40.3, dur: 1.3, ease: ease.move }); + tl.tween(gateU, 1, { at: 42.2, dur: 0.6, ease: ease.enter }); + tl.tween(gdotU, 1, { at: 44.8, dur: 1.3, ease: ease.linear }); + tl.tween(wallU, 1, { at: 46.1, dur: 0.4, ease: ease.pop }); + tl.tween(errU, 1, { at: 46.6, dur: 0.6, ease: ease.enter }); + tl.tween(stU, 1, { at: 48.6, dur: 1.0, ease: ease.move }); + tl.tween(passU, 1, { at: 49.7, dur: 1.2, ease: ease.linear }); + tl.tween(noteU, 1, { at: 50.8, dur: 0.6, ease: ease.enter }); + tl.hold(51.7, 0.4); + + // — B5 · the frozen case: the desktop demo — + tl.caption({ at: 52.1, dur: 9.1, text: 'The desktop demo is the frozen case: three windows and a dock, run by 234 cells, 4,646 live operations and 9,909 weights, none trained.' }); + tl.tween(b34Op, 0, { at: 51.9, dur: 0.6, ease: ease.move }); + tl.tween(stackOp, 0.025, { at: 51.9, dur: 0.8, ease: ease.move }); + tl.tween(stackPos, STACK_CENTER, { at: 51.9, dur: 1.0, ease: ease.move }); + tl.tween(tileDim, 0, { at: 51.9, dur: 0.8, ease: ease.move }); + tl.tween(gateU, 0, { at: 51.9, dur: 0.6, ease: ease.move }); + tl.tween(cam, CAM_WIDE, { at: 52.1, dur: 1.4, ease: ease.move }); + tl.tween(arcsU, 1, { at: 52.4, dur: 2.0, ease: ease.linear }); + tl.tween(mockU, 1, { at: 53.2, dur: 1.0, ease: ease.draw }); + tl.tween(countU, 1, { at: 54.6, dur: 2.4, ease: ease.move }); + tl.tween(b5TextU, 1, { at: 57.2, dur: 0.7, ease: ease.enter }); + tl.tween(lock5U, 1, { at: 58.2, dur: 0.6, ease: ease.enter }); + tl.hold(61.2, 0.4); + + // — B6 · the learned gallery unfreezes the weights — + tl.caption({ at: 61.6, dur: 9.5, text: "The learned gallery unfreezes the weights: its 244 cells are a small network's coefficients, trained with Adam, a standard optimizer, at six hundred steps a second." }); + tl.tween(arcsU, 0.35, { at: 61.5, dur: 0.8, ease: ease.move }); + tl.tween(mockU, 0, { at: 61.5, dur: 0.8, ease: ease.move }); + tl.tween(countU, 0, { at: 61.5, dur: 0.6, ease: ease.move }); + tl.tween(b5TextU, 0, { at: 61.5, dur: 0.6, ease: ease.move }); + tl.tween(cam, CAM_NET, { at: 61.6, dur: 1.2, ease: ease.move }); + tl.tween(netU, 1, { at: 61.9, dur: 1.4, ease: ease.draw }); + tl.tween(wdotU, 1, { at: 62.8, dur: 0.9, ease: ease.linear }); + tl.tween(lock5U, 2, { at: 63.6, dur: 0.6, ease: ease.pop }); + tl.tween(unlockU, 1, { at: 63.6, dur: 0.6, ease: ease.enter }); + tl.tween(cam, { x: 640, y: 350, k: 1.0 }, { at: 64.6, dur: 1.2, ease: ease.move }); + tl.tween(rectU, 1, { at: 65, dur: 0.8, ease: ease.draw }); + tl.tween(pileU, 1, { at: 65.2, dur: 0.5, ease: ease.enter }); + tl.tween(b6TagU, 1, { at: 65.4, dur: 0.6, ease: ease.enter }); + tl.tween(fitU, 1, { at: 65.9, dur: 5.2, ease: ease.linear }); + tl.hold(71.1, 0.4); + + // — B7 · one pass over a hundred thousand documents — + tl.caption({ at: 71.5, dur: 11.3, text: 'The last demo searches a hundred thousand generated Javascript Object Notation documents per keystroke, on a graphics card: every document runs the same operations, so one pass runs them all.' }); + tl.tween(b6Op, 0, { at: 71.4, dur: 0.6, ease: ease.move }); + tl.tween(arcsU, 0, { at: 71.4, dur: 0.6, ease: ease.move }); + tl.tween(mockU, 0, { at: 71.4, dur: 0.6, ease: ease.move }); + tl.tween(lock5U, 0, { at: 71.4, dur: 0.4, ease: ease.move }); + tl.tween(cam, CAM_MAP, { at: 71.5, dur: 1.2, ease: ease.move }); + tl.tween(mapU, 1, { at: 71.9, dur: 0.8, ease: ease.enter }); + tl.tween(oneU, 1, { at: 72.6, dur: 0.5, ease: ease.pop }); + tl.tween(barU, 1, { at: 73, dur: 0.9, ease: ease.draw }); + tl.tween(barU, 0, { at: 75.8, dur: 0.5, ease: ease.move }); + tl.set(mapLit, 1, 74.4); + tl.tween(flashU, 1, { at: 76, dur: 0.3, ease: ease.pop }); + tl.tween(flashU, 0, { at: 76.4, dur: 0.8, ease: ease.move }); + tl.tween(matchU, 1, { at: 76.5, dur: 0.5, ease: ease.enter }); + tl.tween(oneU, 0, { at: 76.5, dur: 0.5, ease: ease.move }); + tl.tween(mapTagU, 1, { at: 77, dur: 0.6, ease: ease.enter }); + tl.tween(cam, { x: 640, y: 360, k: 1.0 }, { at: 77.8, dur: 1.2, ease: ease.move }); + tl.tween(cardU, 1, { at: 78.4, dur: 0.8, ease: ease.enter }); + tl.hold(82.8, 0.4); + + // — B8 · the log-time ruler: neither is always faster — + tl.caption({ at: 83.2, dur: 10.8, text: 'Measured in the repo, a search takes eleven to fifteen milliseconds a keystroke. Regular expressions take eight to twenty-eight, or two seconds when they backtrack. Neither is always faster.' }); + tl.tween(cam, CAM_RULER, { at: 83.2, dur: 1.2, ease: ease.move }); + tl.tween(mapDim, 1, { at: 83.2, dur: 0.8, ease: ease.move }); + tl.tween(stackOp, 0, { at: 83.2, dur: 0.8, ease: ease.move }); + tl.tween(rulerU, 1, { at: 83.6, dur: 1.0, ease: ease.draw }); + tl.tween(band1, 1, { at: 85.7, dur: 0.7, ease: ease.enter }); + tl.tween(band2, 1, { at: 87.9, dur: 0.7, ease: ease.enter }); + tl.tween(band3, 1, { at: 90.6, dur: 0.7, ease: ease.enter }); + tl.tween(band4, 1, { at: 91.4, dur: 0.7, ease: ease.enter }); + tl.tween(overlapU, 1, { at: 92.5, dur: 0.6, ease: ease.enter }); + tl.hold(94, 0.4); + + // — B9 · the recap, lit in the spoken order — + tl.caption({ at: 94.4, dur: 5.1, text: 'From the top: a decision runs both branches and a bit picks one.' }); + tl.caption({ at: 99.5, dur: 5.5, text: 'A loop becomes a count, a tree or a table. The host feeds the cells.' }); + tl.tween(b78Op, 0, { at: 94.2, dur: 0.7, ease: ease.move }); + tl.tween(stackOp, 0.025, { at: 94.4, dur: 0.8, ease: ease.move }); + tl.tween(cam, CAMERA_HOME, { at: 94.4, dur: 1.2, ease: ease.move }); + tl.tween(panelsU, 1, { at: 94.7, dur: 1.2, ease: ease.linear }); + tl.tween(lit1, 1, { at: 95.7, dur: 0.5, ease: ease.enter }); + tl.tween(lit2, 1, { at: 96, dur: 0.5, ease: ease.enter }); + tl.tween(lit4, 1, { at: 99.1, dur: 0.5, ease: ease.enter }); + tl.tween(lit3, 1, { at: 102.4, dur: 0.5, ease: ease.enter }); + tl.hold(105, 0.4); + + // — B10 · the closing line: a bit lets one answer through — + tl.caption({ at: 105.4, dur: 8.7, text: 'The program never decides anything. It computes every answer, and a bit that is exactly zero or one lets one of them through.' }); + tl.tween(panelsOut, 1, { at: 105.3, dur: 0.8, ease: ease.move }); + tl.tween(chromeU, 0, { at: 105.4, dur: 0.8, ease: ease.move }); + tl.tween(stackOp, 0.1, { at: 105.6, dur: 1.0, ease: ease.move }); + tl.tween(stackTextU, 0, { at: 105.4, dur: 0.6, ease: ease.move }); + tl.tween(closeU, 1, { at: 106.2, dur: 0.8, ease: ease.enter }); + tl.tween(closeEqU, 1, { at: 107.5, dur: 0.7, ease: ease.enter }); + tl.hold(114.1, 2.5); + + return { + tl, cam, src, chromeU, stackOp, stackTextU, tileDim, stackPos, + b0Op, dlineU, plot1U, plot2U, plot3U, dropU, overlayU, lanesU, eqTickU, quote0U, + b1Op, gridBU, stripAU, cascU, stripBU, combU, + b2Op, decU, posU, loupeU, quote2U, + b34Op, plotU, eq3U, fwdU, markU, aMark, bwdU, lampU, readU, chipInfoU, hardU, gateU, gdotU, wallU, errU, stU, passU, noteU, + arcsU, mockU, countU, b5TextU, lock5U, + b6Op, netU, wdotU, unlockU, rectU, pileU, fitU, b6TagU, + b78Op, mapU, oneU, barU, mapLit, flashU, matchU, mapTagU, cardU, mapDim, rulerU, band1, band2, band3, band4, overlapU, + panelsU, lit1, lit2, lit3, lit4, panelsOut, closeU, closeEqU, + }; +} + +const scene = buildScene(); +type S = typeof scene; + +// --------------------------------------------------------------------------- +// Glyph kit (local copy, per the book-wide spec) +// --------------------------------------------------------------------------- + +function ReluGlyph({ x, y, color = colors.SECONDARY, opacity = 1 }: { x: number; y: number; color?: string; opacity?: number }) { + return ; +} + +function BitDisc({ x, y, b, r = 11, opacity = 1, label = true }: { x: number; y: number; b: number; r?: number; opacity?: number; label?: boolean }) { + if (opacity <= 0.005) return null; + const on = b >= 0.5; + return ( + + + {label && r >= 11 && ( + + {on ? '1' : '0'} + + )} + + ); +} + +function LensRing({ x, y, r, lx, ly, opacity = 1 }: { x: number; y: number; r: number; lx?: number; ly?: number; opacity?: number }) { + if (opacity <= 0.005) return null; + const ang = lx !== undefined && ly !== undefined ? Math.atan2(ly - y, lx - x) : 0; + return ( + + + {lx !== undefined && ly !== undefined && ( + + )} + + ); +} + +function Lock({ x, y, open, color = colors.MUTED }: { x: number; y: number; open: number; color?: string }) { + return ( + + + + + ); +} + +function CodeChip({ x, y, lines, stripe, opacity = 1, fontSize = 14, dimFirst = 0 }: { x: number; y: number; lines: string[]; stripe: string; opacity?: number; fontSize?: number; dimFirst?: number }) { + if (opacity <= 0.005) return null; + const w = Math.max(...lines.map((l) => l.length)) * fontSize * 0.6 + 26; + const h = lines.length * (fontSize + 8) + 10; + return ( + + + + {lines.map((l, i) => ( + + {l} + + ))} + + ); +} + +function QuoteCard({ cx, y, text, opacity, w }: { cx: number; y: number; text: string; opacity: number; w: number }) { + if (opacity <= 0.005) return null; + return ( + + + + + “{text}” + + + ); +} + +// --------------------------------------------------------------------------- +// Beat renderers +// --------------------------------------------------------------------------- + +function StackLayer(s: SceneState, sc: S): ReactNode { + const op = s.get(sc.stackOp); + if (op <= 0.005) return null; + const pos = s.get(sc.stackPos); + const fwd = s.get(sc.fwdU); + const bwd = s.get(sc.bwdU); + const a = s.get(sc.aMark); + const lamp = s.get(sc.lampU); + const info = s.get(sc.chipInfoU); + const gate = s.get(sc.gateU); + const pass = s.get(sc.passU); + const b34 = s.get(sc.b34Op); + const tdim = s.get(sc.tileDim); + const txt = s.get(sc.stackTextU); + const fwdReach = (k: NodeKey) => clamp01(fwd * 5 - DEPTH[k] + 1); + const bwdReach = (k: NodeKey) => clamp01(bwd * 5 - (4 - DEPTH[k])); + const vals: Record = { a, g1: G1(a), g2: G2(a), ab: ABOVE4(a), p10: ABOVE4(a) * 10, p12: (1 - ABOVE4(a)) * 12, res: RESULT(a) }; + const grads: Record = { res: GRAD.res, p10: GRAD.p10, p12: GRAD.p12, ab: GRAD.ab, g1: GRAD.g1, g2: GRAD.g2, a: DRESULT_DA(a) }; + const names: Record = { g1: 'relu(a−4)', g2: 'relu(a−5)', ab: 'above4', p10: 'above4·10', p12: '(1−above4)·12', res: 'result' }; + const slopes: Record = { g1: SLOPE1(a), g2: SLOPE2(a) }; + return ( + + {/* layer tiles: sparse weight matrices, chapter-1 look */} + {COLS.map((cx, l) => ( + + + layer {l + 1} + + (v > 0 ? (l === 0 ? colors.SECONDARY : colors.ACCENT) : DIM_CELL)} + /> + + ))} + {/* edges */} + {EDGES.map(([f, t, d], i) => { + const e = edgeEnds(f, t); + const isOut = t === 'out'; + return ( + + + {/* forward dots (amber), one per edge, layer by layer */} + {fwd > 0 && fwd < 1 && (() => { + const u = clamp01(fwd * 5 - d); + if (u <= 0 || u >= 1) return null; + return ; + })()} + {/* backward dots (teal), right to left */} + {bwd > 0 && bwd < 1 && (() => { + const u = clamp01(bwd * 5 - (4 - d)); + if (u <= 0 || u >= 1) return null; + return ; + })()} + + ); + })} + {/* the input cell a */} + + + a + + + + + {fmtNum(a)} + + {bwdReach('a') > 0 && ( + + grad {fmtNum(grads.a)} + + )} + + {/* operation chips */} + {(['g1', 'g2', 'ab', 'p10', 'p12', 'res'] as NodeKey[]).map((k) => { + const n = NODES[k]; + const isGate = k === 'g1' || k === 'g2'; + const lit = isGate && slopes[k] === 1; + const fr = fwdReach(k) * info; + const br = bwdReach(k) * info; + return ( + + + {isGate && } + {isGate && 0.5 && lit ? colors.WARM : colors.SECONDARY} />} + + {names[k]} + + {fr > 0 && ( + + = {fmtNum(vals[k])} + + )} + {br > 0 && ( + + + + grad {fmtNum(grads[k])} + + + )} + {isGate && lamp > 0.01 && ( + + + + slope {slopes[k]} + + + )} + + ); + })} + {/* B3: what the teal pills are, and what they flow back through */} + {bwd > 0.01 && info > 0.01 && ( + + + gradient: how much each number should change + + + sent back through the sums, products and relus + + + )} + {/* B4: the boolean atom on the output */} + {gate > 0.01 && ( + + + + + boolean atom + + {pass > 0.95 && ( + + grad 1 in · grad 1 out + + )} + + )} + + ); +} + +function B0(s: SceneState, sc: S): ReactNode { + const op = s.get(sc.b0Op); + if (op <= 0.005) return null; + const dl = s.get(sc.dlineU); + const pu = [s.get(sc.plot1U), s.get(sc.plot2U), s.get(sc.plot3U)]; + const drop = s.get(sc.dropU); + const ov = s.get(sc.overlayU); + const lanes = s.get(sc.lanesU); + const eqT = s.get(sc.eqTickU); + const P3 = PLOTS[2]; + return ( + + {/* three plots */} + {PLOTS.map((p, i) => { + const u = pu[i]; + if (u <= 0.005) return null; + return ( + + + + + + {D_INTS.map((d, j) => { + const vis = clamp01(u * 9 - j); + if (vis <= 0) return null; + return ; + })} + + {i < 2 && } + + ); + })} + {/* plot 3 tick labels + the chapter-1 ramp difference, dotted, coinciding */} + {pu[2] > 0.01 && ( + + 0 + 1 + + )} + {ov > 0.01 && ( + + + + + dotted: equal at every integer + + + )} + {/* the D-line */} + {dl > 0.005 && ( + + + + + + declared range + + D + + {D_INTS.map((d, j) => { + const vis = clamp01(dl * 9 - j); + if (vis <= 0) return null; + const bx = DX(d); + // the bit dot flies down from plot 3 and lands on the integer + const u = clamp01(drop * 1.5 - j * 0.05); + const fx = lerp(P3.X(d), bx, u); + const fy = lerp(P3.Y(BIT(d)), DLINE_Y, u); + return ( + + + + {String(d).replace('-', '−')} + + {drop > 0.001 && u < 1 && } + {u >= 1 && } + + ); + })} + + + )} + {/* lanes: JavaScript's a >= b and the built bit, nine ticks */} + {lanes > 0.005 && ( + + + JavaScript a {'>='} b + + + built bit + + {D_INTS.map((d, j) => { + const v = clamp01(lanes * 10 - j); + const t = clamp01(eqT * 10 - j); + return ( + + = 0 ? colors.TEXT : colors.MUTED} fontSize={14} fontFamily={MONO} opacity={v}> + {d >= 0 ? 'true' : 'false'} + + + + = + + + ); + })} + + )} + + + ); +} + +function B1(s: SceneState, sc: S): ReactNode { + const op = s.get(sc.b1Op) * clamp01(s.get(sc.gridBU) * 4); + if (op <= 0.005) return null; + const gb = s.get(sc.gridBU); + const sa = s.get(sc.stripAU); + const cs = s.get(sc.cascU); + const sb = s.get(sc.stripBU); + const cb = s.get(sc.combU); + return ( + + {/* left: grid declared */} + + + grid declared + + + D + + {[ + { x: 184, w: 120, t: 'relu(−D)' }, + { x: 336, w: 206, t: 'relu(1 − relu(−D))' }, + ].map((c, i) => ( + + + + + {c.t} + layer {i + 1} + + ))} + + + 2 operations, 2 layers + + + {/* right: no grid, a sign cascade (schematic) */} + + + no grid: sign cascade + + {STAIR.map((c, i) => { + const v = clamp01(cs * 15 - i); + return ( + + + {i > 0 && i < 14 && } + + ); + })} + + + 15 operations, 14 layers + + + for a {'>='} b · staircase is schematic + + + {/* the two strips */} + + + twelve samples of D + + {STRIP_D.map((d, i) => { + const x = STRIP.x0 + i * STRIP.pitch; + const ua = clamp01(sa * 12 - i); + const ub = clamp01(sb * 12 - i); + const uc = clamp01(cb * 12 - i); + return ( + + + {String(d).replace('-', '−')} + + + + + = + + + ); + })} + + from 2 operations + + + from 15 operations + + + same bits + + + + ); +} + +const SPRING = (t: number): number => 20 * (1 - Math.exp(-4 * t) * (Math.cos(6 * t) + (4 / 6) * Math.sin(6 * t))); +const SPX = scaleLinear([0, 1.6], [650, 960]); +const SPY = scaleLinear([-1, 24], [440, 236]); +const LOUPE_T = 0.42; +const LOUPE = { x: 1075, y: 318, r: 62 }; + +function B2(s: SceneState, sc: S): ReactNode { + const op = s.get(sc.b2Op) * clamp01(s.get(sc.decU) * 4); + if (op <= 0.005) return null; + const dec = s.get(sc.decU); + const pos = s.get(sc.posU); + const lp = s.get(sc.loupeU); + const px = SPX(LOUPE_T); + const py = SPY(SPRING(LOUPE_T)); + const slope = (SPY(SPRING(LOUPE_T + 0.01)) - SPY(SPRING(LOUPE_T - 0.01))) / (SPX(LOUPE_T + 0.01) - SPX(LOUPE_T - 0.01)); + const ang = (Math.atan(slope) * 180) / Math.PI; + return ( + + {/* decisions */} + + decisions + + + on + armed + ✓ exact + same bit as JavaScript, every frame + + + {/* positions */} + + positions + + + + 20 px + 0 + time · thumb offset (schematic) + + + {lp > 0.01 && ( + + + + + + + + + + + + + at most 1.3e-6 px + + + across a 20 px travel + + + the stated bound, not measured here + + + FP32 vs FP64 (double) + + + )} + + + ); +} + +function B34(s: SceneState, sc: S): ReactNode { + const op = s.get(sc.b34Op); + const pu = s.get(sc.plotU); + if (op <= 0.005 || pu <= 0.005) return null; + const hard = s.get(sc.hardU); + const a = s.get(sc.aMark); + const mk = s.get(sc.markU); + const eq = s.get(sc.eq3U); + const rd = s.get(sc.readU); + const fwd = s.get(sc.fwdU); + const gd = s.get(sc.gdotU); + const wall = s.get(sc.wallU); + const err = s.get(sc.errU); + const st = s.get(sc.stU); + const pass = s.get(sc.passU); + const note = s.get(sc.noteU); + const da = DRESULT_DA(a); + // the gradient dot: from the jump to the wall, then (straight-through) on to the result chip + const resRight = NODES.res.x + CHIP_W / 2; + let gx = lerp(GDOT_FROM.x, WALL_X + 8, gd); + let gy = lerp(GDOT_FROM.y, GATE.y, gd); + if (pass > 0) { + gx = lerp(WALL_X + 8, resRight + 6, pass); + gy = GATE.y; + } + const showDot = gd > 0.001 && pass < 0.999; + // the wall: a vertical bar that becomes a dashed straight line (the stand-in) + const wx1 = lerp(WALL_X, WALL_X - 18, st); + const wy1 = lerp(GATE.y - 22, GATE.y + 20, st); + const wx2 = lerp(WALL_X, WALL_X + 18, st); + const wy2 = lerp(GATE.y + 22, GATE.y - 20, st); + const wallCol = st > 0.5 ? colors.TEAL : colors.NEGATIVE; + return ( + + {/* plot frame */} + + + result as the input a slides + + + a hard decision: a rounding bit + + + + {[2, 3, 4, 5, 6, 7].map((v) => ( + {v} + ))} + a + {[10, 11, 12].map((v) => ( + {v} + ))} + {[0, 1].map((v) => ( + {v} + ))} + 0.5 ? colors.WARM : colors.POSITIVE} width={2.8} /> + + {/* marker on result(a) */} + {mk > 0.01 && ( + + + + + a = {fmtNum(a)} + + + )} + {/* forward arrival dot at the plot */} + {fwd > 0.8 && fwd < 1 && } + + + derived from the README formulas + + + {/* B4: the gradient dot, the wall, the stand-in */} + {wall > 0.01 && ( + 0.5 ? '5 4' : undefined} strokeLinecap="round" opacity={Math.min(1, wall)} /> + )} + {wall > 0.5 && st < 0.5 && ( + + throws + + )} + {st > 0.01 && ( + + + + straight-through: + + + pretend the step is a straight line + + + )} + {showDot && ( + + + + grad 1 + + + )} + 0 && st > 0.01 ? ["throw new Error('cannot backpropagate through a boolean atom')", 'accumulateTermGradients(gradients, operation.leftTerms, outputGradient)'] : ["throw new Error('cannot backpropagate through a boolean atom')"]} + stripe={st > 0.5 ? colors.TEAL : colors.NEGATIVE} + opacity={err} + fontSize={13} + dimFirst={st} + /> + + + + {note > 0.01 && ( + + {['a cascade gives its', 'operands gradient 0,', 'the derivative of a step'].map((l, i) => ( + + {l} + + ))} + + )} + + ); +} + +function DesktopMock({ u }: { u: number }) { + if (u <= 0.005) return null; + const win = (x: number, y: number, w: number, h: number, title: string, body: ReactNode, k: number) => ( + + + + {title} + {body} + + ); + return ( + + + {win(400, 210, 210, 170, 'Monitor', ( + + {[0.5, 0.8, 0.35, 0.65, 0.9, 0.45, 0.7, 0.55].map((h, i) => ( + + ))} + + ), 0)} + {win(548, 256, 196, 172, 'Notes', ( + + {[150, 120, 160, 90, 140, 110].map((w, i) => ( + + ))} + + ), 0.6)} + {win(694, 206, 190, 168, 'Photos', ( + + {[0, 1, 2, 3, 4, 5].map((i) => ( + + ))} + + ), 1.2)} + + + {[0, 1, 2, 3, 4].map((i) => ( + + ))} + + + ); +} + +function B5(s: SceneState, sc: S): ReactNode { + const arcs = s.get(sc.arcsU); + const mock = s.get(sc.mockU); + const cnt = s.get(sc.countU); + const txt = s.get(sc.b5TextU); + const lock = s.get(sc.lock5U); + const b6 = s.get(sc.b6Op); + if (arcs <= 0.005 && mock <= 0.005 && cnt <= 0.005 && lock <= 0.005) return null; + const counters = [ + { v: DESKTOP.cells, l: 'cells', x: 150, y: 540, k: 0 }, + { v: DESKTOP.ops, l: 'live operations', x: 150, y: 588, k: 1 }, + { v: DESKTOP.layers, l: 'layers', x: 960, y: 540, k: 2 }, + { v: DESKTOP.weights, l: 'weights', x: 960, y: 588, k: 3 }, + ]; + const lockOpen = clamp01(lock - 1); + return ( + + {/* 9,909 placeholder arcs, ≤ 0.25 */} + {arcs > 0.005 && + ARC_CHUNKS.map((d, i) => ( + + ))} + + {cnt > 0.005 && ( + + {counters.map((c) => { + const u = clamp01(cnt * 1.3 - c.k * 0.1); + return ( + + + {commas(Math.round(c.v * u))} + + + {c.l} + + + ); + })} + + )} + {txt > 0.005 && ( + + + + arcs: one per stored weight (placeholder) · desktop sketch, not a screenshot + + + )} + {lock > 0.005 && ( + 1 ? b6 : 1)}> + {lock <= 1.001 ? ( + + + + desktop demo: frozen, none trained + + + ) : ( + + + + unfrozen + + + )} + + )} + + ); +} + +function B6(s: SceneState, sc: S): ReactNode { + const op = s.get(sc.b6Op); + const net = s.get(sc.netU); + if (op <= 0.005 || net <= 0.005) return null; + const wd = s.get(sc.wdotU); + const rect = s.get(sc.rectU); + const pile = s.get(sc.pileU); + const fit = s.get(sc.fitU); + const tag = s.get(sc.b6TagU); + const heads = ['features', '6 units', '6 units', '4 outputs']; + const outs = ['left', 'top', 'width', 'height']; + return ( + + {/* the network: features → 6 → 6 → 4 */} + {NET.paths.map((d, l) => ( + + ))} + {NET.layers.map((col, l) => ( + + {heads[l]} + {col.map((n, i) => ( + + ))} + {l === 3 && + col.map((n, i) => ( + {outs[i]} + ))} + + ))} + + slot position, sine and cosine + + {/* 244 weights = the program's 244 source cells */} + {WEIGHT_DOTS.map((w, i) => { + const v = clamp01(wd * 244 / 20 - i / 20); + if (v <= 0) return null; + const drift = 0.5 + 0.5 * Math.sin(2 * Math.PI * (w.ph + fit * 1.5)); + return 0 ? drift * clamp01(fit * 4) : 0)} opacity={v} />; + })} + + 244 weights · the program's 244 source cells + + {/* the sketch panel: target rectangles and the photos */} + {rect > 0.005 && ( + + + sketch (schematic) · Story preset: 10 rectangles on a 5-wide, 4-tall stage + + + {TARGETS.map((r, i) => ( + + ))} + + )} + {pile > 0.005 && + PILE.map((p, i) => { + const T = TARGETS[i]; + const u = ease.move(clamp01((fit * 1.5 - p.delay) / 0.8)); + const w = lerp(88, T.w, u); + const h = lerp(66, T.h, u); + const cx = lerp(p.cx, T.x + T.w / 2, u); + const cy = lerp(p.cy, T.y + T.h / 2, u); + const rot = lerp(p.rot, 0, u); + const m = Math.min(w, h); + return ( + + + + + + ); + })} + {pile > 0.005 && fit < 0.05 && ( + + the seeded weights put the photos in a pile + + )} + {tag > 0.005 && ( + + + step {Math.round(GALLERY.shown * fit)} + + + Story: first fit in {GALLERY.fitLo} to {GALLERY.fitHi} steps + + + Adam · {GALLERY.stepsPerSecond} steps a second + + + schematic · shown slower than real time + + + )} + + ); +} + +function B78(s: SceneState, sc: S): ReactNode { + const op = s.get(sc.b78Op); + const mu = s.get(sc.mapU); + if (op <= 0.005 || mu <= 0.005) return null; + const one = s.get(sc.oneU); + const bar = s.get(sc.barU); + const lit = s.get(sc.mapLit); + const flash = s.get(sc.flashU); + const match = s.get(sc.matchU); + const tag = s.get(sc.mapTagU); + const card = s.get(sc.cardU); + const dim = s.get(sc.mapDim); + const ruler = s.get(sc.rulerU); + const bands = [s.get(sc.band1), s.get(sc.band2), s.get(sc.band3), s.get(sc.band4)]; + const ovl = s.get(sc.overlapU); + const mapOp = lerp(1, 0.15, dim); + const baseOp = 0.07 + 0.28 * lit + 0.65 * clamp01(flash); + const decades = [0.001, 0.01, 0.1, 1, 10, 100]; + const decLabel = ['1 ms', '10 ms', '100 ms', '1 s', '10 s', '100 s']; + return ( + + {/* the dot map: one dot per 10 documents */} + + 0.05 ? '#f1f5f9' : '#9fb0c8'} opacity={baseOp} /> + 0.01 ? COPPER : '#9fb0c8'} opacity={match > 0.01 ? lerp(baseOp, 1, match) : baseOp} /> + + {one > 0.005 && ( + + + + + )} + {bar > 0.005 && ( + + + one document on the graphics card + + + + 0.6 ms or more + + + the round trip alone: wasted on a single document + + + )} + {tag > 0.005 && ( + + 1 dot = 10 documents · copper = a match · positions illustrative + default 100,000 documents (a window 720 px or wider) + + )} + {card > 0.005 && ( + + an example line the page prints (README) + + + {['10,559 of 100,000 documents match.', 'This program: 13 ms on the GPU.', 'Native RegExp: 21 ms.'].map((l, i) => ( + + {l} + + ))} + + )} + {/* the log-time ruler */} + {ruler > 0.005 && ( + + + {decades.map((d, i) => ( + + + + {decLabel[i]} + + ))} + + + per keystroke, includes compiling the pattern and reading back + + + measured on an Apple M5 Max, Chrome 155 (src/json-search/README.md:53) + + + {BANDS.map((b, i) => { + const u = bands[i]; + if (u <= 0.005) return null; + const y = RULER.rows[i]; + const x0 = LX(b.lo); + const x1 = Math.max(LX(b.hi), x0 + 6); + const isPoint = b.lo === b.hi; + return ( + + {isPoint ? ( + + ) : ( + + )} + + {b.label} + + + ); + })} + {ovl > 0.005 && ( + + + + 11–15 ms + + + + neither is always faster + + + )} + + )} + + ); +} + +function PanelGlyph({ i }: { i: number }): ReactNode { + if (i === 0) + return ( + + + + 10 + + + 12 + + + + ); + if (i === 1) + return ( + + + + {Array.from({ length: 8 }, (_, k) => ( + + ))} + + + ); + if (i === 2) + return ( + + + {Array.from({ length: 8 }, (_, k) => ( + + ))} + + + ); + if (i === 3) + return ( + + + {Array.from({ length: 6 }, (_, k) => ( + + ))} + {Array.from({ length: 5 }, (_, k) => ( + + ))} + + + ); + return ( + + {Array.from({ length: 60 }, (_, k) => ( + + ))} + + + + ); +} + +function B9(s: SceneState, sc: S): ReactNode { + const pu = s.get(sc.panelsU); + if (pu <= 0.005) return null; + const out = s.get(sc.panelsOut); + if (out >= 0.999) return null; + const lits = [s.get(sc.lit1), s.get(sc.lit2), s.get(sc.lit3), s.get(sc.lit4), 0]; + return ( + + {PANEL_TITLES.map((title, i) => { + const u = ease.enter(clamp01(pu * 1.8 - i * 0.2)); + if (u <= 0) return null; + const lit = lits[i]; + const x = PANEL_X(i); + const y = PANEL_Y + (1 - u) * 30; + return ( + + 0.5 ? colors.WARM : colors.GRID} strokeWidth={lit > 0.5 ? 2 : 1.2} /> + {i + 1} + {title} + + + ); + })} + + ); +} + +function B10(s: SceneState, sc: S): ReactNode { + const u = s.get(sc.closeU); + if (u <= 0.005) return null; + const eq = s.get(sc.closeEqU); + const D = { x: 640, y: 196 }; + const T10 = { x: 500, y: 322 }; + const T12 = { x: 780, y: 322 }; + const R = { x: 640, y: 446 }; + const tile = (x: number, y: number, v: string, dashed: boolean, color: string) => ( + + + {v} + + ); + return ( + + {/* the bit weights both answers */} + + + × 1 + × 0 + + the bit, exactly 1 + {tile(T10.x, T10.y, '10', false, colors.POSITIVE)} + {tile(T12.x, T12.y, '12', true, colors.POSITIVE)} + + + + + + 10 + + + ); +} + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +export function Render({ s }: { s: SceneState }) { + const cam = s.get(scene.cam); + const srcIdx = Math.max(0, Math.min(SOURCES.length - 1, Math.round(s.get(scene.src)))); + const chrome = s.get(scene.chromeU); + const chip = `${PIN} · ${SOURCES[srcIdx]}`; + const chipW = chip.length * 7.8 + 22; + return ( + <> + + + {B5(s, scene)} + {StackLayer(s, scene)} + {B0(s, scene)} + {B1(s, scene)} + {B2(s, scene)} + {B34(s, scene)} + {B6(s, scene)} + {B78(s, scene)} + {B9(s, scene)} + {B10(s, scene)} + + {chrome > 0.005 && ( + + + 5 · Exact, Trainable, Wide + + + + {chip} + + + )} + + ); +} + +export const vizScene = () => scene; diff --git a/generator/tts-speed.test.mjs b/generator/tts-speed.test.mjs new file mode 100644 index 00000000..83e9b3d5 --- /dev/null +++ b/generator/tts-speed.test.mjs @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { validateSpeed, voiceSettingsFor } from './tts.mjs'; + +test('validateSpeed accepts 0.7–1.2 and rejects everything else', () => { + for (const ok of [0.7, 0.95, 1, 1.2]) assert.equal(validateSpeed(ok), ok); + for (const bad of [0.69, 1.21, 0, NaN, Infinity, '0.9', null]) assert.throws(() => validateSpeed(bad), /speed must be/); +}); + +test('no speed → no voice settings and no API read', async () => { + const client = { voices: { settings: { get: () => assert.fail('must not read settings') } } }; + assert.deepEqual(await voiceSettingsFor(client, 'v1', undefined), {}); +}); + +test('speed merges onto the stored voice settings', async () => { + const stored = { stability: 0.4, similarityBoost: 0.8, style: 0, useSpeakerBoost: true, speed: 1 }; + const client = { voices: { settings: { get: async (id) => (id === 'v1' ? stored : assert.fail('wrong voice')) } } }; + assert.deepEqual(await voiceSettingsFor(client, 'v1', 0.93), { voiceSettings: { ...stored, speed: 0.93 } }); +}); + +test('unreadable stored settings warns once and falls back to a speed-only override', async (t) => { + const client = { voices: { settings: { get: async () => { throw new Error('401 missing_permissions'); } } } }; + const warn = t.mock.method(console, 'error', () => {}); + assert.deepEqual(await voiceSettingsFor(client, 'v1', 0.9), { voiceSettings: { speed: 0.9 } }); + assert.equal(warn.mock.callCount(), 1); + assert.match(warn.mock.calls[0].arguments[0], /could not read stored voice settings for v1 \(401 missing_permissions\); sending speed only as a per-request override/); +}); + +test('an invalid speed still throws before any read', async () => { + const client = { voices: { settings: { get: () => assert.fail('must not read settings') } } }; + await assert.rejects(voiceSettingsFor(client, 'v1', 2), /speed must be/); +}); diff --git a/generator/tts.mjs b/generator/tts.mjs index 8f7e3a71..00308288 100644 --- a/generator/tts.mjs +++ b/generator/tts.mjs @@ -98,6 +98,38 @@ function cuesFromAlignment(segments, alignment) { return { cues, approx, audioEnd }; } +export const SPEED_MIN = 0.7; +export const SPEED_MAX = 1.2; + +/** Throws unless `speed` is a finite number within ElevenLabs' supported range. */ +export function validateSpeed(speed) { + if (typeof speed !== 'number' || !Number.isFinite(speed) || speed < SPEED_MIN || speed > SPEED_MAX) { + throw new Error(`speed must be a number between ${SPEED_MIN} and ${SPEED_MAX} (got ${speed})`); + } + return speed; +} + +/** + * The extra request fields for an opt-in speed. With no speed this is `{}`, so + * the request is exactly what it always was. With a speed we merge it onto the + * voice's STORED settings (a partial voiceSettings object may reset the rest) + * and, if those cannot be read (e.g. the API key lacks `voices_read`), warn once + * and send the speed alone as a per-request override. + */ +export async function voiceSettingsFor(client, voiceId, speed) { + if (speed === undefined) return {}; + validateSpeed(speed); + let stored; + try { + stored = await client.voices.settings.get(voiceId); + } catch (e) { + const reason = String(e?.message ?? e).split('\n')[0].slice(0, 120); + console.error(`⚠ could not read stored voice settings for ${voiceId} (${reason}); sending speed only as a per-request override`); + return { voiceSettings: { speed } }; + } + return { voiceSettings: { ...stored, speed } }; +} + export async function synthesizeChapter({ spokenSegments, voiceId, @@ -107,8 +139,11 @@ export async function synthesizeChapter({ // storyboard behavior; pass '\n\n' (paragraph break) for natural ~0.6s // pauses between narration lines (used by the v3 video pipeline). sep = SEP, + // Opt-in narration speed (0.7–1.2; 1.0 = the voice's normal pace). Omitted → request unchanged. + speed, }) { if (!apiKey) throw new Error('ELEVENLABS_API_KEY is required for TTS'); + if (speed !== undefined) validateSpeed(speed); const client = new ElevenLabsClient({ apiKey }); // Sanitize the SPOKEN string only (captions on screen keep their typography), @@ -118,7 +153,11 @@ export async function synthesizeChapter({ const spoken = spokenSegments.map(sanitizeSpoken); const text = spoken.join(sep); - const res = await client.textToSpeech.convertWithTimestamps(voiceId, { text, modelId }); + const res = await client.textToSpeech.convertWithTimestamps(voiceId, { + text, + modelId, + ...(await voiceSettingsFor(client, voiceId, speed)), + }); const mp3 = Buffer.from(res.audioBase64, 'base64'); const al = res.alignment; // maps to the (possibly normalized) input characters const alignedExact = al.characters.length === text.length; diff --git a/generator/video.mjs b/generator/video.mjs index b36661ab..c912c79e 100644 --- a/generator/video.mjs +++ b/generator/video.mjs @@ -16,14 +16,14 @@ // [--subtitle "…"] [--accent "#hex"] [--animal ""] // [--chapter-titles "t1|t2|…"] [--blurbs "b1|b2|…"] // [--series "" [--series-order ]] -// [--voice ] [--no-tts] [--no-cover] +// [--voice ] [--speed <0.7-1.2>] [--no-tts] [--no-cover] // // Env (.env, gitignored): ELEVENLABS_API_KEY (narration), OPENAI_API_KEY (cover). import { mkdirSync, writeFileSync, readdirSync, existsSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { synthesizeChapter, sanitizeSpoken } from './tts.mjs'; +import { synthesizeChapter, sanitizeSpoken, validateSpeed } from './tts.mjs'; import { extractScene } from './scene-captions.mjs'; import { generateAnimal, thumbnailAnimal } from './cover.mjs'; import { upsertBook, colorForSlug } from './library.mjs'; @@ -56,6 +56,7 @@ function parseArgs(argv) { else if (k === '--series-order') a.seriesOrder = Number(val()); else if (k === '--paper-date') a.paperDate = val(); else if (k === '--voice') a.voice = val(); + else if (k === '--speed') a.speed = Number(val()); else if (k === '--no-tts') a.tts = false; else if (k === '--silent-audio') a.silentAudio = true; else if (k === '--no-cover') a.cover = false; @@ -115,6 +116,7 @@ async function main() { --chapter-titles "a|b|…" per-chapter titles (default: scene leading comment / first caption) --blurbs "a|b|…" per-chapter blurbs (default: first caption) --voice ElevenLabs voice id (default ${VOICE_DEFAULT}) + --speed narration speed, 0.7–1.2 (ElevenLabs voice_settings.speed; default: the voice's own, unrecorded) --no-tts skip narration (silent preview; cues fall back to authored times) --silent-audio add a silent MP3 so the authored timeline remains seekable --no-cover skip the animal cover @@ -122,6 +124,7 @@ Env: ELEVENLABS_API_KEY (narration), OPENAI_API_KEY (cover).`); process.exit(args.help ? 0 : 1); } const { slug } = args; + if (args.speed !== undefined) validateSpeed(args.speed); // 1) discover + extract const files = discoverChapters(slug); @@ -161,6 +164,7 @@ Env: ELEVENLABS_API_KEY (narration), OPENAI_API_KEY (cover).`); spokenSegments: ex.captions.map((c) => c.text), voiceId: args.voice || VOICE_DEFAULT, sep: '\n\n', + ...(args.speed !== undefined ? { speed: args.speed } : {}), }); writeFileSync(join(outDir, 'audio', `chapter-${n}.mp3`), r.mp3); // TTS sometimes appends junk after the aligned text ("bumbpawee") — @@ -255,6 +259,7 @@ Env: ELEVENLABS_API_KEY (narration), OPENAI_API_KEY (cover).`); ...(args.subtitle ? { subtitle: args.subtitle } : {}), animal: 'animal.webp', accent, + ...(args.speed !== undefined ? { voiceSpeed: args.speed } : {}), chapters, }; // keep what other stages bind into the manifest: music (generator/book-score.mjs) and the sources page diff --git a/public/generated/handwired/audio/chapter-1.mp3 b/public/generated/handwired/audio/chapter-1.mp3 new file mode 100644 index 00000000..c5bfdc78 Binary files /dev/null and b/public/generated/handwired/audio/chapter-1.mp3 differ diff --git a/public/generated/handwired/audio/chapter-2.mp3 b/public/generated/handwired/audio/chapter-2.mp3 new file mode 100644 index 00000000..72d4cf0f Binary files /dev/null and b/public/generated/handwired/audio/chapter-2.mp3 differ diff --git a/public/generated/handwired/audio/chapter-3.mp3 b/public/generated/handwired/audio/chapter-3.mp3 new file mode 100644 index 00000000..7a79c17d Binary files /dev/null and b/public/generated/handwired/audio/chapter-3.mp3 differ diff --git a/public/generated/handwired/audio/chapter-4.mp3 b/public/generated/handwired/audio/chapter-4.mp3 new file mode 100644 index 00000000..c2246622 Binary files /dev/null and b/public/generated/handwired/audio/chapter-4.mp3 differ diff --git a/public/generated/handwired/audio/chapter-5.mp3 b/public/generated/handwired/audio/chapter-5.mp3 new file mode 100644 index 00000000..890ac19c Binary files /dev/null and b/public/generated/handwired/audio/chapter-5.mp3 differ diff --git a/public/generated/handwired/blog.md b/public/generated/handwired/blog.md new file mode 100644 index 00000000..e7927322 --- /dev/null +++ b/public/generated/handwired/blog.md @@ -0,0 +1,99 @@ +# Handwired + +Handwired is Cheng Lou's repository for writing programs by hand as neural-net-style arithmetic: weighted sums, products with a ReLU, and a fixed table of cells that plain JavaScript reads and writes. Its README rewrites one branch, `const result = (a > 4) ? 10 : 12`, so that both answers are computed and a 0 or 1 picks one. This book follows the repository at commit 4ea7fcb ([the pinned tree](https://github.com/chenglou/handwired/tree/4ea7fcb27ebe93acf49b9f29a93e162b975bad46)). + +### Two ramps make a bit + +The README's ternary becomes a difference of two ReLU ramps, `const above4 = Math.max(0, a - 4) - Math.max(0, a - 5)`. For whole numbers this is 0 up to four and exactly 1 from five on. Both answers are then computed and weighted by that bit: `const result = above4 * 10 + (1 - above4) * 12`. The bit only decides which product survives. + +{% viz scene="books/handwired/chapter-1" section="chapter-1-two-ramps-make-a-bit" cue="0" from="0.000" to="62.333" title="Both branches compute, and a bit picks one." %} +{% endviz %} + +The README assumes whole numbers. At a = 4.5 the same formulas give 0.5 and a result of 11, a blend of the two answers. + +### Builder code appends the operations + +No tool converts existing JavaScript. Builder code is plain JavaScript that executes a single time and adds operations to a circuit, with one familiar expression per line, such as `const not = (bit: Wire) => circuit.affine([[bit, -1]], 1)`. + +There are four kinds of operation: a weighted sum, a product with a ReLU, a score rounded to 0 or 1, and a table read. Independent operations share a layer, and each layer is one sparse matrix multiply followed by the gates. The result is a stack of layers with no runtime branch or loop. So if every branch always runs, what happens to a loop? + +{% viz scene="books/handwired/chapter-1" section="chapter-1-builder-code-and-layers" cue="8" from="62.333" to="108.623" title="Builder code appends operations, and a program is a stack of layers." %} +{% endviz %} + +### A toggle is a table of cells + +The toggle's table has 20 cells: the viewport, the pointer inputs, the on and armed bits, the spring matrix, the settle and snap bits, and the thumb. JavaScript writes the inputs, and the program computes the states. + +{% viz scene="books/handwired/chapter-2" section="chapter-2-toggle-cells-and-hit-tests" cue="0" from="0.000" to="56.226" title="A press lands on the track only when all four edge bits are one." %} +{% endviz %} + +A press lands on the track when four edge comparisons all hold. With pointer coordinates on a 1/64 px grid and track edges on a 1/2 px grid, each comparison takes two operations. The four bits combine as `relu(sum of bits - (N - 1))`, so only an all-ones result survives. Both hit tests run in every evaluation, whether or not an event arrived. + +### Memory, a flip and a spring + +Where JavaScript reassigns a variable, builder code makes a new wire. `armedAfterDown` holds the value after a press, while the armed cell keeps the previous evaluation's value. An `if` that assigns a cell becomes `emitSelect`, which computes both sides and picks one. The flip of the on bit is a select, and a release or a cancel disarms the switch. + +{% viz scene="books/handwired/chapter-2" section="chapter-2-memory-flip-and-spring" cue="7" from="56.226" to="106.439" title="A flip is a select, and the spring is one matrix power." %} +{% endviz %} + +A spring step is a 2 × 2 matrix on displacement and velocity, so n steps are that matrix to the power n. The host computes that power and writes its four numbers into four cells. The program applies it in 14 operations across three layers, for any step count, so a frame that needs many steps costs the program no more than a frame that needs one. + +### The host queues, plans and writes + +A program cannot wait, listen or loop, so the host does those things. Event handlers only queue events with their timestamps. Each frame, `advanceFrame` plans one evaluation per distinct event time, one at the frame, and a settle check every 64 ms of simulated time. + +{% viz scene="books/handwired/chapter-3" section="chapter-3-host-plans-the-frame" cue="0" from="0.000" to="56.075" title="The host queues events, plans evaluations and writes the table." %} +{% endviz %} + +Before each evaluation the host writes the spring matrix, the bits, the viewport and any event into the table. Then `evaluate` runs every operation. The host copies the next values back only when the evaluation writes back. + +### Stored state follows events + +A frame's own evaluation runs on a copy of the table. With the template's policy, only event, settle-check and snap evaluations write state back, so stored state depends on events and not on frame rate. The repository's replay tests run timestamped sessions at 10, 30, 60 and 120 frames a second and find every stored table identical in every bit, provided events arrive in timestamp order and no object is snapped onto its destination. Under the alternative, where every evaluation writes back, FP32 rounds the stored state once per evaluation, by amounts that differ between frame rates. + +{% viz scene="books/handwired/chapter-3" section="chapter-3-stored-state-and-the-check" cue="6" from="56.075" to="113.870" title="Stored state follows events, and the plain JavaScript version agrees on every decision." %} +{% endviz %} + +The page draws only from output numbers. To check the host, the repository runs the same switch in plain float64 JavaScript over a scripted trace and compares every frame. The on and armed bits and the track position must match exactly. The thumb must stay within 1e-5 px, and the measured error is 1.3e-6 px over the 20 px travel. The trace presses exactly on the edges, where an inexact decision shows up first. + +### First hit by counting + +A JavaScript loop can stop early. The program cannot, since every operation runs on every evaluation. So a loop becomes a count, a tree, a table, or the same work on every item. To find the pointed row, test every row at once. With bottoms 40, 80, 80, 140 and 180 and a pointer at 90, the bits `rowBottom <= pointerY` read 1, 1, 1, 0, 0. + +{% viz scene="books/handwired/chapter-4" section="chapter-4-first-hit-by-counting" cue="0" from="0.000" to="50.422" title="Testing every row at once turns the first hit into a count." %} +{% endviz %} + +The count of ones, 3, is the loop's index, because the bits never go back from 0 to 1. At 64 rows this costs 195 operations in 3 layers, against 447 in 34 for a tree built for the first hit. The count is right only for monotone bits. With bottoms 40, 30 and 80 at pointer 35, the count is 1 while the loop returns 0, and no error is raised. + +### Running totals and ranks + +Running totals form a chain: at 1,024 rows, 1,023 operations in 1,023 layers. A Brent-Kung network gives every total in 18 layers for 2,036 operations. That matches the loop only when the combine is associative and every partial sum is exact. Integers below 2^24 pass that test in any grouping, but fractions do not. With row heights 229, 167.4, 208.5 and 194.4 px, the loop's content height is 799.300048828125, and adding in pairs gives 799.2999877929688. + +{% viz scene="books/handwired/chapter-4" section="chapter-4-running-totals-and-ranks" cue="6" from="50.422" to="119.071" title="Running totals take a tree only when grouping cannot change the bits." %} +{% endviz %} + +Sorting needs no sort: an item's rank is the count of items before it. One comparison per pair, on keys on a grid, feeds one sum per item, which is 4,096 operations in 3 layers at 64 items. When data changes only between frames, the host can run the loop for every input and write the answers into cells, which the program reads in one operation. Fewer layers is not always faster: on the CPU executor the tree loses, while on a graphics card one level of the tree is a single dispatch. + +### Exact comparisons and gradients + +Chapter one's ramps were right only for whole numbers. For every pair of FP32 values in the declared ranges, a comparison bit equals JavaScript's comparison. On a declared grid, `a >= b` is `relu(1 - relu(-D))`, with D = (a - b) / grid, which costs 2 operations in 2 layers. Without a grid, a sign cascade costs 15 operations in 14 layers and returns the same bits. Exact means equal to the stated FP32 calculation, not to float64 arithmetic, so positions carry a tolerance. + +{% viz scene="books/handwired/chapter-5" section="chapter-5-exact-comparisons-and-gradients" cue="0" from="0.000" to="53.615" title="A comparison bit is exact, and backpropagation needs a stated exception for hard decisions." %} +{% endviz %} + +Most operations are weighted sums or ReLU products, so backpropagation can run through the program. Boolean atoms and index reads are the exceptions. A boolean atom throws unless the caller passes the straight-through option, which passes the output gradient unchanged to the atom's score. An index read throws by default when a nonzero gradient reaches it. A sign cascade gives its operands gradient 0, the derivative of a step. + +### Three demos and the recap + +The desktop demo is frozen: three windows and a dock, run by one compiled program of 234 source cells, 4,646 live operations in 130 layers and 9,909 stored weights. Builder code writes every weight, and none is trained. The learned gallery unfreezes its 244 cells, the coefficients of a small network, and Adam trains those coefficients at 600 steps a second. + +{% viz scene="books/handwired/chapter-5" section="chapter-5-three-demos-and-recap" cue="5" from="53.615" to="122.276" title="Three demos make three different claims, and the program never decides anything." %} +{% endviz %} + +The JSON search is the demo on the graphics card. Its GPU code lives in `src/json-search`, since `src/runtime` has no GPU backend. It searches 100,000 generated documents per keystroke, the default for a window 720 px wide or wider, with two fixed numeric programs. On an Apple M5 Max in Chrome 155, a search takes 11 to 15 ms per keystroke. Ordinary RegExp takes 8 to 28 ms, and `([a-z]+)+x` takes 2.1 s because it backtracks, so neither is always faster. + +To recap: a decision runs both branches and a bit picks one. The program never decides anything. It computes every answer, and a bit that is exactly zero or one lets one of them through. + +The main sources are the repository at commit 4ea7fcb27ebe93acf49b9f29a93e162b975bad46, especially its [tutorial](https://github.com/chenglou/handwired/blob/4ea7fcb27ebe93acf49b9f29a93e162b975bad46/docs/tutorial.md), [execution model](https://github.com/chenglou/handwired/blob/4ea7fcb27ebe93acf49b9f29a93e162b975bad46/docs/execution-model.md) and [patterns](https://github.com/chenglou/handwired/blob/4ea7fcb27ebe93acf49b9f29a93e162b975bad46/docs/patterns.md) docs, and the [live demos](https://chenglou.me/handwired/). The sources page under the player lists each spoken claim with its file and lines. + +[Watch all five narrated chapters](?bundle=handwired). diff --git a/public/generated/handwired/manifest.json b/public/generated/handwired/manifest.json new file mode 100644 index 00000000..be480307 --- /dev/null +++ b/public/generated/handwired/manifest.json @@ -0,0 +1,128 @@ +{ + "format": 3, + "slug": "handwired", + "title": "Handwired", + "subtitle": "If-else, loops and state as matrix multiply and relu", + "accent": "#41607f", + "voiceSpeed": 0.94, + "chapters": [ + { + "number": 1, + "title": "Both Branches Run", + "blurb": "An if-else becomes two ramps and a bit: both branches always compute, and one number picks the survivor.", + "scene": "books/handwired/chapter-1", + "audio": "audio/chapter-1.mp3", + "cues": [ + 0, + 8.905, + 19.586, + 28.851, + 31.323, + 36.246, + 43.909, + 53.649, + 62.333, + 72.19, + 80.399, + 92.531, + 100.054, + 102.156, + 105.441 + ], + "duration": 108.623 + }, + { + "number": 2, + "title": "A Switch of Arithmetic", + "blurb": "A toggle switch with memory, written as comparisons, selects and a matrix power: no if, no loop.", + "scene": "books/handwired/chapter-2", + "audio": "audio/chapter-2.mp3", + "cues": [ + 0, + 1.753, + 8.104, + 18.762, + 27.701, + 37.14, + 46.649, + 56.226, + 66.223, + 69.056, + 74.269, + 80.329, + 83.731, + 90.012, + 98.045 + ], + "duration": 106.439 + }, + { + "number": 3, + "title": "The Host Loop", + "blurb": "Waiting, listening and looping live outside the program: the host writes cells, evaluates, and reads results.", + "scene": "books/handwired/chapter-3", + "audio": "audio/chapter-3.mp3", + "cues": [ + 0, + 8.522, + 14.791, + 26.621, + 36.083, + 46.149, + 56.075, + 69.242, + 76.73, + 84.52, + 95.294, + 105.198 + ], + "duration": 113.87 + }, + { + "number": 4, + "title": "Loops That Cannot Stop", + "blurb": "A fixed program cannot stop a loop early, so loops become counts, trees, tables, or the same work everywhere.", + "scene": "books/handwired/chapter-4", + "audio": "audio/chapter-4.mp3", + "cues": [ + 0, + 9.392, + 16.312, + 27.516, + 34.47, + 43.886, + 50.422, + 60.43, + 70.739, + 77.925, + 92.101, + 101.784, + 114.253 + ], + "duration": 119.071 + }, + { + "number": 5, + "title": "Exact, Trainable, Wide", + "blurb": "Exact bits, gradients through the layers, frozen and learned weights, one program over 100,000 documents.", + "scene": "books/handwired/chapter-5", + "audio": "audio/chapter-5.mp3", + "cues": [ + 0, + 11.413, + 18.948, + 29.292, + 42.017, + 53.615, + 67.512, + 78.588, + 93.495, + 104.92, + 109.958, + 114.822 + ], + "duration": 122.276 + } + ], + "sources": "sources.json" +} \ No newline at end of file diff --git a/public/generated/handwired/previews/chapter-1.png b/public/generated/handwired/previews/chapter-1.png new file mode 100644 index 00000000..84f10405 Binary files /dev/null and b/public/generated/handwired/previews/chapter-1.png differ diff --git a/public/generated/handwired/previews/chapter-2.png b/public/generated/handwired/previews/chapter-2.png new file mode 100644 index 00000000..7a54027f Binary files /dev/null and b/public/generated/handwired/previews/chapter-2.png differ diff --git a/public/generated/handwired/previews/chapter-3.png b/public/generated/handwired/previews/chapter-3.png new file mode 100644 index 00000000..62daac96 Binary files /dev/null and b/public/generated/handwired/previews/chapter-3.png differ diff --git a/public/generated/handwired/previews/chapter-4.png b/public/generated/handwired/previews/chapter-4.png new file mode 100644 index 00000000..0b21822e Binary files /dev/null and b/public/generated/handwired/previews/chapter-4.png differ diff --git a/public/generated/handwired/previews/chapter-5.png b/public/generated/handwired/previews/chapter-5.png new file mode 100644 index 00000000..8019e6df Binary files /dev/null and b/public/generated/handwired/previews/chapter-5.png differ diff --git a/public/generated/handwired/sources.json b/public/generated/handwired/sources.json new file mode 100644 index 00000000..a017c506 --- /dev/null +++ b/public/generated/handwired/sources.json @@ -0,0 +1,1871 @@ +{ + "version": 1, + "slug": "handwired", + "chapters": [ + { + "number": 1, + "title": "Both Branches Run", + "claims": [ + { + "id": "c1-relu-and-sums", + "claim": "A Handwired program is built from weighted sums and products with a relu over floats, the neural-net form the README describes; a plain relu is a gated relu whose left side is the constant 1.", + "status": "paraphrase", + "sources": [ + { + "label": "README.md lines 7", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "7", + "quote": "matrix multiplication + reglu over floats" + }, + { + "label": "docs/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/README.md", + "lines": "3", + "quote": "mostly weighted sums and products with a ReLU" + }, + { + "label": "README.md lines 18", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "18", + "quote": "Math.max(0, x) is ReLU" + }, + { + "label": "docs/tutorial.md lines 74", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "74", + "quote": "`circuit.reglu([], 1, terms, bias)` computes `max(0, terms + bias)`, a ReLU." + } + ], + "caption": 0 + }, + { + "id": "c1-the-decision", + "claim": "The README example is a ternary, a greater than 4 gives 10 else 12, rewritten as basic arithmetic.", + "status": "verified", + "sources": [ + { + "label": "README.md lines 11", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "11", + "quote": "const result = (a > 4) ? 10 : 12" + }, + { + "label": "README.md lines 14", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "14", + "quote": "The above can be expressed as basic arithmetic" + } + ], + "caption": 1 + }, + { + "id": "c1-two-ramps", + "claim": "above4 is built from Math.max(0, a - 4) and Math.max(0, a - 5), both ReLUs.", + "status": "verified", + "sources": [ + { + "label": "README.md lines 19", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "19", + "quote": "const above4 = Math.max(0, a - 4) - Math.max(0, a - 5)" + } + ], + "caption": 2 + }, + { + "id": "c1-difference-is-a-step", + "claim": "For an integer a, above4 is 1 if a > 4 and 0 otherwise.", + "status": "verified", + "sources": [ + { + "label": "README.md lines 18", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "18", + "quote": "For an integer a, above4 is 1 if a > 4, and 0 otherwise" + } + ], + "caption": 4 + }, + { + "id": "c1-both-branches-compute", + "claim": "Both branches compute, and above4 picks one: result = above4 10 + (1 - above4) 12.", + "status": "verified", + "sources": [ + { + "label": "README.md lines 20", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "20", + "quote": "Both branches compute, and above4 picks one" + }, + { + "label": "README.md lines 21", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "21", + "quote": "const result = above4 * 10 + (1 - above4) * 12" + } + ], + "caption": 5 + }, + { + "id": "c1-slide-a", + "claim": "At a = 5 above4 is 1, so the result is 10 and the else branch is weighted by 1 - above4 = 0.", + "status": "verified", + "sources": [ + { + "label": "README.md lines 10", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "10", + "quote": "const a = 5" + }, + { + "label": "README.md lines 21", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "21", + "quote": "(1 - above4) * 12" + } + ], + "caption": 6 + }, + { + "id": "c1-fraction-blend", + "claim": "At a = 4.5 the README formulas give above4 = 0.5 and result = 11 (arithmetic on the README lines; the README states the integer assumption).", + "status": "inferred", + "sources": [ + { + "label": "README.md lines 18", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "18", + "quote": "For an integer a" + }, + { + "label": "README.md lines 21", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "21", + "quote": "above4 * 10 + (1 - above4) * 12" + } + ], + "caption": 7 + }, + { + "id": "c1-no-converter", + "claim": "No tool converts existing JavaScript source; builder code is ordinary JavaScript that runs once and appends operations to a Circuit.", + "status": "verified", + "sources": [ + { + "label": "docs/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/README.md", + "lines": "3", + "quote": "No tool converts existing JavaScript source into a program." + }, + { + "label": "docs/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/README.md", + "lines": "3", + "quote": "ordinary JavaScript that runs once and appends the operations to a Circuit" + } + ], + "caption": 8 + }, + { + "id": "c1-builder-line", + "claim": "Each line of builder code appends the operations of one familiar expression; the comment shows the JavaScript it replaces (not = affine with weight -1 and bias 1).", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 74", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "74", + "quote": "Each line of builder code appends the operations of one familiar expression" + }, + { + "label": "template/toggle.ts lines 50", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "50", + "quote": "// JS: !bit" + }, + { + "label": "template/toggle.ts lines 51", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "51", + "quote": "const not = (bit: Wire) => circuit.affine([[bit, -1]], 1)" + } + ], + "caption": 9 + }, + { + "id": "c1-four-kinds", + "claim": "Four kinds of operation: affine (weighted sum), reglu (product with a ReLU), boolean (score rounded to 0 or 1), index read (table read). Every arithmetic step rounds to 32-bit floats (FP32), like Math.fround.", + "status": "verified", + "sources": [ + { + "label": "docs/README.md lines 15", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/README.md", + "lines": "15", + "quote": "a weighted sum (circuit.affine), a product with a ReLU (circuit.reglu), a score rounded to 0 or 1 (circuit.boolean), or a table read (circuit.indexRead)" + }, + { + "label": "docs/tutorial.md lines 74", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "74", + "quote": "Every arithmetic step rounds to FP32, like `Math.fround`." + } + ], + "caption": 10 + }, + { + "id": "c1-layers", + "claim": "Independent operations at the same dependency depth share a layer; a layer is a sparse affine projection followed by the gates.", + "status": "paraphrase", + "sources": [ + { + "label": "docs/execution-model.md lines 40", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "40", + "quote": "Independent instructions at the same dependency depth share a layer." + }, + { + "label": "docs/execution-model.md lines 40", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "40", + "quote": "evaluates its sparse affine projection" + } + ], + "caption": 11 + }, + { + "id": "c1-no-runtime-loops", + "claim": "Nothing in the program branches or loops at runtime; the host JavaScript around it does.", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 13", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "13", + "quote": "so nothing in the program branches or loops at runtime" + } + ], + "caption": 13 + }, + { + "id": "c1-os-scale", + "claim": "On-screen only (zoom-out label): the desktop demo program is 234 source cells, 4,646 live operations in 130 layers, 9,909 stored weights.", + "status": "verified", + "sources": [ + { + "label": "src/handwiredos/README.md lines 13", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/handwiredos/README.md", + "lines": "13", + "quote": "The program has 234 source cells, 4,646 live operations in 130 layers and 9,909 stored weights" + } + ] + }, + { + "id": "c1-subtract-ramps", + "caption": 3, + "status": "verified", + "claim": "above4 is the first ramp, max(0, a - 4), minus the second ramp, max(0, a - 5).", + "sources": [ + { + "label": "README.md lines 19", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "19", + "quote": "const above4 = Math.max(0, a - 4) - Math.max(0, a - 5)" + } + ] + }, + { + "id": "c1-stack-of-layers", + "caption": 12, + "status": "paraphrase", + "claim": "A compiled program is packed into a sequence of layers of sparse matrices, one layer after another.", + "sources": [ + { + "label": "docs/execution-model.md lines 298", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "298", + "quote": "a program packed into layers of sparse matrices" + } + ] + } + ] + }, + { + "number": 2, + "title": "A Switch of Arithmetic", + "claims": [ + { + "id": "c2-what-a-toggle-does", + "claim": "A press on the track arms the switch, a release on the track while armed flips it, and the thumb springs to its new side.", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "3", + "quote": "a press on the track arms it, and a release on the track while it's armed flips it. The thumb springs to its new side." + } + ], + "caption": 1 + }, + { + "id": "c2-twenty-cells", + "claim": "The toggle's source table has 20 cells (viewport 2, down 3, up 3, cancel, on, armed, springMatrix 4, settleCheck, snap, thumb 3).", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 145", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "145", + "quote": "the toggle's 20 cells" + }, + { + "label": "template/toggle.ts lines 19", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "19", + "quote": "viewportWidth: input" + }, + { + "label": "template/toggle.ts lines 34", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "34", + "quote": "displacement: state" + } + ], + "caption": 2 + }, + { + "id": "c2-cell-kinds", + "claim": "input: JavaScript writes it; event: JavaScript sets it to 1 and the program resets it to 0; state: the program computes its next value.", + "status": "verified", + "sources": [ + { + "label": "src/runtime/cells.ts lines 29", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/runtime/cells.ts", + "lines": "29", + "quote": "- input: JavaScript writes it before an evaluation" + }, + { + "label": "src/runtime/cells.ts lines 30", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/runtime/cells.ts", + "lines": "30", + "quote": "- event: an exact bit that JavaScript sets to 1 when an event arrives" + }, + { + "label": "src/runtime/cells.ts lines 31", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/runtime/cells.ts", + "lines": "31", + "quote": "The program resets it to 0." + }, + { + "label": "src/runtime/cells.ts lines 32", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/runtime/cells.ts", + "lines": "32", + "quote": "- state: the program computes its next value" + } + ], + "caption": 3 + }, + { + "id": "c2-four-comparisons", + "claim": "insideTrack is four emitCompare calls (left, right, top, bottom edge); with pointer coordinates on a 1/64 px grid and track edges on a 1/2 px grid each comparison takes 2 operations.", + "status": "paraphrase", + "sources": [ + { + "label": "template/toggle.ts lines 54", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "54", + "quote": "emitCompare(circuit, point.x, '>=', trackLeft)," + }, + { + "label": "template/toggle.ts lines 57", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "57", + "quote": "emitCompare(circuit, point.y, '<', circuit.affine([[trackTop, 1]], layout.trackHeight))," + }, + { + "label": "docs/tutorial.md lines 53", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "53", + "quote": "the comparison takes 2 operations" + }, + { + "label": "docs/execution-model.md lines 139", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "139", + "quote": "becomes its own operation, since the bit is exact for the operand" + }, + { + "label": "docs/tutorial.md lines 53", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "53", + "quote": "When both operands have grids, like a pointer coordinate on 1/64 px and a track edge on 1/2 px, the comparison takes 2 operations." + } + ], + "caption": 4 + }, + { + "id": "c2-and-as-relu", + "claim": "AND of N exact bits is relu(sum of bits - (N - 1)); the toggle combines the four hit-test bits that way.", + "status": "verified", + "sources": [ + { + "label": "template/toggle.ts lines 48", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "48", + "quote": "JS: bits.every(bit => bit === 1), as relu(sum of bits - (count - 1))." + }, + { + "label": "template/toggle.ts lines 49", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "49", + "quote": "circuit.reglu([], 1, bits.map(bit => [bit, 1] as const), 1 - bits.length)" + } + ], + "caption": 5 + }, + { + "id": "c2-select", + "claim": "emitSelect(circuit, condition, off, on) is condition x on + (1 - condition) x off; both sides always compute; an if that assigns a cell becomes a select on its next value.", + "status": "verified", + "sources": [ + { + "label": "docs/expressions.md lines 11", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/expressions.md", + "lines": "11", + "quote": "Both sides always compute" + }, + { + "label": "docs/tutorial.md lines 93", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "93", + "quote": "computes both sides and picks one, so an if that assigns a cell becomes a select on the cell" + } + ], + "caption": 6 + }, + { + "id": "c2-new-wire", + "claim": "Where JavaScript would reassign a variable, builder code makes a new wire: armedAfterDown holds the value after the press while cells.armed still holds the previous evaluation's value.", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 93", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "93", + "quote": "Where JavaScript would reassign a variable, builder code makes a new wire" + }, + { + "label": "template/toggle.ts lines 61", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "61", + "quote": "const armedAfterDown = emitSelect(circuit, cells.down.active, cells.armed, insideTrack(cells.down))" + } + ], + "caption": 7 + }, + { + "id": "c2-the-flip", + "claim": "on = select(all(up.active, armedAfterDown, insideTrack(up)), cells.on, not(cells.on)).", + "status": "verified", + "sources": [ + { + "label": "template/toggle.ts lines 62", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "62", + "quote": "JS: if (up && armed && insideTrack(up)) on = !on" + }, + { + "label": "template/toggle.ts lines 63", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "63", + "quote": "const on = emitSelect(circuit, all(cells.up.active, armedAfterDown, insideTrack(cells.up)), cells.on, not(cells.on))" + } + ], + "caption": 9 + }, + { + "id": "c2-disarm", + "claim": "armed = all(armedAfterDown, not(up.active), not(cancel)).", + "status": "verified", + "sources": [ + { + "label": "template/toggle.ts lines 64", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "64", + "quote": "JS: if (up || cancel) armed = false" + }, + { + "label": "template/toggle.ts lines 65", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "65", + "quote": "const armed = all(armedAfterDown, not(cells.up.active), not(cells.cancel))" + } + ], + "caption": 10 + }, + { + "id": "c2-matrix-power", + "claim": "One 4 ms spring step is a 2x2 matrix on (displacement, velocity); n steps are that matrix to the power n.", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 106", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "106", + "quote": "One step is a 2 × 2 matrix applied to the spring's displacement and velocity, and n steps are that matrix to the power n." + }, + { + "label": "docs/patterns.md lines 200", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "200", + "quote": "any number of steps is one matrix power" + } + ], + "caption": 12 + }, + { + "id": "c2-host-writes-matrix", + "claim": "JavaScript writes springMatrixPower(springStepMatrix(...), steps) into the pair's 4 cells; the program applies it with 4 products: 14 operations in 3 layers.", + "status": "verified", + "sources": [ + { + "label": "docs/execution-model.md lines 173", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "173", + "quote": "JavaScript writes springMatrixPower(springStepMatrix({stiffness, damping}), steps) into the pair's 4 cells." + }, + { + "label": "docs/execution-model.md lines 178", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "178", + "quote": "14 operations in 3 layers for any step count" + } + ], + "caption": 13 + }, + { + "id": "c2-fixed-list", + "claim": "Inputs are handled in a fixed order (down, up, cancel); each insideTrack call appends a whole hit test and the program computes both in every evaluation.", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 93", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "93", + "quote": "The inputs are handled in a fixed order, down, then up, then cancel." + }, + { + "label": "docs/tutorial.md lines 93", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "93", + "quote": "the program computes both hit tests in every evaluation" + } + ], + "caption": 14 + }, + { + "id": "c2-os-spring-numbers", + "claim": "On-screen only: stiffness 650, damping 45, 4 ms step; matrix bound 10.36; released 20 px away the thumb peaks near 207 px/s (module-scope sanity check for the matrix).", + "status": "verified", + "sources": [ + { + "label": "template/toggle.ts lines 10", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "10", + "quote": "export const springConstants = {stiffness: 650, damping: 45}" + }, + { + "label": "docs/execution-model.md lines 243", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "243", + "quote": "e.g. 10.36 for stiffness 650 and damping 45" + }, + { + "label": "docs/tutorial.md lines 55", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "55", + "quote": "peaks at 207 px/s" + } + ] + }, + { + "id": "c2-os-step-matrix", + "claim": "On-screen only: the step matrix entries are [1 - k dt^2, dt (1 - c dt), -k dt, 1 - c dt] = [0.9896, 0.00328, -2.6, 0.82] for k=650, c=45, dt=0.004.", + "status": "verified", + "sources": [ + { + "label": "src/runtime/host-timing.ts lines 162", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/runtime/host-timing.ts", + "lines": "162", + "quote": "return [1 - constants.stiffness * dt * dt, dt * velocityKeep, -constants.stiffness * dt, velocityKeep]" + } + ] + }, + { + "id": "c2-os-track-geometry", + "claim": "On-screen only: at 1280 x 800 the track spans x 614..666 and y 384..416; the scripted press is at (640, 400).", + "status": "verified", + "sources": [ + { + "label": "template/toggle.test.ts lines 9", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "9", + "quote": "where the track spans x 614..666 and y 384..416" + }, + { + "label": "template/toggle.test.ts lines 22", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "22", + "quote": "pointer('down', 640, 400, 0, 1, false)" + } + ] + }, + { + "id": "c2-flip-is-select", + "caption": 8, + "status": "verified", + "claim": "The flip of the on bit is built with emitSelect, like the arming step.", + "sources": [ + { + "label": "template/toggle.ts lines 63", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "63", + "quote": "const on = emitSelect(circuit, all(cells.up.active, armedAfterDown, insideTrack(cells.up)), cells.on, not(cells.on))" + } + ] + }, + { + "id": "c2-spring-steps", + "caption": 11, + "status": "verified", + "claim": "The time since the last evaluation decides how many fixed 4 ms steps the spring takes, which a program cannot loop over at runtime.", + "sources": [ + { + "label": "docs/tutorial.md lines 106", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "106", + "quote": "the time since the last evaluation decides how many fixed 4 ms steps the spring takes" + } + ] + } + ] + }, + { + "number": 3, + "title": "The Host Loop", + "claims": [ + { + "id": "c3-host-defined", + "claim": "The JavaScript around the program is the host: it writes inputs into the table, evaluates, copies next-state values back, and writes the DOM.", + "status": "verified", + "sources": [ + { + "label": "docs/README.md lines 21", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/README.md", + "lines": "21", + "quote": "It writes inputs into cells, evaluates, copies next-state values back into the table, and writes the DOM." + } + ], + "caption": 0 + }, + { + "id": "c3-queue", + "claim": "Event handlers only queue events with their timestamps.", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 110", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "110", + "quote": "Event handlers only queue events with their timestamps." + }, + { + "label": "template/toggle.ts lines 107", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "107", + "quote": "queued.push(event)" + } + ], + "caption": 1 + }, + { + "id": "c3-advance-frame", + "claim": "advanceFrame plans one evaluation per distinct event timestamp, a settle check every 64 ms of simulated time, and one at the frame timestamp; at a settle check a spring near rest lands on its destination and stops.", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 110", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "110", + "quote": "one per distinct event timestamp, which steps the spring to that timestamp before applying the events, a settle check every 64 ms of simulated time, and one at the frame's timestamp" + }, + { + "label": "docs/tutorial.md lines 110", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "110", + "quote": "A settle check is an evaluation in which a spring that is within 0.01 px of its destination and no faster than 0.01 px/s lands on the destination and stops." + } + ], + "caption": 2 + }, + { + "id": "c3-two-evaluations", + "claim": "The test's events arrive 3 ms before their frames; applying advanceFrame's rule (one evaluation per event time plus one at the frame) gives two evaluations in an event frame.", + "status": "inferred", + "sources": [ + { + "label": "template/toggle.test.ts lines 9", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "9", + "quote": "Each event arrives 3 ms before" + }, + { + "label": "docs/tutorial.md lines 110", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "110", + "quote": "and one at the frame's timestamp" + } + ], + "caption": 3 + }, + { + "id": "c3-host-writes-table", + "claim": "The callback writes the spring matrix, settleCheck, snap, viewport and each event's active bit and coordinates into the table before evaluating.", + "status": "verified", + "sources": [ + { + "label": "template/toggle.ts lines 115", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "115", + "quote": "cells.springMatrix.forEach((wire, index) => { values[wire] = matrix[index]! })" + }, + { + "label": "template/toggle.ts lines 116", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "116", + "quote": "values[cells.settleCheck] = evaluation.settleCheck ? 1 : 0" + }, + { + "label": "template/toggle.ts lines 124", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "124", + "quote": "values[cells[kind].active] = 1" + } + ], + "caption": 4 + }, + { + "id": "c3-evaluate-and-write-back", + "claim": "evaluate(values) runs the program; when the evaluation writes back, source.set(result.nextValues) copies next state into the table.", + "status": "verified", + "sources": [ + { + "label": "template/toggle.ts lines 129", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "129", + "quote": "const result = evaluate(values)" + }, + { + "label": "template/toggle.ts lines 130", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "130", + "quote": "if (evaluation.writesBack) source.set(result.nextValues)" + }, + { + "label": "template/toggle.ts lines 130", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "130", + "quote": "if (evaluation.writesBack) source.set(result.nextValues)" + } + ], + "caption": 5 + }, + { + "id": "c3-frame-rate-independence", + "claim": "With 'events and settle checks', a frame's own evaluation runs on a copy, so stored state depends on the events, not the frame rate; the repo's replay tests (spring-matrix traces at 10, 30, 60 and 120 fps, the toast stack, the desktop demo's window manager) find stored tables identical in every bit across frame rates.", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 143", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "143", + "quote": "a frame's own evaluation runs on a copy, so stored state depends on the events and not on the frame rate" + }, + { + "label": "docs/execution-model.md lines 226", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "226", + "quote": "every stored table and every drawn output at a shared timestamp is identical in every bit at every frame rate" + }, + { + "label": "docs/execution-model.md lines 224", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "224", + "quote": "at 10, 30, 60 and 120 fps" + }, + { + "label": "docs/behavior-choices.md lines 48", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/behavior-choices.md", + "lines": "48", + "quote": "every stored table and every drawn top at a shared timestamp is identical in every bit at 10, 30, 60 and 120 fps" + }, + { + "label": "docs/behavior-choices.md lines 52", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/behavior-choices.md", + "lines": "52", + "quote": "replays a timestamped session at several frame rates and checks that the stored tables and drawn outputs are identical" + } + ], + "caption": 6 + }, + { + "id": "c3-dom-from-outputs", + "claim": "mountToggle runs one step per requestAnimationFrame, then writes the DOM from output numbers only.", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 151", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "151", + "quote": "then writes the DOM from output numbers only" + }, + { + "label": "template/toggle.ts lines 153", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.ts", + "lines": "153", + "quote": "track.style.transform" + } + ], + "caption": 7 + }, + { + "id": "c3-reference-test", + "claim": "toggle-reference.ts is the same switch in plain JavaScript and toggle.test.ts compares the two over a scripted trace every frame.", + "status": "verified", + "sources": [ + { + "label": "template/toggle.test.ts lines 1", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "1", + "quote": "Runs the toggle switch program and its plain JavaScript version over a scripted trace, and compares them every frame." + }, + { + "label": "docs/tutorial.md lines 165", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "165", + "quote": "is the same switch in plain JavaScript, in float64" + } + ], + "caption": 8 + }, + { + "id": "c3-exact-decisions", + "claim": "The test compares on, armed and the track position exactly and the thumb within 1e-5 px (largest error over the whole trace).", + "status": "verified", + "sources": [ + { + "label": "docs/tutorial.md lines 167", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "167", + "quote": "the on and armed bits and the track position exactly, the thumb within 1e-5 px" + }, + { + "label": "template/toggle.test.ts lines 59", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "59", + "quote": "expect(largestThumbError).toBeLessThan(1e-5)" + } + ], + "caption": 9 + }, + { + "id": "c3-edge-presses", + "claim": "The trace presses 1/64 px left of the track (no arm), exactly on the left and top edges (arms), and 1/64 px inside the bottom right corner (flips); tests put presses exactly on edges since an inexact decision shows there first.", + "status": "verified", + "sources": [ + { + "label": "template/toggle.test.ts lines 28", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "28", + "quote": "1/64 px left of the track" + }, + { + "label": "template/toggle.test.ts lines 29", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "29", + "quote": "exactly on the left and top edges" + }, + { + "label": "template/toggle.test.ts lines 35", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "35", + "quote": "1/64 px inside the bottom right corner" + }, + { + "label": "docs/tutorial.md lines 169", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "169", + "quote": "since an inexact decision shows there first" + } + ], + "caption": 10 + }, + { + "id": "c3-no-early-exit", + "claim": "The program has no runtime loop and runs every operation on every evaluation, so a loop must become a count, a tree, a table, or the same work on every item.", + "status": "inferred", + "sources": [ + { + "label": "docs/tutorial.md lines 13", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "13", + "quote": "nothing in the program branches or loops at runtime" + }, + { + "label": "docs/README.md lines 9", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/README.md", + "lines": "9", + "quote": "The program runs every operation on every frame and can't stop early" + } + ], + "caption": 11 + }, + { + "id": "c3-os-38-frames", + "claim": "On-screen only: the scripted trace has 38 frames during which the clock runs; the thumb rests at 20 px; measured thumb error at most 1.3e-6 px.", + "status": "verified", + "sources": [ + { + "label": "template/toggle.test.ts lines 58", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "58", + "quote": "expect(animatingFrames).toBe(38)" + }, + { + "label": "template/toggle.test.ts lines 61", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "61", + "quote": "thumbOffset).toBe(20)" + }, + { + "label": "docs/tutorial.md lines 167", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "167", + "quote": "differ from float64 by 1.3e-6 px at most over this 20 px travel" + } + ] + }, + { + "id": "c3-os-grid-throw", + "claim": "On-screen only: a pointer coordinate off the 1/64 px grid throws \"down.x must be a multiple of 0.015625\".", + "status": "verified", + "sources": [ + { + "label": "template/toggle.test.ts lines 68", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle.test.ts", + "lines": "68", + "quote": "down.x must be a multiple of 0.015625" + } + ] + }, + { + "id": "c3-os-timeline-derived", + "claim": "On-screen only (derived, not printed output): from the first event at 1045 ms the clock starts at the release (1061 ms) and its first settle check falls at 1061 + 64 = 1125 ms; at 16 ms a frame the clock runs 4 steps of 4 ms.", + "status": "inferred", + "sources": [ + { + "label": "template/toggle-reference.ts lines 49", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle-reference.ts", + "lines": "49", + "quote": "switchState.animatedUntilMs = eventMs" + }, + { + "label": "template/toggle-reference.ts lines 22", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "template/toggle-reference.ts", + "lines": "22", + "quote": "% 64 === 0 && withinThresholds()" + } + ] + } + ] + }, + { + "number": 4, + "title": "Loops That Cannot Stop", + "claims": [ + { + "id": "c4-loops-stop-programs-dont", + "claim": "A loop stops at the first item that fails the test; the program runs every operation on every frame and cannot stop early.", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 69", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "69", + "quote": "A loop stops at the first item that fails the test." + }, + { + "label": "docs/patterns.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "3", + "quote": "The program runs every operation on every frame and can't stop early" + } + ], + "caption": 0 + }, + { + "id": "c4-four-answers", + "claim": "A loop becomes a count, a tree, a table, or the same work on every item.", + "status": "verified", + "sources": [ + { + "label": "docs/README.md lines 9", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/README.md", + "lines": "9", + "quote": "a loop becomes a count, a tree, a table, or the same work on every item" + } + ], + "caption": 1 + }, + { + "id": "c4-find-index-bits", + "claim": "findIndex on row bottoms 40 80 80 140 180 with pointerY 90: the bits [rowBottom <= pointerY] are 1 1 1 0 0.", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 72", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "72", + "quote": "rowBottoms.findIndex(rowBottom => pointerY < rowBottom)" + }, + { + "label": "docs/patterns.md lines 75", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "75", + "quote": "rowBottom 40 80 80 140 180 pointerY = 90" + }, + { + "label": "docs/patterns.md lines 76", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "76", + "quote": "[rowBottom <= pointerY] 1 1 1 0 0" + } + ], + "caption": 2 + }, + { + "id": "c4-count-is-index", + "claim": "The number of items that pass is the index the loop returns, because the bits never go from 0 back to 1.", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 69", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "69", + "quote": "the number of items that pass is the index the loop returns" + }, + { + "label": "docs/patterns.md lines 80", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "80", + "quote": "The count gives the loop's index because the bits never go from 0 back to 1" + } + ], + "caption": 3 + }, + { + "id": "c4-find-index-cost", + "claim": "195 operations in 3 layers at 64 rows, against 447 in 34 for a tree built with emitFirstActive.", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 81", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "81", + "quote": "195 in 3 at 64 rows, against 447 in 34 for a tree built with emitFirstActive" + } + ], + "caption": 4 + }, + { + "id": "c4-monotone-caveat", + "claim": "The bits must be monotone: unsorted bottoms give a wrong row without an error (bottoms 40, 30, 80 at pointerY 35: count 1, loop 0).", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 82", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "82", + "quote": "unsorted bottoms give a wrong row without an error" + }, + { + "label": "docs/patterns.md lines 82", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "82", + "quote": "at pointerY 35 the count is 1 while the loop returns 0" + } + ], + "caption": 5 + }, + { + "id": "c4-chain-depth", + "claim": "Each total reads the total before it, so the loop is a chain n layers deep; at 1,024 rows the chain is 1,023 operations in 1,023 layers.", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 177", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "177", + "quote": "Each total reads the total before it, so the loop is a chain n layers deep." + }, + { + "label": "docs/patterns.md lines 193", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "193", + "quote": "At 1,024 rows, the chain is 1,023 operations in 1,023 layers" + } + ], + "caption": 6 + }, + { + "id": "c4-brent-kung", + "claim": "A Brent-Kung prefix network, built by combining in pairs and then pairs of pairs, gives every running total of 1,024 rows in 18 layers for 2,036 operations.", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 193", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "193", + "quote": "Brent-Kung about 2n operations in about 2 log2 n layers, 2,036 in 18" + }, + { + "label": "docs/patterns.md lines 50", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "50", + "quote": "2,036 combines in 18 levels" + } + ], + "caption": 7 + }, + { + "id": "c4-ranks-not-sorting", + "claim": "An item's position after sorting is a count of the items before it; one comparison bit per pair, each rank one sum: 4,096 operations in 3 layers at 64 items (keys on a grid).", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 358", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "358", + "quote": "that position is a count: how many items come before this item" + }, + { + "label": "docs/patterns.md lines 373", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "373", + "quote": "One emitCompare(circuit, keys[earlier], '<=', keys[later]) per pair gives both of the pair's entries, and each rank is one sum of bits" + }, + { + "label": "docs/patterns.md lines 374", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "374", + "quote": "4,096 operations in 3 layers at 64 items, with keys on a grid" + } + ], + "caption": 9 + }, + { + "id": "c4-table-javascript-writes", + "claim": "When data only changes between frames, JavaScript runs the loop for every input and writes the results into cells; the program reads one with 1 operation in 1 layer at any table size.", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 458", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "458", + "quote": "JavaScript runs the loop for every possible input whenever the data changes and writes the results into cells" + }, + { + "label": "docs/patterns.md lines 470", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "470", + "quote": "1 operation in 1 layer per read at any table size" + } + ], + "caption": 10 + }, + { + "id": "c4-cpu-vs-gpu", + "claim": "On the CPU executor time follows operations more than layers and the tree takes longer than the loop; on a GPU a level of the tree is one dispatch over all of its combines, i.e. one batch of parallel work.", + "status": "paraphrase", + "sources": [ + { + "label": "docs/patterns.md lines 7", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "7", + "quote": "an evaluation's time follows its operations more than its layers" + }, + { + "label": "docs/patterns.md lines 54", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "54", + "quote": "On the CPU executor, the tree takes longer than the loop, with either network." + }, + { + "label": "docs/patterns.md lines 55", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "55", + "quote": "a level of the tree is one dispatch over all of the level's combines" + } + ], + "caption": 11 + }, + { + "id": "c4-loop-ends-like-branch", + "claim": "Synthesis, worded as an inference: like the branch, each loop pattern runs every test or combine in fixed layers and reads the answer from a count, a sum or a read.", + "status": "inferred", + "sources": [ + { + "label": "docs/patterns.md lines 65", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "65", + "quote": "Each item's test or score is computed in the same layers, and one small structure over the results picks one item: a count when the results read 1...1 0...0, or a tree when they can be in any order." + } + ], + "caption": 12 + }, + { + "id": "c4-os-bk-diagram", + "claim": "On-screen only: the 4-item running-total diagram (layer 1: 0..1 and 2..3; layer 2: 0..3 and 0..2 = (0..1) + h2).", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 186", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "186", + "quote": "layer 1: 0..1 and 2..3" + }, + { + "label": "docs/patterns.md lines 188", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "188", + "quote": "layer 2: 0..3, and 0..2 = (0..1) + h2" + } + ] + }, + { + "id": "c4-os-rank-table", + "claim": "On-screen only: keys 30 10 30 20 give ranks 2, 0, 3, 1; of two equal keys the earlier item comes first.", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 366", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "366", + "quote": "keys 30 10 30 20" + }, + { + "label": "docs/patterns.md lines 367", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "367", + "quote": "rank 2" + }, + { + "label": "docs/patterns.md lines 369", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "369", + "quote": "rank 3" + } + ] + }, + { + "id": "c4-os-ancestor-table", + "claim": "On-screen only: depths [0 1 2 1 2 3], ancestorAtLevel rows e.g. row 3 [0 3 3 3], row 5 [0 3 4 5]; the read is ancestorAtLevel[row x 4 + level], 1 operation.", + "status": "verified", + "sources": [ + { + "label": "docs/patterns.md lines 465", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "465", + "quote": "depths [0 1 2 1 2 3]" + }, + { + "label": "docs/patterns.md lines 465", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "465", + "quote": "row 3: [0 3 3 3], row 5: [0 3 4 5]" + }, + { + "label": "docs/patterns.md lines 466", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "466", + "quote": "ancestor = ancestorAtLevel[row × 4 + level], 1 operation" + } + ] + }, + { + "id": "c4-integers-exact", + "caption": 8, + "status": "verified", + "claim": "Integers below 2^24 (about sixteen million) add exactly in any grouping, which is when running totals built as a tree match the loop bit for bit.", + "sources": [ + { + "label": "docs/patterns.md lines 39", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/patterns.md", + "lines": "39", + "quote": "Integers below 2^24, and multiples of a power-of-two grid while totals stay small, add exactly in any grouping" + } + ] + } + ] + }, + { + "number": 5, + "title": "Exact, Trainable, Wide", + "claims": [ + { + "id": "c5-exact-compare", + "claim": "For every pair of values within the declared ranges the comparison bit equals JavaScript's comparison; on a grid a >= b is relu(1 - relu(-D)), equal to relu(D + 1) - relu(D) at every integer.", + "status": "verified", + "sources": [ + { + "label": "docs/execution-model.md lines 118", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "118", + "quote": "For every pair of values within the declared ranges, the bit equals JavaScript's comparison of the two wires' FP32 values." + }, + { + "label": "docs/execution-model.md lines 123", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "123", + "quote": "a >= b is relu(1 - relu(-D)), which equals relu(D + 1) - relu(D) at every integer" + } + ], + "caption": 0 + }, + { + "id": "c5-two-or-fifteen", + "claim": "Integer bits on a 1/64 grid: 2 operations in 2 layers; sign cascade: 15 operations in 14 layers for >=; both forms return the same bits.", + "status": "verified", + "sources": [ + { + "label": "docs/execution-model.md lines 136", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "136", + "quote": "integer bits on a 1/64 grid | 2 operations, 2 layers | 2 operations, 2 layers" + }, + { + "label": "docs/execution-model.md lines 134", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "134", + "quote": "sign cascade | 16 operations, 15 layers | 15 operations, 14 layers" + }, + { + "label": "docs/tutorial.md lines 53", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "53", + "quote": "Both forms return the same bits" + } + ], + "caption": 1 + }, + { + "id": "c5-exact-means-bits", + "claim": "Exactness means equality to the stated FP32 calculation, not arbitrary float64 arithmetic; toggle positions differ from float64 by 1.3e-6 px at most over the 20 px travel.", + "status": "verified", + "sources": [ + { + "label": "docs/execution-model.md lines 257", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "257", + "quote": "Exactness means equality to the stated FP32 calculation" + }, + { + "label": "docs/tutorial.md lines 167", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/tutorial.md", + "lines": "167", + "quote": "differ from float64 by 1.3e-6 px at most over this 20 px travel" + } + ], + "caption": 2 + }, + { + "id": "c5-backprop", + "claim": "Almost every operation is a weighted sum or a product with a relu, so the program can be differentiated: backpropagation differentiates the intended real arithmetic; boolean atoms and index reads are the exceptions that throw by default.", + "status": "paraphrase", + "sources": [ + { + "label": "README.md lines 29", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "29", + "quote": "the full program stays differentiable" + }, + { + "label": "docs/execution-model.md lines 36", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "36", + "quote": "Backpropagation differentiates the intended real arithmetic" + }, + { + "label": "docs/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/README.md", + "lines": "3", + "quote": "mostly weighted sums and products with a ReLU" + }, + { + "label": "docs/execution-model.md lines 36", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "36", + "quote": "`backpropagateReference` throws when a gradient reaches a boolean atom" + }, + { + "label": "docs/execution-model.md lines 112", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "112", + "quote": "By default, `backpropagateReference` throws when a nonzero gradient reaches an index read" + } + ], + "caption": 3 + }, + { + "id": "c5-straight-through", + "claim": "backpropagateReference throws at a boolean atom unless booleanGradient is 'straight-through', which passes the output gradient to the atom's score terms unchanged; a cascade gives its operands gradient 0, the derivative of a step.", + "status": "verified", + "sources": [ + { + "label": "docs/execution-model.md lines 36", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "36", + "quote": "throws when a gradient reaches a boolean atom, unless the caller passes {booleanGradient: 'straight-through'}" + }, + { + "label": "src/runtime/numeric-ui-core.ts lines 720", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/runtime/numeric-ui-core.ts", + "lines": "720", + "quote": "throw new Error('cannot backpropagate through a boolean atom')" + }, + { + "label": "src/runtime/numeric-ui-core.ts lines 722", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/runtime/numeric-ui-core.ts", + "lines": "722", + "quote": "accumulateTermGradients(gradients, operation.leftTerms, outputGradient)" + }, + { + "label": "docs/execution-model.md lines 143", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "143", + "quote": "A cascade gives its operands gradient 0, the derivative of a step." + } + ], + "caption": 4 + }, + { + "id": "c5-desktop-frozen", + "claim": "A small desktop with 3 windows and a dock, run by one compiled program of 234 source cells, 4,646 live operations in 130 layers, 9,909 stored weights; every weight written by builder code, none trained.", + "status": "verified", + "sources": [ + { + "label": "src/handwiredos/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/handwiredos/README.md", + "lines": "3", + "quote": "A small desktop with 3 windows (Monitor, Notes and Photos) and a dock, run by one compiled program." + }, + { + "label": "src/handwiredos/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/handwiredos/README.md", + "lines": "3", + "quote": "Every weight of the program is written by builder code, and none is trained." + }, + { + "label": "src/handwiredos/README.md lines 13", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/handwiredos/README.md", + "lines": "13", + "quote": "The program has 234 source cells, 4,646 live operations in 130 layers and 9,909 stored weights" + } + ], + "caption": 5 + }, + { + "id": "c5-learned-gallery", + "claim": "The learned gallery's 244 source cells are the trained coefficients of a small network (two ReGLU layers of 6 units), stepped with Adam using gradients from circuit.backpropagateReference; training runs 600 steps a second while the user edits.", + "status": "verified", + "sources": [ + { + "label": "src/learned-gallery/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/learned-gallery/README.md", + "lines": "3", + "quote": "Training steps the coefficients with Adam, using gradients from" + }, + { + "label": "src/learned-gallery/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/learned-gallery/README.md", + "lines": "3", + "quote": "Its 244 source cells are the trained coefficients" + }, + { + "label": "src/learned-gallery/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/learned-gallery/README.md", + "lines": "3", + "quote": "two ReGLU layers of 6 units" + }, + { + "label": "src/learned-gallery/README.md lines 17", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/learned-gallery/README.md", + "lines": "17", + "quote": "Training runs 600 steps a second at any frame rate" + } + ], + "caption": 6 + }, + { + "id": "c5-json-search", + "claim": "JSON search searches 100,000 generated JSON documents again at each keystroke, on the GPU, with two fixed numeric programs; every document runs the same operations and reads no other document's values, so one pass runs them all (100,000 is the default for a window of 720 px or wider).", + "status": "paraphrase", + "sources": [ + { + "label": "src/json-search/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "3", + "quote": "A page that searches 100,000 generated JSON documents again at each keystroke, on the GPU, with two fixed numeric programs." + }, + { + "label": "src/json-search/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "3", + "quote": "every document runs the same operations and reads no other document's values, so one pass runs them all" + }, + { + "label": "src/json-search/README.md lines 47", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "47", + "quote": "A window narrower than 720 px" + } + ], + "caption": 7 + }, + { + "id": "c5-timings", + "claim": "On an Apple M5 Max in Chrome 155 with 100,000 documents: a search takes 11 to 15 ms at a keystroke; RegExp 8 to 28 ms for ordinary patterns and 2.1 s for ([a-z]+)+x; neither is always faster; the same smallest program takes 13 s on the CPU.", + "status": "verified", + "sources": [ + { + "label": "src/json-search/README.md lines 53", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "53", + "quote": "A search takes 11 to 15 ms at a keystroke" + }, + { + "label": "src/json-search/README.md lines 53", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "53", + "quote": "RegExp takes 8 to 28 ms for ordinary patterns" + }, + { + "label": "src/json-search/README.md lines 53", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "53", + "quote": "and 2.1 s for ([a-z]+)+x, which makes it backtrack" + }, + { + "label": "src/json-search/README.md lines 53", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "53", + "quote": "So neither the search nor RegExp is always faster" + }, + { + "label": "src/json-search/README.md lines 53", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "53", + "quote": "What it costs, on an Apple M5 Max in Chrome 155 with 100,000 documents" + } + ], + "caption": 8 + }, + { + "id": "c5-recap", + "claim": "Recap of chapters one to four (sources in those chapters).", + "status": "inferred", + "sources": [ + { + "label": "README.md lines 20", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "README.md", + "lines": "20", + "quote": "Both branches compute, and above4 picks one" + }, + { + "label": "docs/README.md lines 9", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/README.md", + "lines": "9", + "quote": "a loop becomes a count, a tree, a table, or the same work on every item" + } + ], + "caption": 9 + }, + { + "id": "c5-closing", + "claim": "Synthesis: both sides of a select always compute, and an exact bit (0 or 1) weights which result survives.", + "status": "inferred", + "sources": [ + { + "label": "docs/expressions.md lines 11", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/expressions.md", + "lines": "11", + "quote": "Both sides always compute" + }, + { + "label": "docs/execution-model.md lines 264", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "docs/execution-model.md", + "lines": "264", + "quote": "gate × on + (1 - gate) × off" + } + ], + "caption": 11 + }, + { + "id": "c5-os-wallpaper", + "claim": "On-screen only: the desktop demo's wallpaper draws every stored weight as one arc (our arcs are a stand-in for the idea).", + "status": "verified", + "sources": [ + { + "label": "src/handwiredos/README.md lines 42", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/handwiredos/README.md", + "lines": "42", + "quote": "the wallpaper, which draws every stored weight as one arc" + } + ] + }, + { + "id": "c5-os-round-trip", + "claim": "On-screen only: a round trip to the GPU alone takes 0.6 ms or more, so a GPU is of no use for one document (the lab measured 0.58 ms for one document).", + "status": "verified", + "sources": [ + { + "label": "src/json-search/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "3", + "quote": "since a round trip to the GPU alone takes 0.6 ms or more" + } + ] + }, + { + "id": "c5-os-first-fit", + "claim": "On-screen only: Story preset first fit takes 630 to 750 steps; seeded weights put the photos in a pile; network has 20 output slots, page uses up to 12.", + "status": "verified", + "sources": [ + { + "label": "src/learned-gallery/README.md lines 17", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/learned-gallery/README.md", + "lines": "17", + "quote": "630 to 750 steps for Story" + }, + { + "label": "src/learned-gallery/README.md lines 28", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/learned-gallery/README.md", + "lines": "28", + "quote": "The seeded weights put the photos in a pile" + }, + { + "label": "src/learned-gallery/README.md lines 3", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/learned-gallery/README.md", + "lines": "3", + "quote": "The network has 20 output slots, of which the page uses up to 12." + } + ] + }, + { + "id": "c5-os-page-line", + "claim": "On-screen only: the page line \"10,559 of 100,000 documents match. This program: 13 ms on the GPU. Native RegExp: 21 ms.\" and the 13 s CPU figure.", + "status": "verified", + "sources": [ + { + "label": "src/json-search/README.md lines 30", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "30", + "quote": "10,559 of 100,000 documents match. This program: 13 ms on the GPU. Native RegExp: 21 ms." + }, + { + "label": "src/json-search/README.md lines 53", + "repo": "chenglou/handwired@4ea7fcb27ebe93acf49b9f29a93e162b975bad46", + "path": "src/json-search/README.md", + "lines": "53", + "quote": "On the CPU, a search on the smallest program takes 13 s." + } + ] + } + ] + } + ] +} diff --git a/public/generated/library.json b/public/generated/library.json index 5a500ce9..ec08b59a 100644 --- a/public/generated/library.json +++ b/public/generated/library.json @@ -37,6 +37,43 @@ ], "createdAt": "2026-10-15T18:00:31.597Z" }, + { + "slug": "handwired", + "title": "Handwired", + "subtitle": "If-else, loops and state as matrix multiply and relu", + "author": "Brett Lamy", + "role": "Software Engineer at Replay", + "color": "#41607f", + "href": "?bundle=handwired", + "chapters": [ + { + "number": 1, + "title": "Both Branches Run", + "duration": 108.623 + }, + { + "number": 2, + "title": "A Switch of Arithmetic", + "duration": 106.439 + }, + { + "number": 3, + "title": "The Host Loop", + "duration": 113.87 + }, + { + "number": 4, + "title": "Loops That Cannot Stop", + "duration": 119.071 + }, + { + "number": 5, + "title": "Exact, Trainable, Wide", + "duration": 122.276 + } + ], + "createdAt": "2026-10-15T21:39:17.372Z" + }, { "slug": "jeva-fresh-decisions", "title": "Jeva: Fresh Decisions", @@ -5957,4 +5994,4 @@ "createdAt": "2026-07-15T20:55:33.777Z" } ] -} \ No newline at end of file +}