Files
rohitg00--ai-engineering-fr…/site/figures.js
T
2026-07-13 12:09:03 +08:00

939 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* figures.js — large-scale animated SVG explainers in the makingsoftware.com style.
Auto-mount: <div data-figure="tokenizer-bpe"></div>
Catalog:
tokenizer-bpe — text → words → BPE merges, watching merges learned live
ngram-machine — sliding window builds prob table, samples new text
attention-matrix — full N×N attention grid lighting up, value blend
embedding-arithmetic — king man + woman → queen, vectors flying in 2D
transformer-block — data flowing through residual + MHA + FFN layers
attention-lookup — compact (legacy) softmax-of-scores
token-strip — compact (legacy) text vs words vs BPE
loss-curve — compact (legacy) training curve
embedding-projection — compact (legacy) cluster jitter
kv-cache — compact (legacy) growing cache
No deps. Hover-pause. Step controls on big figures. Reduced-motion = nice still.
*/
(function () {
'use strict';
const NS = 'http://www.w3.org/2000/svg';
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
function el(name, attrs = {}, kids = []) {
const e = document.createElementNS(NS, name);
for (const k in attrs) e.setAttribute(k, attrs[k]);
(Array.isArray(kids) ? kids : [kids]).filter(Boolean).forEach(c => e.appendChild(c));
return e;
}
const txt = (s) => document.createTextNode(s);
// host loop with hover-pause + optional step control
function loop(host, fn, period = 6000, opts = {}) {
let raf, paused = false, t0 = performance.now(), localT = 0;
const onTick = (now) => {
if (!paused) localT = ((now - t0) % period) / period;
fn(localT);
raf = requestAnimationFrame(onTick);
};
host.addEventListener('mouseenter', () => paused = true);
host.addEventListener('mouseleave', () => paused = false);
if (reduced) { fn(opts.staticT ?? 0.62); return () => {}; }
raf = requestAnimationFrame(onTick);
return () => cancelAnimationFrame(raf);
}
function softmax(xs, t = 1) {
const m = Math.max(...xs), e = xs.map(x => Math.exp((x - m) / t));
const s = e.reduce((a, b) => a + b, 0);
return e.map(v => v / s);
}
function lerp(a, b, t) { return a + (b - a) * t; }
function easeIO(t) { return t < .5 ? 2*t*t : 1 - Math.pow(-2*t+2, 2) / 2; }
/* ───────────────────────── BIG FIGURES ───────────────────────── */
/* tokenizer-bpe ── 720x520
A long string ticks through three "tracks":
1. raw chars (pixel-by-pixel)
2. byte-pair scan (highlights consecutive pair, increments count)
3. merged tokens (newly-learned merges replace pairs)
Right side shows the merge rules being learned, top-to-bottom.
*/
function tokenizerBPE(host) {
const W = 760, H = 540;
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: '100%', role: 'img', 'aria-label': 'BPE tokenizer training' });
host.appendChild(svg);
// Title row
svg.appendChild(el('text', { x: 18, y: 22, 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.16em', fill:'var(--ink-mute)' }, [txt('CORPUS')]));
svg.appendChild(el('text', { x: 18, y: 162, 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.16em', fill:'var(--ink-mute)' }, [txt('SCAN PAIRS')]));
svg.appendChild(el('text', { x: 18, y: 322, 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.16em', fill:'var(--ink-mute)' }, [txt('MERGED TOKENS')]));
svg.appendChild(el('text', { x: W - 18, y: 22, 'text-anchor':'end', 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.16em', fill:'var(--blueprint)' }, [txt('LEARNED MERGES')]));
// The tiny corpus — chars in cells
const corpus = "the_cat_sat_on_the_mat_the_cat_ate";
const chars = corpus.split('');
const CW = 18, CH = 28, X0 = 18, Y_RAW = 36, Y_SCAN = 176, Y_MERGE = 336;
// Raw row
const rawCells = chars.map((c, i) => {
const x = X0 + i * CW;
const g = el('g', {});
g.appendChild(el('rect', { x, y: Y_RAW, width: CW - 1, height: CH, fill: 'transparent', stroke:'var(--rule-soft)', 'stroke-width': 1 }));
g.appendChild(el('text', { x: x + (CW-1)/2, y: Y_RAW + CH/2 + 4, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 13, fill:'var(--ink)' }, [txt(c === '_' ? '·' : c)]));
svg.appendChild(g);
return g;
});
// Scan row — same chars, with a sliding pair highlighter and a "count++" pop
const scanCells = chars.map((c, i) => {
const x = X0 + i * CW;
const g = el('g', {});
g.appendChild(el('rect', { x, y: Y_SCAN, width: CW - 1, height: CH, fill: 'transparent', stroke:'var(--rule-soft)', 'stroke-width': 1 }));
g.appendChild(el('text', { x: x + (CW-1)/2, y: Y_SCAN + CH/2 + 4, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 13, fill:'var(--ink)' }, [txt(c === '_' ? '·' : c)]));
svg.appendChild(g);
return g;
});
const scanBracket = el('rect', { y: Y_SCAN - 4, height: CH + 8, width: CW * 2 + 2, fill:'transparent', stroke:'var(--blueprint)', 'stroke-width': 2 });
svg.appendChild(scanBracket);
// pair-counter floater
const counter = el('g', {});
const counterBg = el('rect', { x: 0, y: 0, width: 84, height: 24, fill:'var(--blueprint-tint-strong)', stroke:'var(--blueprint)', 'stroke-width': 1 });
const counterTx = el('text', { x: 42, y: 16, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 12, fill:'var(--ink)' });
counter.appendChild(counterBg); counter.appendChild(counterTx);
counter.setAttribute('opacity', '0');
svg.appendChild(counter);
// Merge row — starts as chars, accrues merges as the run advances
// Plan a merge schedule: each "step" merges a pair across all occurrences.
const mergeSchedule = [
{ pair: ['t','h'], joined: 'th' },
{ pair: ['th','e'], joined: 'the' },
{ pair: ['the','·'], joined: 'the·' },
{ pair: ['c','a'], joined: 'ca' },
{ pair: ['ca','t'], joined: 'cat' }
];
const STEPS = mergeSchedule.length + 1;
function tokensAt(step) {
// Start from chars (· for spaces).
let toks = chars.map(c => c === '_' ? '·' : c);
const lastStep = Math.min(step, mergeSchedule.length);
for (let s = 0; s < lastStep; s++) {
const { pair, joined } = mergeSchedule[s];
const out = [];
for (let i = 0; i < toks.length; i++) {
if (toks[i] === pair[0] && toks[i+1] === pair[1]) {
out.push(joined); i++;
} else out.push(toks[i]);
}
toks = out;
}
return toks;
}
// we render the merge row as a flexible run; redraw on step change
const mergeRowG = el('g', {});
svg.appendChild(mergeRowG);
function drawMergeRow(step, justMerged) {
while (mergeRowG.firstChild) mergeRowG.removeChild(mergeRowG.firstChild);
const toks = tokensAt(step);
let x = X0;
toks.forEach(tok => {
const w = Math.max(CW, tok.length * 9 + 10);
const isJustMerged = justMerged && tok === justMerged;
const fill = isJustMerged ? 'var(--blueprint)' : (tok.length > 1 ? 'var(--blueprint-tint-strong)' : 'transparent');
const stroke = tok.length > 1 ? 'var(--blueprint)' : 'var(--rule-soft)';
mergeRowG.appendChild(el('rect', { x, y: Y_MERGE, width: w - 2, height: CH, fill, stroke, 'stroke-width': 1 }));
mergeRowG.appendChild(el('text', {
x: x + (w - 2)/2, y: Y_MERGE + CH/2 + 4, 'text-anchor':'middle',
'font-family':'var(--font-mono)', 'font-size': 12,
fill: isJustMerged ? 'var(--bg)' : 'var(--ink)'
}, [txt(tok)]));
x += w;
});
}
drawMergeRow(0, null);
// Learned-merges sidebar (right side)
const SBX = W - 240;
svg.appendChild(el('rect', { x: SBX, y: 36, width: 220, height: 360, fill:'transparent', stroke:'var(--rule-soft)', 'stroke-width': 1 }));
const merges = mergeSchedule.map((m, i) => {
const y = 60 + i * 30;
const g = el('g', { opacity: 0 });
g.appendChild(el('text', { x: SBX + 12, y, 'font-family':'var(--font-mono)', 'font-size': 12, fill:'var(--ink-mute)' }, [txt(String(i+1).padStart(2,'0'))]));
g.appendChild(el('text', { x: SBX + 40, y, 'font-family':'var(--font-mono)', 'font-size': 12, fill:'var(--ink)' },
[txt(m.pair.join(' + ') + ' → ')]));
g.appendChild(el('text', { x: SBX + 156, y, 'font-family':'var(--font-mono)', 'font-size': 12, 'font-weight': 700, fill:'var(--blueprint)' }, [txt(m.joined)]));
svg.appendChild(g);
return g;
});
// vocab-size readout
const vocabReadout = el('text', { x: W - 18, y: H - 18, 'text-anchor':'end', 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.12em', fill:'var(--ink-mute)' });
svg.appendChild(vocabReadout);
// status caption
const status = el('text', { x: 18, y: H - 18, 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.12em', fill:'var(--blueprint)' });
svg.appendChild(status);
// Animation: each "stage" first slides scan window across, then commits a merge.
const PAIR_SCAN_FRAC = 0.65; // 65% of stage scanning, 35% applying merge
let lastStage = -1;
loop(host, (t) => {
const stage = Math.floor(t * STEPS);
const tInStage = (t * STEPS) - stage;
const m = mergeSchedule[stage];
// Scan bracket position — sweeps across the corpus
const scanIdx = Math.floor(tInStage / PAIR_SCAN_FRAC * (chars.length - 1));
const clamped = Math.min(Math.max(scanIdx, 0), chars.length - 2);
scanBracket.setAttribute('x', X0 + clamped * CW - 1);
// Tint scan cells: highlight current pair
scanCells.forEach((g, i) => {
const r = g.firstChild;
r.setAttribute('fill', (i === clamped || i === clamped + 1) ? 'var(--blueprint-tint-strong)' : 'transparent');
});
// floating count next to the scan bracket
if (m && tInStage < PAIR_SCAN_FRAC) {
const a = chars[clamped] === '_' ? '·' : chars[clamped];
const b = chars[clamped+1] === '_' ? '·' : chars[clamped+1];
const looksLikePair = (a === m.pair[0] && b === m.pair[1]) ||
(m.pair[0].length > 1 && a + b === m.pair[0] + m.pair[1]);
counter.setAttribute('opacity', looksLikePair ? '1' : '0.35');
counterTx.textContent = `${a}${b} ×${looksLikePair ? Math.max(1, Math.floor(tInStage * 8)) : 1}`;
counter.setAttribute('transform', `translate(${X0 + clamped * CW - 12}, ${Y_SCAN - 36})`);
} else {
counter.setAttribute('opacity', '0');
}
// Apply merge once we cross PAIR_SCAN_FRAC
const applied = tInStage >= PAIR_SCAN_FRAC ? stage + 1 : stage;
if (applied !== lastStage) {
drawMergeRow(applied, m && tInStage >= PAIR_SCAN_FRAC ? m.joined : null);
lastStage = applied;
}
// reveal sidebar entries
merges.forEach((g, i) => {
const visible = applied > i ? 1 : (applied === i && tInStage >= PAIR_SCAN_FRAC ? 1 : 0);
g.setAttribute('opacity', visible);
});
// status + vocab
const baseVocab = 28; // alphabet-ish
vocabReadout.textContent = 'VOCAB · ' + (baseVocab + applied);
if (!m) {
status.textContent = 'TOKENIZER LEARNED · LOOPING';
} else if (tInStage < PAIR_SCAN_FRAC) {
status.textContent = 'COUNTING PAIRS · step ' + (stage+1) + '/' + mergeSchedule.length;
} else {
status.textContent = 'MERGED ' + m.pair.join(' + ') + ' → ' + m.joined;
}
}, 14000);
}
/* ngram-machine ── 720x420
Sliding bigram window over a sentence builds a probability table on the right.
Once the table is "warm" it samples a new sentence beneath.
*/
function ngramMachine(host) {
const W = 760, H = 460;
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: '100%', role: 'img', 'aria-label': 'N-gram language model' });
host.appendChild(svg);
svg.appendChild(el('text', { x: 18, y: 22, 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.16em', fill:'var(--ink-mute)' }, [txt('CORPUS · BIGRAM WINDOW')]));
svg.appendChild(el('text', { x: 18, y: 248, 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.16em', fill:'var(--blueprint)' }, [txt('SAMPLED FROM P(next | current)')]));
svg.appendChild(el('text', { x: W - 18, y: 22, 'text-anchor':'end', 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.16em', fill:'var(--ink-mute)' }, [txt('TRANSITION TABLE')]));
const tokens = ['the','cat','sat','on','the','mat','the','dog','sat','on','the','log','the','cat','ate'];
// chip layout
const Y_CORPUS = 56;
const chipW = 56, chipH = 30, chipGap = 6;
const chips = tokens.map((tok, i) => {
const x = 18 + i * (chipW + chipGap);
const g = el('g', {});
g.appendChild(el('rect', { x, y: Y_CORPUS, width: chipW, height: chipH, fill:'transparent', stroke:'var(--rule-soft)', 'stroke-width':1 }));
g.appendChild(el('text', { x: x + chipW/2, y: Y_CORPUS + 20, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size':12, fill:'var(--ink)' }, [txt(tok)]));
svg.appendChild(g);
return { g, x, tok };
});
// window bracket
const winRect = el('rect', { y: Y_CORPUS - 4, height: chipH + 8, width: chipW * 2 + chipGap, fill:'transparent', stroke:'var(--blueprint)', 'stroke-width': 2 });
svg.appendChild(winRect);
// Build the actual transition table (counts → probs) we'll animate filling
const transitions = {};
for (let i = 0; i < tokens.length - 1; i++) {
const a = tokens[i], b = tokens[i+1];
transitions[a] = transitions[a] || {};
transitions[a][b] = (transitions[a][b] || 0) + 1;
}
// sidebar: list a few rows from the table
const SBX = W - 280, SBY = 50;
svg.appendChild(el('rect', { x: SBX, y: SBY, width: 264, height: 180, fill:'transparent', stroke:'var(--rule-soft)', 'stroke-width': 1 }));
const rowsToShow = ['the','cat','sat','on','dog'];
const rowEls = {};
rowsToShow.forEach((src, i) => {
const y = SBY + 20 + i * 32;
const head = el('text', { x: SBX + 12, y, 'font-family':'var(--font-mono)', 'font-size': 12, fill:'var(--ink)' }, [txt(src + ' →')]);
svg.appendChild(head);
rowEls[src] = { y, bars: [] };
});
// each (src,dst) bar gets a placeholder we fill in later
function ensureBar(src, dst) {
const row = rowEls[src]; if (!row) return null;
let b = row.bars.find(b => b.dst === dst);
if (b) return b;
const idx = row.bars.length;
const x = SBX + 70 + idx * 40;
const wid = 36;
const labelTx = el('text', { x: x + wid/2, y: row.y - 16, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 9, fill:'var(--ink-mute)' }, [txt(dst)]);
const bg = el('rect', { x, y: row.y - 12, width: wid, height: 12, fill: 'var(--rule-soft)' });
const fg = el('rect', { x, y: row.y - 12, width: 0, height: 12, fill: 'var(--blueprint)' });
svg.appendChild(labelTx); svg.appendChild(bg); svg.appendChild(fg);
b = { dst, fg, count: 0, max: 1 };
row.bars.push(b);
return b;
}
// sampling lane (bottom)
const sampleY = 300;
const sampleChipsG = el('g', {});
svg.appendChild(sampleChipsG);
// status
const status = el('text', { x: 18, y: H - 18, 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.12em', fill:'var(--blueprint)' });
svg.appendChild(status);
const phaseTx = el('text', { x: W - 18, y: H - 18, 'text-anchor':'end', 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.12em', fill:'var(--ink-mute)' });
svg.appendChild(phaseTx);
// Animation: phase A — slide window through corpus filling table; phase B — generate new tokens
const PHASE_A = 0.6; // 60% counting, 40% generating
const totalPairs = tokens.length - 1;
let lastSampleStep = -1, sampleHistory = [];
function rebuildSample() {
while (sampleChipsG.firstChild) sampleChipsG.removeChild(sampleChipsG.firstChild);
let x = 18;
sampleHistory.forEach((tok, i) => {
const wid = 56;
const fresh = (i === sampleHistory.length - 1);
sampleChipsG.appendChild(el('rect', { x, y: sampleY, width: wid, height: chipH, fill: fresh ? 'var(--blueprint)' : 'var(--blueprint-tint-strong)', stroke:'var(--blueprint)', 'stroke-width': 1 }));
sampleChipsG.appendChild(el('text', { x: x + wid/2, y: sampleY + 20, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 12, fill: fresh ? 'var(--bg)' : 'var(--ink)' }, [txt(tok)]));
x += wid + chipGap;
});
}
function pickNext(cur) {
const row = transitions[cur]; if (!row) return tokens[0];
const entries = Object.entries(row);
const total = entries.reduce((a, [, c]) => a + c, 0);
let r = Math.random() * total;
for (const [k, c] of entries) { r -= c; if (r <= 0) return k; }
return entries[entries.length-1][0];
}
loop(host, (t) => {
if (t < PHASE_A) {
const localT = t / PHASE_A;
const i = Math.min(Math.floor(localT * totalPairs), totalPairs - 1);
winRect.setAttribute('x', chips[i].x - 1);
chips.forEach((c, k) => c.g.firstChild.setAttribute('fill', (k === i || k === i+1) ? 'var(--blueprint-tint-strong)' : 'transparent'));
// accumulate counts up to index i
// (recompute deterministically each frame for visual consistency)
const counts = {};
for (let p = 0; p <= i; p++) {
const a = tokens[p], b = tokens[p+1];
counts[a] = counts[a] || {};
counts[a][b] = (counts[a][b] || 0) + 1;
}
Object.keys(rowEls).forEach(src => {
if (!counts[src]) return;
const total = Object.values(counts[src]).reduce((a, b) => a + b, 0);
Object.entries(counts[src]).forEach(([dst, c]) => {
const bar = ensureBar(src, dst);
if (!bar) return;
const prob = c / total;
bar.fg.setAttribute('width', prob * 36);
});
});
status.textContent = 'COUNTING BIGRAMS · ' + (i+1) + ' / ' + totalPairs;
phaseTx.textContent = 'PHASE 1';
// reset sample
sampleHistory = []; lastSampleStep = -1;
rebuildSample();
} else {
const localT = (t - PHASE_A) / (1 - PHASE_A);
// fade out window
winRect.setAttribute('x', -50);
chips.forEach(c => c.g.firstChild.setAttribute('fill', 'transparent'));
const N = 9;
const step = Math.min(N, Math.floor(localT * N));
if (step !== lastSampleStep) {
if (step === 0) sampleHistory = ['the'];
else if (step > sampleHistory.length - 1) {
const cur = sampleHistory[sampleHistory.length - 1];
sampleHistory.push(pickNext(cur));
}
rebuildSample();
lastSampleStep = step;
}
status.textContent = 'SAMPLING · ' + sampleHistory.join(' ');
phaseTx.textContent = 'PHASE 2';
}
}, 12000);
}
/* attention-matrix ── 720x520
A 12-token sentence, full N×N attention grid lighting up.
A query head sweeps row-by-row; cells light by softmax weight.
A small "value blend" panel below shows the resulting context vector.
*/
function attentionMatrix(host) {
const W = 760, H = 540;
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: '100%', role: 'img', 'aria-label': 'Self-attention matrix' });
host.appendChild(svg);
const TOKENS = ['the','cat','sat','on','the','mat','because','it','was','warm','and','sunny'];
const N = TOKENS.length;
const M = 384; // matrix size
const MX = 110, MY = 70;
const cell = M / N;
svg.appendChild(el('text', { x: 18, y: 22, 'font-family':'var(--font-mono)', 'font-size':11, 'letter-spacing':'.16em', fill:'var(--ink-mute)' }, [txt('SELF-ATTENTION · Q · Kᵀ → SOFTMAX → · V')]));
svg.appendChild(el('text', { x: MX + M + 18, y: MY + 14, 'font-family':'var(--font-mono)', 'font-size': 10, 'letter-spacing':'.14em', fill:'var(--ink-mute)' }, [txt('KEYS →')]));
svg.appendChild(el('text', { x: MX - 8, y: MY - 12, 'font-family':'var(--font-mono)', 'font-size': 10, 'letter-spacing':'.14em', fill:'var(--ink-mute)', 'text-anchor':'end' }, [txt('QUERIES ↓')]));
// top labels
TOKENS.forEach((t, i) => {
svg.appendChild(el('text', { x: MX + i*cell + cell/2, y: MY - 8, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 10, fill:'var(--ink-soft)' }, [txt(t)]));
});
// left labels
TOKENS.forEach((t, i) => {
svg.appendChild(el('text', { x: MX - 8, y: MY + i*cell + cell/2 + 4, 'text-anchor':'end', 'font-family':'var(--font-mono)', 'font-size': 10, fill:'var(--ink-soft)' }, [txt(t)]));
});
// grid cells
const cells = [];
for (let i = 0; i < N; i++) {
cells.push([]);
for (let j = 0; j < N; j++) {
const r = el('rect', {
x: MX + j*cell, y: MY + i*cell, width: cell - 1, height: cell - 1,
fill:'var(--blueprint-tint)', stroke:'var(--rule-soft)', 'stroke-width': .5
});
svg.appendChild(r);
cells[i].push(r);
}
}
// current row highlight
const rowHi = el('rect', { x: MX - 4, y: MY, width: M + 8, height: cell, fill:'transparent', stroke:'var(--blueprint)', 'stroke-width': 1.5 });
svg.appendChild(rowHi);
// value blend bar — shows weighted blend at bottom
const VY = MY + M + 32;
svg.appendChild(el('text', { x: MX, y: VY - 8, 'font-family':'var(--font-mono)', 'font-size':10, 'letter-spacing':'.14em', fill:'var(--blueprint)' }, [txt('CONTEXT VECTOR = Σ αⱼ · vⱼ')]));
const valBars = [];
const VBW = M / N;
for (let j = 0; j < N; j++) {
const x = MX + j * VBW;
svg.appendChild(el('rect', { x, y: VY, width: VBW - 2, height: 60, fill: 'var(--rule-soft)' }));
const fg = el('rect', { x, y: VY + 60, width: VBW - 2, height: 0, fill: 'var(--blueprint)' });
svg.appendChild(fg);
svg.appendChild(el('text', { x: x + (VBW-2)/2, y: VY + 78, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 9, fill: 'var(--ink-mute)' }, [txt(TOKENS[j])]));
valBars.push(fg);
}
// status
const status = el('text', { x: 18, y: H - 18, 'font-family':'var(--font-mono)', 'font-size': 11, 'letter-spacing':'.12em', fill:'var(--blueprint)' });
svg.appendChild(status);
// synthetic affinity: each query "looks for" semantically related keys.
// We hard-code an interesting case: "it" (idx 7) softly attends to "cat" (1).
function affinityRow(qi) {
// Base: noise; add bonus for a chosen target
const targets = { 6: [3,4,5], 7: [1, 0], 9: [3,4,5], 10:[6], 11:[6] };
const tArr = targets[qi];
return Array.from({length: N}, (_, kj) => {
let s = -Math.pow(qi - kj, 2) * 0.15;
if (tArr && tArr.includes(kj)) s += 2.4;
if (kj === qi) s += 1.0;
return s;
});
}
loop(host, (t) => {
const qF = t * N;
const qi = Math.min(N - 1, Math.floor(qF));
const sub = qF - qi;
rowHi.setAttribute('y', MY + qi * cell);
// fill row weights
const w = softmax(affinityRow(qi), 0.7);
for (let j = 0; j < N; j++) {
const a = w[j];
cells[qi][j].setAttribute('fill', `color-mix(in srgb, var(--blueprint) ${Math.round(a*100*1.4)}%, var(--blueprint-tint))`);
}
// reset other rows to soft default (subtle decay)
for (let i = 0; i < N; i++) {
if (i === qi) continue;
for (let j = 0; j < N; j++) {
cells[i][j].setAttribute('fill', i === j ? 'var(--blueprint-tint-strong)' : 'var(--blueprint-tint)');
}
}
// value blend
for (let j = 0; j < N; j++) {
valBars[j].setAttribute('y', VY + 60 - w[j] * 60);
valBars[j].setAttribute('height', w[j] * 60);
}
status.textContent = 'QUERY: "' + TOKENS[qi] + '" · TOP KEY: "' + TOKENS[w.indexOf(Math.max(...w))] + '"';
}, 11000);
}
/* embedding-arithmetic ── 760x440
Words plotted in 2D embedding space. Vectors animate to show:
king man + woman ≈ queen
*/
function embeddingArithmetic(host) {
const W = 760, H = 460, P = 60;
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: '100%', role: 'img', 'aria-label': 'Word vector arithmetic' });
host.appendChild(svg);
svg.appendChild(el('text', { x: 18, y: 22, 'font-family':'var(--font-mono)', 'font-size':11, 'letter-spacing':'.16em', fill:'var(--ink-mute)' }, [txt('EMBEDDING SPACE · PCA(2) PROJECTION')]));
svg.appendChild(el('text', { x: W - 18, y: 22, 'text-anchor':'end', 'font-family':'var(--font-mono)', 'font-size':11, 'letter-spacing':'.16em', fill:'var(--blueprint)' }, [txt('king man + woman ≈ queen')]));
// grid
for (let i = 0; i <= 8; i++) {
const x = P + (i / 8) * (W - 2*P);
const y = P + (i / 8) * (H - 2*P - 40);
svg.appendChild(el('line', { x1: x, y1: P, x2: x, y2: H - P - 40, stroke:'var(--rule-soft)', 'stroke-width': .6 }));
svg.appendChild(el('line', { x1: P, y1: y, x2: W - P, y2: y, stroke:'var(--rule-soft)', 'stroke-width': .6 }));
}
// ambient cloud (greyed words)
const cloud = ['cat','dog','run','sit','apple','car','river','code','book','sky','fire','river'];
cloud.forEach((wd, i) => {
const cx = P + ((Math.sin(i*1.7)+1)/2) * (W - 2*P);
const cy = P + ((Math.cos(i*1.3)+1)/2) * (H - 2*P - 40);
svg.appendChild(el('circle', { cx, cy, r: 2, fill:'var(--ink-mute)', opacity: .35 }));
svg.appendChild(el('text', { x: cx + 6, y: cy + 4, 'font-family':'var(--font-mono)', 'font-size': 10, fill:'var(--ink-mute)', opacity: .5 }, [txt(wd)]));
});
// anchored points (in unit square 0..1)
const pts = {
king: { x: 0.30, y: 0.34 },
man: { x: 0.30, y: 0.62 },
queen: { x: 0.62, y: 0.34 },
woman: { x: 0.62, y: 0.62 }
};
function px(p) { return { x: P + p.x * (W - 2*P), y: P + p.y * (H - 2*P - 40) }; }
const dots = {};
Object.entries(pts).forEach(([name, p]) => {
const { x, y } = px(p);
const g = el('g', {});
g.appendChild(el('circle', { cx: x, cy: y, r: 5, fill:'var(--blueprint)' }));
g.appendChild(el('text', { x: x + 10, y: y + 5, 'font-family':'var(--font-mono)', 'font-size': 13, 'font-weight': 700, fill:'var(--ink)' }, [txt(name)]));
svg.appendChild(g);
dots[name] = { x, y };
});
// arrow defs
const defs = el('defs', {});
const mk = (id, color) => {
const m = el('marker', { id, viewBox:'0 0 10 10', refX: 8, refY: 5, markerWidth: 6, markerHeight: 6, orient: 'auto' });
m.appendChild(el('path', { d: 'M0,0 L10,5 L0,10 Z', fill: color }));
defs.appendChild(m);
};
mk('arr-blue', 'var(--blueprint)');
mk('arr-warn', 'var(--warn)');
mk('arr-soft', 'var(--ink-mute)');
svg.appendChild(defs);
// moving traveller dot + trail line + result dot
const trail = el('path', { fill:'none', stroke:'var(--blueprint)', 'stroke-width': 2, 'stroke-dasharray':'4 4' });
svg.appendChild(trail);
const trav = el('circle', { r: 6, fill:'var(--warn)', stroke:'var(--bg)', 'stroke-width': 2 });
svg.appendChild(trav);
const result = el('g', { opacity: 0 });
result.appendChild(el('circle', { cx: dots.queen.x, cy: dots.queen.y, r: 14, fill:'transparent', stroke:'var(--warn)', 'stroke-width': 2, 'stroke-dasharray':'3 3' }));
result.appendChild(el('text', { x: dots.queen.x + 22, y: dots.queen.y - 12, 'font-family':'var(--font-mono)', 'font-size': 11, fill:'var(--warn)' }, [txt('= QUEEN ✓')]));
svg.appendChild(result);
const eqn = el('text', { x: W/2, y: H - 28, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 13, 'letter-spacing':'.06em', fill:'var(--ink)' }, [txt('king')]);
svg.appendChild(eqn);
// four phases: show king → subtract man → add woman → land near queen
loop(host, (t) => {
// path waypoints
const start = dots.king;
const afterMinusMan = { x: start.x + (start.x - dots.man.x), y: start.y + (start.y - dots.man.y) };
const afterPlusWoman = { x: afterMinusMan.x + (dots.woman.x - dots.king.x), y: afterMinusMan.y + (dots.woman.y - dots.king.y) };
// ↑ that's "king + (woman - man)" which lands on queen.
const wp = [start, afterMinusMan, afterPlusWoman];
let pos = start, segIdx = 0, segT = 0;
if (t < 0.33) { segIdx = 0; segT = t / 0.33; }
else if (t < 0.66) { segIdx = 1; segT = (t - 0.33) / 0.33; }
else { segIdx = 2; segT = (t - 0.66) / 0.34; }
const a = wp[segIdx], b = wp[Math.min(segIdx+1, wp.length-1)];
const e = easeIO(segT);
pos = { x: lerp(a.x, b.x, e), y: lerp(a.y, b.y, e) };
trav.setAttribute('cx', pos.x); trav.setAttribute('cy', pos.y);
// trail = path so far
let d = `M ${start.x} ${start.y} `;
for (let i = 0; i < segIdx; i++) d += `L ${wp[i+1].x} ${wp[i+1].y} `;
d += `L ${pos.x} ${pos.y}`;
trail.setAttribute('d', d);
// equation status
if (t < 0.05) eqn.textContent = 'king';
else if (t < 0.33) eqn.textContent = 'king';
else if (t < 0.66) eqn.textContent = 'king man';
else if (t < 0.95) eqn.textContent = 'king man + woman';
else eqn.textContent = 'king man + woman ≈ queen';
result.setAttribute('opacity', t > 0.93 ? 1 : 0);
}, 9000);
}
/* transformer-block ── 760x540
Token vectors flow through: + pos enc → MHA → residual → norm → FFN → residual → norm
Beams of light travel left → right; residual arcs glow when reached.
*/
function transformerBlock(host) {
const W = 820, H = 620;
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: '100%', role: 'img', 'aria-label': 'Transformer block data flow' });
host.appendChild(svg);
svg.appendChild(el('text', { x: 18, y: 22, 'font-family':'var(--font-mono)', 'font-size':11, 'letter-spacing':'.16em', fill:'var(--ink-mute)' }, [txt('ONE TRANSFORMER BLOCK · EMBED · MHA · NORM · FFN · NORM')]));
// input column (left): 6 token "vectors" stacked
const colX = [70, 220, 380, 540, 720];
const yTop = 70, vH = 26, vW = 78, gap = 7, NTOK = 6;
function drawColumn(x, label) {
svg.appendChild(el('text', { x, y: yTop - 12, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size':10, 'letter-spacing':'.14em', fill:'var(--ink-mute)' }, [txt(label)]));
const arr = [];
for (let i = 0; i < NTOK; i++) {
const y = yTop + i * (vH + gap);
const r = el('rect', { x: x - vW/2, y, width: vW, height: vH, fill:'var(--blueprint-tint)', stroke:'var(--rule-soft)', 'stroke-width': 1 });
svg.appendChild(r);
// little stripes inside to look like a vector
for (let k = 0; k < 5; k++) {
svg.appendChild(el('rect', { x: x - vW/2 + 6 + k*14, y: y + 8, width: 10, height: 12, fill:'var(--blueprint-tint-strong)' }));
}
arr.push({ r, x, y, cy: y + vH/2 });
}
return arr;
}
const colInput = drawColumn(colX[0], 'EMBED + POS');
const colMHA = drawColumn(colX[1], 'AFTER MHA');
const colResA = drawColumn(colX[2], '+ RESIDUAL · NORM');
const colFFN = drawColumn(colX[3], 'AFTER FFN');
const colResB = drawColumn(colX[4], '+ RESIDUAL · NORM');
// op boxes & residual arcs sit BELOW the column block so nothing overlaps
const colsBottom = yTop + NTOK*(vH+gap); // ≈ 268
const OP_Y = colsBottom + 50; // op-box top
const ARC_Y = OP_Y + 110; // residual arc baseline
// residual arcs (skip lines), curving DOWN under the op row
function arc(x1, x2, y) {
return el('path', {
d: `M ${x1} ${y - 80} C ${(x1+x2)/2} ${y + 30}, ${(x1+x2)/2} ${y + 30}, ${x2} ${y - 80}`,
fill:'none', stroke:'var(--ink-mute)', 'stroke-width': 1.2,
'stroke-dasharray':'4 4', opacity: .5
});
}
const resA = arc(colX[0], colX[2], ARC_Y);
const resB = arc(colX[2], colX[4], ARC_Y);
svg.appendChild(resA); svg.appendChild(resB);
svg.appendChild(el('text', { x: (colX[0]+colX[2])/2, y: ARC_Y + 50, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 10, fill:'var(--ink-mute)' }, [txt('residual')]));
svg.appendChild(el('text', { x: (colX[2]+colX[4])/2, y: ARC_Y + 50, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 10, fill:'var(--ink-mute)' }, [txt('residual')]));
// op boxes between columns
function opBox(x, label, sub) {
const w = 110, h = 50, y = OP_Y;
const g = el('g', {});
g.appendChild(el('rect', { x: x - w/2, y, width: w, height: h, fill:'var(--bg)', stroke:'var(--blueprint)', 'stroke-width': 1.4 }));
g.appendChild(el('text', { x, y: y + 21, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 13, 'font-weight': 700, fill:'var(--blueprint)' }, [txt(label)]));
g.appendChild(el('text', { x, y: y + 37, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size': 8.5, fill:'var(--ink-mute)' }, [txt(sub)]));
svg.appendChild(g);
return { x, y, w, h, box: g };
}
const op1 = opBox((colX[0]+colX[1])/2, 'MHA', 'multi-head attn');
const op2 = opBox((colX[1]+colX[2])/2, '+', 'residual + norm');
const op3 = opBox((colX[2]+colX[3])/2, 'FFN', 'feed-forward');
const op4 = opBox((colX[3]+colX[4])/2, '+', 'residual + norm');
// beam particles
const beam = el('circle', { r: 5, fill:'var(--warn)' });
svg.appendChild(beam);
// pulse glow circle on op
const pulse = el('circle', { r: 0, fill:'transparent', stroke:'var(--warn)', 'stroke-width': 2, opacity: 0 });
svg.appendChild(pulse);
// status
const status = el('text', { x: 18, y: H - 18, 'font-family':'var(--font-mono)', 'font-size':11, 'letter-spacing':'.12em', fill:'var(--blueprint)' });
svg.appendChild(status);
const stations = [
{ from: colX[0], to: colX[1], op: op1, label: 'tokens query each other', color: 'var(--blueprint)' },
{ from: colX[1], to: colX[2], op: op2, label: 'add residual, normalize', color: 'var(--ink)' },
{ from: colX[2], to: colX[3], op: op3, label: 'per-token MLP transform', color: 'var(--blueprint)' },
{ from: colX[3], to: colX[4], op: op4, label: 'add residual, normalize', color: 'var(--ink)' }
];
loop(host, (t) => {
const stage = Math.min(stations.length - 1, Math.floor(t * stations.length));
const sT = (t * stations.length) - stage;
const s = stations[stage];
// beam travels through op
let bx, by = OP_Y + 25;
if (sT < 0.4) {
bx = lerp(s.from, s.op.x, sT / 0.4);
} else if (sT < 0.6) {
bx = s.op.x;
// pulse
const p = (sT - 0.4) / 0.2;
pulse.setAttribute('cx', s.op.x); pulse.setAttribute('cy', s.op.y + s.op.h/2);
pulse.setAttribute('r', p * 60);
pulse.setAttribute('opacity', 1 - p);
} else {
bx = lerp(s.op.x, s.to, (sT - 0.6) / 0.4);
pulse.setAttribute('opacity', 0);
}
beam.setAttribute('cx', bx); beam.setAttribute('cy', by);
// tint columns through which the beam has passed
[colInput, colMHA, colResA, colFFN, colResB].forEach((col, i) => {
const reached = i <= stage + (sT > 0.6 ? 1 : 0);
col.forEach(c => c.r.setAttribute('fill', reached ? 'var(--blueprint-tint-strong)' : 'var(--blueprint-tint)'));
});
// residual arc highlight
resA.setAttribute('opacity', stage >= 1 ? 1 : 0.5);
resA.setAttribute('stroke', stage >= 1 && sT < 0.6 ? 'var(--warn)' : 'var(--ink-mute)');
resB.setAttribute('opacity', stage >= 3 ? 1 : 0.5);
resB.setAttribute('stroke', stage >= 3 && sT < 0.6 ? 'var(--warn)' : 'var(--ink-mute)');
status.textContent = 'STAGE ' + (stage+1) + ' / 4 · ' + s.label;
}, 12000);
}
/* ───── compact / legacy figures (kept) ───── */
function attentionLookup(host) {
const W = 720, H = 280;
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: '100%' });
host.appendChild(svg);
const KEYS = 8;
const qx = 60, qy = H/2;
svg.appendChild(el('rect', { x: qx-26, y: qy-22, width: 52, height: 44, fill:'var(--blueprint)' }));
svg.appendChild(el('text', { x: qx, y: qy+5, 'text-anchor':'middle', fill:'var(--bg)', 'font-family':'var(--font-mono)', 'font-size':14, 'font-weight':500 }, [txt('q')]));
svg.appendChild(el('text', { x: qx, y: qy+44, 'text-anchor':'middle', fill:'var(--ink-mute)', 'font-family':'var(--font-mono)', 'font-size':9, 'letter-spacing':'.16em' }, [txt('QUERY')]));
const kx0 = 200, kgap = 60, kw = 36, kh = 44;
const ks = [], scoreLines = [], bars = [];
for (let i = 0; i < KEYS; i++) {
const x = kx0 + i*kgap;
const k = el('rect', { x: x-kw/2, y: 30, width: kw, height: kh, fill:'var(--blueprint-tint-strong)', stroke:'var(--blueprint)', 'stroke-width':1 });
svg.appendChild(k); ks.push(k);
svg.appendChild(el('text', { x, y: 55, 'text-anchor':'middle', fill:'var(--ink)', 'font-family':'var(--font-mono)', 'font-size':12 }, [txt('k'+(i+1))]));
const ln = el('line', { x1: qx+26, y1: qy, x2: x, y2: 74, stroke:'var(--rule-soft)', 'stroke-width':1, 'stroke-dasharray':'2 4' });
svg.appendChild(ln); scoreLines.push(ln);
svg.appendChild(el('rect', { x: x-kw/2, y: 158, width: kw, height: 80, fill:'var(--rule-soft)' }));
const fg = el('rect', { x: x-kw/2, y: 238, width: kw, height: 0, fill:'var(--blueprint)' });
svg.appendChild(fg); bars.push(fg);
svg.appendChild(el('text', { x, y: 254, 'text-anchor':'middle', fill:'var(--ink-mute)', 'font-family':'var(--font-mono)', 'font-size':10 }, [txt('α'+(i+1))]));
}
svg.appendChild(el('text', { x: W-20, y: 22, 'text-anchor':'end', fill:'var(--ink-mute)', 'font-family':'var(--font-mono)', 'font-size':10, 'letter-spacing':'.16em' }, [txt('SOFTMAX(qK/√d)')]));
loop(host, (t) => {
const center = t * (KEYS - 1);
const raw = Array.from({length: KEYS}, (_, i) => -Math.pow(i - center, 2) * 0.7);
const w = softmax(raw, 0.4);
for (let i = 0; i < KEYS; i++) {
const a = w[i];
bars[i].setAttribute('y', 238 - a * 78);
bars[i].setAttribute('height', a * 78);
ks[i].setAttribute('fill', `color-mix(in srgb, var(--blueprint) ${Math.round(a*100)}%, var(--blueprint-tint-strong))`);
scoreLines[i].setAttribute('stroke', a > 0.18 ? 'var(--blueprint)' : 'var(--rule-soft)');
scoreLines[i].setAttribute('stroke-dasharray', a > 0.18 ? '4 0' : '2 4');
}
}, 6200);
}
function tokenStrip(host) {
const W = 720, H = 140;
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: '100%' });
host.appendChild(svg);
const text = "the cat sat on the mat";
const tokens = text.split(' ');
const subwords = ["the", " cat", " sat", " on", " the", " m", "at"];
const rows = [
{ y: 28, items: [text], label: 'TEXT' },
{ y: 68, items: tokens, label: 'WORDS' },
{ y: 108, items: subwords, label: 'BPE' }
];
rows.forEach((row, ri) => {
svg.appendChild(el('text', { x: 8, y: row.y+4, fill:'var(--ink-mute)', 'font-family':'var(--font-mono)', 'font-size':10, 'letter-spacing':'.16em' }, [txt(row.label)]));
let x = 88;
row.items.forEach((tok) => {
const tx = tok.replace(/^ /, '·');
const w = Math.max(36, tx.length * 9 + 12);
const g = el('g', {});
g.appendChild(el('rect', { x, y: row.y-14, width: w, height: 24,
fill: ri === 2 ? 'var(--blueprint-tint)' : 'transparent',
stroke: ri === 0 ? 'var(--rule-soft)' : 'var(--blueprint)', 'stroke-width': 1 }));
g.appendChild(el('text', { x: x+w/2, y: row.y+2, 'text-anchor':'middle', 'font-family':'var(--font-mono)', 'font-size':12, fill:'var(--ink)' }, [txt(tx)]));
if (ri === 2) g.setAttribute('opacity', '0');
svg.appendChild(g);
if (ri === 2) row._gs = (row._gs || []).concat(g);
x += w + 6;
});
});
loop(host, (t) => {
const gs = rows[2]._gs || [];
const visible = Math.floor(t * (gs.length + 2));
gs.forEach((g, i) => g.setAttribute('opacity', i < visible ? '1' : '0'));
}, 5200);
}
function lossCurve(host) {
const W = 720, H = 240, P = 36;
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width:'100%' });
host.appendChild(svg);
for (let i = 0; i <= 4; i++) {
const y = P + i * ((H - 2*P) / 4);
svg.appendChild(el('line', { x1: P, y1: y, x2: W-P, y2: y, stroke:'var(--rule-soft)', 'stroke-width': 1 }));
}
svg.appendChild(el('line', { x1: P, y1: P, x2: P, y2: H-P, stroke:'var(--ink)', 'stroke-width': 1 }));
svg.appendChild(el('line', { x1: P, y1: H-P, x2: W-P, y2: H-P, stroke:'var(--ink)', 'stroke-width': 1 }));
svg.appendChild(el('text', { x: P, y: 18, 'font-family':'var(--font-mono)', 'font-size':10, 'letter-spacing':'.14em', fill:'var(--ink-mute)' }, [txt('LOSS')]));
svg.appendChild(el('text', { x: W-P, y: H-8, 'text-anchor':'end', 'font-family':'var(--font-mono)', 'font-size':10, 'letter-spacing':'.14em', fill:'var(--ink-mute)' }, [txt('STEP →')]));
const N = 240;
const path = el('path', { fill:'none', stroke:'var(--blueprint)', 'stroke-width':1.6 });
svg.appendChild(path);
const dot = el('circle', { r: 4, fill:'var(--blueprint)', cx: P, cy: P });
svg.appendChild(dot);
const lbl = el('text', { 'font-family':'var(--font-mono)', 'font-size':10, fill:'var(--blueprint)' });
svg.appendChild(lbl);
function lossFn(i) { return 0.06 + 0.9 * Math.exp(-i / 70) + 0.025 * Math.sin(i * 0.5) * Math.exp(-i/120); }
loop(host, (t) => {
const upto = Math.floor(t * N) + 1;
let d = '';
for (let i = 0; i < upto; i++) {
const x = P + (i / (N-1)) * (W - 2*P);
const y = P + (1 - lossFn(i)) * (H - 2*P);
d += (i ? 'L' : 'M') + x.toFixed(1) + ' ' + y.toFixed(1) + ' ';
}
path.setAttribute('d', d);
const lx = P + ((upto-1) / (N-1)) * (W - 2*P);
const ly = P + (1 - lossFn(upto-1)) * (H - 2*P);
dot.setAttribute('cx', lx); dot.setAttribute('cy', ly);
lbl.setAttribute('x', lx + 8); lbl.setAttribute('y', ly - 6);
lbl.textContent = 'loss=' + lossFn(upto-1).toFixed(3);
}, 5400);
}
function embeddingProjection(host) {
const W = 720, H = 280;
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: '100%' });
host.appendChild(svg);
for (let i = 0; i <= 8; i++) {
const x = (i / 8) * W;
const y = (i / 8) * H;
svg.appendChild(el('line', { x1: x, y1: 0, x2: x, y2: H, stroke:'var(--rule-soft)', 'stroke-width':.6 }));
svg.appendChild(el('line', { x1: 0, y1: y, x2: W, y2: y, stroke:'var(--rule-soft)', 'stroke-width':.6 }));
}
const clusters = [
{ x: 0.22, y: 0.30, words: ['king','queen','prince','duke'] },
{ x: 0.74, y: 0.36, words: ['apple','banana','peach','grape'] },
{ x: 0.42, y: 0.78, words: ['run','walk','jump','sit'] }
];
const all = [];
clusters.forEach((c, ci) => c.words.forEach((w, wi) => {
const node = el('g', {});
const cx = c.x*W + (Math.cos(wi)*22), cy = c.y*H + (Math.sin(wi*1.3)*22);
node.appendChild(el('circle', { cx, cy, r: 4, fill:'var(--blueprint)' }));
node.appendChild(el('text', { x: cx+8, y: cy+4, 'font-family':'var(--font-mono)', 'font-size':11, fill:'var(--ink)' }, [txt(w)]));
svg.appendChild(node);
all.push({ node, ci, wi });
}));
loop(host, (t) => {
all.forEach(o => {
const dx = Math.sin((t * Math.PI*2) + o.ci + o.wi) * 2;
const dy = Math.cos((t * Math.PI*2) + o.ci - o.wi) * 2;
o.node.setAttribute('transform', `translate(${dx} ${dy})`);
});
}, 7000);
svg.appendChild(el('text', { x: 12, y: 22, 'font-family':'var(--font-mono)', 'font-size':10, 'letter-spacing':'.16em', fill:'var(--ink-mute)' }, [txt('EMBEDDING SPACE · 2D PROJECTION')]));
}
function kvCache(host) {
const W = 720, H = 220, P = 32, COLS = 12;
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width:'100%' });
host.appendChild(svg);
const cw = (W - 2*P) / COLS;
const cells = [];
for (let r = 0; r < 2; r++) {
const label = r === 0 ? 'K' : 'V';
svg.appendChild(el('text', { x: 12, y: P+30 + r*70, 'font-family':'var(--font-mono)', 'font-size':14, fill:'var(--ink)' }, [txt(label)]));
for (let c = 0; c < COLS; c++) {
const x = P + c*cw, y = P + r*70;
const cell = el('rect', { x, y, width: cw - 4, height: 50, fill:'transparent', stroke:'var(--rule-soft)', 'stroke-width':1 });
svg.appendChild(cell);
cells.push({ cell, c, r });
}
}
svg.appendChild(el('text', { x: P, y: H-10, 'font-family':'var(--font-mono)', 'font-size':10, 'letter-spacing':'.16em', fill:'var(--ink-mute)' }, [txt('CACHE GROWS LEFT → RIGHT · NEW TOKEN HIGHLIGHTED')]));
loop(host, (t) => {
const head = Math.floor(t * COLS);
cells.forEach(({ cell, c }) => {
if (c < head) cell.setAttribute('fill', 'var(--blueprint-tint-strong)');
else if (c === head) cell.setAttribute('fill', 'var(--blueprint)');
else cell.setAttribute('fill', 'transparent');
});
}, 6000);
}
const FIGURES = {
'tokenizer-bpe': tokenizerBPE,
'ngram-machine': ngramMachine,
'attention-matrix': attentionMatrix,
'embedding-arithmetic': embeddingArithmetic,
'transformer-block': transformerBlock,
'attention-lookup': attentionLookup,
'token-strip': tokenStrip,
'loss-curve': lossCurve,
'embedding-projection': embeddingProjection,
'kv-cache': kvCache
};
function mount(root = document) {
root.querySelectorAll('[data-figure]').forEach(host => {
if (host.dataset.figureMounted) return;
const fn = FIGURES[host.dataset.figure];
if (!fn) return;
try {
fn(host);
host.dataset.figureMounted = '1';
} catch (err) {
console.warn(`figure "${host.dataset.figure}" failed to render:`, err);
}
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => mount());
else mount();
window.AIFS_FIGURES = FIGURES;
window.AIFS_mountFigures = mount;
})();