finephrase / app /src /content /embeds /banner-library.html
joelniklaus's picture
joelniklaus HF Staff
add another suggestion for a banner visualization
75ef4f9
<div class="babel-library" style="width:100%;margin:0;aspect-ratio:2.5/1;min-height:300px;position:relative;overflow:hidden;cursor:grab;"></div>
<script>
(() => {
const bootstrap = () => {
const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
const container = (mount && mount.classList.contains('babel-library') && mount) ||
Array.from(document.querySelectorAll('.babel-library')).find(el => el.dataset.mounted !== 'true');
if (!container || container.dataset.mounted === 'true') return;
container.dataset.mounted = 'true';
const canvas = document.createElement('canvas');
canvas.style.display = 'block';
canvas.style.width = '100%';
canvas.style.height = '100%';
container.appendChild(canvas);
let ctx = canvas.getContext('2d');
// --- Constants ---
const TOKENS_PER_BOOK = 125_000;
const BOOKS_PER_SHELF = 250;
const SHELVES_PER_WALL = 50;
const WALLS_PER_CORRIDOR = 20;
const TOTAL_TOKENS = 1_056_095_620_280;
const TOTAL_BOOKS = Math.round(TOTAL_TOKENS / TOKENS_PER_BOOK);
// Prompt labels and canonical mapping
const PROMPT_LABELS = {
article: 'Article', commentary: 'Commentary', discussion: 'Discussion',
explanation: 'Explanation', faq: 'FAQ', math: 'Math', narrative: 'Narrative',
table: 'Table', tutorial: 'Tutorial', distill: 'Distill',
diverse_qa_pairs: 'Diverse QA', extract_knowledge: 'Extract Knowledge',
knowledge_list: 'Knowledge List', wikipedia_style_rephrasing: 'Wikipedia Style',
guided_rewrite_original: 'Guided Rewrite', guided_rewrite_improved: 'Guided Rewrite+',
continue: 'Continue', summarize: 'Summarize'
};
function toCanonical(promptPath) {
let base = promptPath.split('/').pop().replace('.md', '');
// Strip model-specific suffixes like -1b-hq, -falcon3-1b-hq, -smollm2-1.7b-hq, etc.
return base.replace(/-([\w.]+-)?\d+(\.\d+)?[bm]-\w+$/, '');
}
// --- State ---
let W = 800, H = 320, dpr = 1;
let zoom = 0.0, targetZoom = 1.0;
let autoPlaying = true, autoStartTime = 0, autoResumeZoom = 0;
const AUTO_DURATION = 60000;
let scrollResumeTimer = null;
let promptDistribution = null; // [{name, label, tokens, fraction, color}]
let shelfTextures = []; // offscreen canvases for shelf strips
let isDark = document.documentElement.getAttribute('data-theme') === 'dark';
// Seeded RNG for deterministic book colors
function mulberry32(a) {
return () => { a |= 0; a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296; };
}
// Tooltip
const tip = document.createElement('div');
Object.assign(tip.style, {
position: 'absolute', pointerEvents: 'none', padding: '8px 12px', borderRadius: '8px',
fontSize: '12px', lineHeight: '1.4', border: '1px solid var(--border-color)',
background: 'var(--surface-bg)', color: 'var(--text-color)',
boxShadow: '0 4px 24px rgba(0,0,0,.18)', opacity: '0', transition: 'opacity .12s ease',
whiteSpace: 'nowrap', zIndex: '10', fontFamily: "'Inter', system-ui, sans-serif"
});
container.appendChild(tip);
// --- Colors ---
function getColors() {
isDark = document.documentElement.getAttribute('data-theme') === 'dark';
// Curated palette that works in both light and dark mode
return {
bg: isDark ? '#1a1a2e' : '#faf9f6',
shelfWood: isDark ? '#3d2b1f' : '#c8a882',
shelfEdge: isDark ? '#2a1d14' : '#a08060',
wallBg: isDark ? '#141424' : '#f0ece4',
text: isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.85)',
textMuted: isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)',
shadow: isDark ? 'rgba(0,0,0,0.6)' : 'rgba(0,0,0,0.15)',
vignette: isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.06)',
};
}
let colors = getColors();
const observer = new MutationObserver(() => {
colors = getColors();
rebuildShelfTextures();
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
// Prompt colors: use ColorPalettes if available, otherwise fall back
function getPromptColors(n) {
if (window.ColorPalettes) return window.ColorPalettes.getColors('categorical', n);
// Tableau-inspired fallback
return ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc949',
'#af7aa1','#ff9da7','#9c755f','#bab0ab','#5fa2ce','#fc7d0b',
'#a3cce9','#ffc428','#c85200','#8cd17d'].slice(0, n);
}
// --- Data loading ---
const JSON_PATHS = ['/data/rephrasing_metadata.json', './assets/data/rephrasing_metadata.json',
'../assets/data/rephrasing_metadata.json'];
async function fetchFirst(paths) {
for (const p of paths) {
try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return r.json(); } catch(_) {}
}
throw new Error('Data not found');
}
function buildDistribution(raw) {
const byPrompt = {};
for (const entry of raw) {
const key = toCanonical(entry.prompt);
byPrompt[key] = (byPrompt[key] || 0) + entry.output_tokens;
}
const totalTokens = Object.values(byPrompt).reduce((a, b) => a + b, 0);
const keys = Object.keys(byPrompt).sort((a, b) => byPrompt[b] - byPrompt[a]);
const palette = getPromptColors(keys.length);
return keys.map((key, i) => ({
name: key,
label: PROMPT_LABELS[key] || key,
tokens: byPrompt[key],
fraction: byPrompt[key] / totalTokens,
color: palette[i]
}));
}
// Weighted random prompt selection using precomputed CDF
let cdf = [];
function buildCdf() {
if (!promptDistribution) return;
cdf = [];
let acc = 0;
for (const p of promptDistribution) { acc += p.fraction; cdf.push(acc); }
}
function randomPrompt(rand) {
const r = rand();
for (let i = 0; i < cdf.length; i++) { if (r <= cdf[i]) return promptDistribution[i]; }
return promptDistribution[promptDistribution.length - 1];
}
// --- Shelf texture generation ---
// Each shelf texture is a thin offscreen canvas filled with book spines
function makeShelfTexture(shelfWidth, shelfHeight, seed) {
const oc = document.createElement('canvas');
oc.width = shelfWidth; oc.height = shelfHeight;
const octx = oc.getContext('2d');
const localRng = mulberry32(seed);
let x = 0;
while (x < shelfWidth) {
const p = randomPrompt(localRng);
const spineW = 2 + localRng() * 4;
octx.fillStyle = p.color;
octx.fillRect(x, 0, spineW, shelfHeight);
// Subtle spine highlight
octx.fillStyle = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.25)';
octx.fillRect(x, 0, 1, shelfHeight);
x += spineW + 0.5;
}
return oc;
}
function rebuildShelfTextures() {
if (!promptDistribution) return;
shelfTextures = [];
for (let i = 0; i < 60; i++) {
shelfTextures.push(makeShelfTexture(400, 20, 1000 + i));
}
}
// --- Zoom mapping ---
// Each level has a core range and overlapping fade zones for crossfade transitions.
// Format: [start, end, fadeWidth]
const LEVEL_BOUNDS = [
{ name: 'book', start: 0.00, end: 0.30, fade: 0.06 },
{ name: 'shelf', start: 0.24, end: 0.52, fade: 0.06 },
{ name: 'wall', start: 0.46, end: 0.72, fade: 0.06 },
{ name: 'corridor', start: 0.66, end: 0.90, fade: 0.06 },
{ name: 'library', start: 0.84, end: 1.00, fade: 0.06 },
];
function getLevelAlpha(z, bound) {
const fadeIn = Math.min(1, (z - bound.start) / bound.fade);
// Last level (library) doesn't fade out; first level (book) doesn't fade in
if (bound.end >= 1.0) return Math.max(0, Math.min(1, fadeIn));
const fadeOut = Math.min(1, (bound.end - z) / bound.fade);
if (bound.start <= 0.0) return Math.max(0, Math.min(1, fadeOut));
return Math.max(0, Math.min(1, fadeIn, fadeOut));
}
// Returns the primary level info (used for scale indicator, tooltips, etc.)
function getScaleInfo(z) {
if (z < 0.30) return { level: 'book', t: z / 0.30, booksVisible: Math.round(20 + (z / 0.30) * 480) };
if (z < 0.52) return { level: 'shelf', t: (z - 0.24) / 0.28, shelvesVisible: Math.round(1 + ((z - 0.24) / 0.28) * 49) };
if (z < 0.72) return { level: 'wall', t: (z - 0.46) / 0.26, wallsVisible: Math.round(2 + ((z - 0.46) / 0.26) * 18) };
if (z < 0.90) return { level: 'corridor', t: (z - 0.66) / 0.24, corridorsVisible: Math.round(2 + ((z - 0.66) / 0.24) * 14) };
return { level: 'library', t: (z - 0.84) / 0.16, corridorsVisible: 33 };
}
// Build per-level info for crossfade rendering
function getLevelInfoForBound(b, z) {
const t = Math.max(0, Math.min(1, (z - b.start) / (b.end - b.start)));
switch (b.name) {
case 'book': return { level: 'book', t, booksVisible: Math.round(20 + t * 480) };
case 'shelf': return { level: 'shelf', t, shelvesVisible: Math.round(1 + t * 49) };
case 'wall': return { level: 'wall', t, wallsVisible: Math.round(2 + t * 18) };
case 'corridor': return { level: 'corridor', t, corridorsVisible: Math.round(2 + t * 14) };
case 'library': return { level: 'library', t, corridorsVisible: 33 };
}
}
// --- Drawing functions ---
function drawBookLevel(info) {
const n = info.booksVisible;
const bookW = W / (n * 1.05);
const bookH = H * 0.7;
const startY = (H - bookH) / 2;
const shelfY = startY + bookH;
const localRng = mulberry32(7);
// Shelf board
ctx.fillStyle = colors.shelfWood;
ctx.fillRect(0, shelfY, W, H * 0.06);
ctx.fillStyle = colors.shelfEdge;
ctx.fillRect(0, shelfY, W, 2);
// Shadow under shelf
const grad = ctx.createLinearGradient(0, shelfY + H * 0.06, 0, shelfY + H * 0.06 + 10);
grad.addColorStop(0, colors.shadow);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.fillRect(0, shelfY + H * 0.06, W, 10);
for (let i = 0; i < n; i++) {
const p = randomPrompt(localRng);
const x = i * (W / n) + (W / n - bookW) / 2;
const heightVar = 0.85 + localRng() * 0.15;
const bh = bookH * heightVar;
const by = shelfY - bh;
const bw = bookW * (0.7 + localRng() * 0.3);
ctx.fillStyle = p.color;
ctx.fillRect(x, by, bw, bh);
if (bw > 3) {
// 3D edge highlight (left)
ctx.fillStyle = isDark ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.35)';
ctx.fillRect(x, by, Math.max(1, bw * 0.08), bh);
// 3D shadow (right)
ctx.fillStyle = isDark ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.15)';
ctx.fillRect(x + bw - Math.max(1, bw * 0.06), by, Math.max(1, bw * 0.06), bh);
// Top edge
ctx.fillStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.2)';
ctx.fillRect(x, by, bw, Math.max(1, bh * 0.02));
}
if (bw > 16) {
ctx.save();
ctx.translate(x + bw / 2, by + bh / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillStyle = isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.6)';
ctx.font = `600 ${Math.min(bw * 0.5, 11)}px 'Inter', system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(p.label, 0, 0);
ctx.restore();
}
}
}
function drawShelfLevel(info) {
const nShelves = info.shelvesVisible;
const shelfH = H / (nShelves + 1);
const bookAreaH = shelfH * 0.78;
const woodH = shelfH * 0.08;
const gapH = shelfH - bookAreaH - woodH;
for (let s = 0; s < nShelves; s++) {
const y = s * shelfH + gapH / 2;
// Draw books as texture strip
const texIdx = s % shelfTextures.length;
if (shelfTextures[texIdx]) {
ctx.drawImage(shelfTextures[texIdx], 0, 0, shelfTextures[texIdx].width, shelfTextures[texIdx].height,
0, y, W, bookAreaH);
}
// Shelf board
ctx.fillStyle = colors.shelfWood;
ctx.fillRect(0, y + bookAreaH, W, woodH);
ctx.fillStyle = colors.shelfEdge;
ctx.fillRect(0, y + bookAreaH, W, 1);
}
}
function drawWallLevel(info) {
const nWalls = info.wallsVisible;
const wallW = W / Math.min(nWalls, 6);
const wallH = H * 0.85;
const startY = (H - wallH) / 2;
const cols = Math.min(nWalls, 6);
const rows = Math.ceil(nWalls / cols);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const idx = r * cols + c;
if (idx >= nWalls) break;
const wx = c * wallW + 2;
const wy = startY + r * (wallH / rows);
const ww = wallW - 4;
const wh = wallH / rows - 4;
// Wall background
ctx.fillStyle = colors.wallBg;
ctx.fillRect(wx, wy, ww, wh);
// Mini shelves as horizontal stripes (show ~12 visible, implying many more)
const nStripes = 12;
const stripeH = wh / nStripes;
for (let s = 0; s < nStripes; s++) {
const texIdx = (idx * nStripes + s) % shelfTextures.length;
if (shelfTextures[texIdx]) {
ctx.drawImage(shelfTextures[texIdx], 0, 0, shelfTextures[texIdx].width, shelfTextures[texIdx].height,
wx, wy + s * stripeH, ww, stripeH * 0.85);
}
// Tiny shelf line
ctx.fillStyle = colors.shelfEdge;
ctx.fillRect(wx, wy + (s + 1) * stripeH - 1, ww, 1);
}
// Wall border
ctx.strokeStyle = colors.shelfEdge;
ctx.lineWidth = 1;
ctx.strokeRect(wx, wy, ww, wh);
}
}
}
function drawCorridorLevel(info) {
const nCorridors = info.corridorsVisible;
const cols = Math.min(nCorridors, 8);
const rows = Math.ceil(nCorridors / cols);
const cellW = W / cols;
const cellH = H / rows;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const idx = r * cols + c;
if (idx >= nCorridors) break;
const cx_ = c * cellW;
const cy = r * cellH;
// Background
ctx.fillStyle = colors.wallBg;
ctx.fillRect(cx_, cy, cellW, cellH);
// Perspective tunnel: nested rectangles with shelf textures on sides
const depth = 5;
const inset = Math.min(cellW, cellH) * 0.15;
for (let d = depth; d >= 0; d--) {
const t = d / depth;
const dx = cx_ + inset * t;
const dy = cy + inset * t * 0.5;
const dw = cellW - inset * t * 2;
const dh = cellH - inset * t;
// Left wall strip with shelf texture
const wallW = dw * 0.1;
const texIdx = (idx * depth + d) % shelfTextures.length;
if (shelfTextures[texIdx]) {
ctx.globalAlpha = 0.25 + (1 - t) * 0.5;
ctx.drawImage(shelfTextures[texIdx], 0, 0, shelfTextures[texIdx].width, shelfTextures[texIdx].height,
dx, dy, wallW, dh);
// Right wall
ctx.drawImage(shelfTextures[(texIdx + 7) % shelfTextures.length], 0, 0, shelfTextures[0].width, shelfTextures[0].height,
dx + dw - wallW, dy, wallW, dh);
}
ctx.globalAlpha = 1;
}
// Corridor border
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
ctx.lineWidth = 1;
ctx.strokeRect(cx_, cy, cellW, cellH);
}
}
}
function drawLibraryLevel(info) {
// Grand cathedral: one-point perspective with vanishing point
const vpX = W / 2, vpY = H * 0.38;
const localRng = mulberry32(2026);
// Floor gradient
const floorGrad = ctx.createLinearGradient(0, H * 0.5, 0, H);
floorGrad.addColorStop(0, isDark ? '#1a1520' : '#e8e0d4');
floorGrad.addColorStop(1, isDark ? '#0d0a12' : '#d4cab8');
ctx.fillStyle = floorGrad;
ctx.fillRect(0, H * 0.5, W, H * 0.5);
// Ceiling
const ceilGrad = ctx.createLinearGradient(0, 0, 0, H * 0.38);
ceilGrad.addColorStop(0, isDark ? '#0d0a18' : '#f5f0e8');
ceilGrad.addColorStop(1, isDark ? '#151020' : '#ece4d8');
ctx.fillStyle = ceilGrad;
ctx.fillRect(0, 0, W, H * 0.38);
// Rows of shelving walls receding toward vanishing point
const nRows = 12;
for (let i = nRows; i >= 0; i--) {
const t = i / nRows; // 0 = closest, 1 = farthest
const perspective = 1 - t * 0.92;
const rowW = W * perspective;
const rowH = H * 0.7 * perspective;
const rowX = vpX - rowW / 2;
const rowY = vpY - rowH * 0.4 + (1 - perspective) * H * 0.1;
// Left wall
const wallW = rowW * 0.08;
ctx.fillStyle = isDark ? `rgba(30,20,40,${0.3 + t * 0.5})` : `rgba(180,160,130,${0.2 + t * 0.4})`;
ctx.fillRect(rowX, rowY, wallW, rowH);
// Book colors on wall
const stripes = 12;
const sH = rowH / stripes;
for (let s = 0; s < stripes; s++) {
const p = randomPrompt(localRng);
ctx.fillStyle = p.color;
ctx.globalAlpha = 0.15 + perspective * 0.35;
ctx.fillRect(rowX + 1, rowY + s * sH, wallW - 2, sH * 0.8);
}
ctx.globalAlpha = 1;
// Right wall (mirror)
const rwX = rowX + rowW - wallW;
ctx.fillStyle = isDark ? `rgba(30,20,40,${0.3 + t * 0.5})` : `rgba(180,160,130,${0.2 + t * 0.4})`;
ctx.fillRect(rwX, rowY, wallW, rowH);
for (let s = 0; s < stripes; s++) {
const p = randomPrompt(localRng);
ctx.fillStyle = p.color;
ctx.globalAlpha = 0.15 + perspective * 0.35;
ctx.fillRect(rwX + 1, rowY + s * sH, wallW - 2, sH * 0.8);
}
ctx.globalAlpha = 1;
// Floor tile line
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)';
ctx.lineWidth = 1;
const floorY = rowY + rowH;
ctx.beginPath();
ctx.moveTo(rowX, floorY);
ctx.lineTo(rowX + rowW, floorY);
ctx.stroke();
}
// Vanishing point glow
const vpGrad = ctx.createRadialGradient(vpX, vpY, 0, vpX, vpY, H * 0.3);
vpGrad.addColorStop(0, isDark ? 'rgba(180,160,220,0.25)' : 'rgba(255,240,200,0.4)');
vpGrad.addColorStop(1, 'transparent');
ctx.fillStyle = vpGrad;
ctx.beginPath();
ctx.arc(vpX, vpY, H * 0.3, 0, Math.PI * 2);
ctx.fill();
}
const drawFns = { book: drawBookLevel, shelf: drawShelfLevel, wall: drawWallLevel, corridor: drawCorridorLevel, library: drawLibraryLevel };
// --- Scale indicator & annotations ---
function drawScaleIndicator(z) {
const info = getScaleInfo(z);
let line1 = '', line2 = '';
if (info.level === 'book') {
line1 = `~${info.booksVisible} books in view`;
line2 = `1 book = ${(TOKENS_PER_BOOK).toLocaleString()} tokens`;
} else if (info.level === 'shelf') {
line1 = `~${info.shelvesVisible} shelves`;
line2 = `1 shelf = ${BOOKS_PER_SHELF} books = ${(BOOKS_PER_SHELF * TOKENS_PER_BOOK / 1e6).toFixed(1)}M tokens`;
} else if (info.level === 'wall') {
line1 = `~${info.wallsVisible} walls`;
line2 = `1 wall = ${SHELVES_PER_WALL} shelves = ${(SHELVES_PER_WALL * BOOKS_PER_SHELF * TOKENS_PER_BOOK / 1e9).toFixed(2)}B tokens`;
} else if (info.level === 'corridor') {
line1 = `~${info.corridorsVisible} corridors`;
line2 = `1 corridor = ${WALLS_PER_CORRIDOR} walls = ${(WALLS_PER_CORRIDOR * SHELVES_PER_WALL * BOOKS_PER_SHELF * TOKENS_PER_BOOK / 1e9).toFixed(0)}B tokens`;
} else {
line1 = 'The Full Library';
line2 = `${TOTAL_BOOKS.toLocaleString()} books \u00B7 ${(TOTAL_TOKENS / 1e12).toFixed(2)}T tokens \u00B7 16 prompt formats`;
}
const fs1 = Math.max(10, Math.min(14, W * 0.014));
const fs2 = Math.max(9, Math.min(12, W * 0.012));
const pad = 10;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// Background pill
ctx.font = `600 ${fs1}px 'Inter', system-ui, sans-serif`;
const w1 = ctx.measureText(line1).width;
ctx.font = `400 ${fs2}px 'Inter', system-ui, sans-serif`;
const w2 = ctx.measureText(line2).width;
const boxW = Math.max(w1, w2) + pad * 2;
const boxH = fs1 + fs2 + 8 + pad * 2;
ctx.fillStyle = isDark ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.7)';
ctx.beginPath();
const bx = 8, by = 8, br = 6;
ctx.moveTo(bx + br, by);
ctx.lineTo(bx + boxW - br, by);
ctx.quadraticCurveTo(bx + boxW, by, bx + boxW, by + br);
ctx.lineTo(bx + boxW, by + boxH - br);
ctx.quadraticCurveTo(bx + boxW, by + boxH, bx + boxW - br, by + boxH);
ctx.lineTo(bx + br, by + boxH);
ctx.quadraticCurveTo(bx, by + boxH, bx, by + boxH - br);
ctx.lineTo(bx, by + br);
ctx.quadraticCurveTo(bx, by, bx + br, by);
ctx.fill();
ctx.font = `600 ${fs1}px 'Inter', system-ui, sans-serif`;
ctx.fillStyle = colors.text;
ctx.fillText(line1, bx + pad, by + pad);
ctx.font = `400 ${fs2}px 'Inter', system-ui, sans-serif`;
ctx.fillStyle = colors.textMuted;
ctx.fillText(line2, bx + pad, by + pad + fs1 + 6);
}
function drawFullZoomAnnotation(z) {
if (z < 0.8) return;
const alpha = Math.min(1, (z - 0.8) / 0.2);
ctx.globalAlpha = alpha;
const bigFS = Math.min(W * 0.06, H * 0.14, 56);
const subFS = Math.max(10, bigFS * 0.28);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Main number
ctx.font = `900 ${bigFS}px 'Inter', system-ui, sans-serif`;
ctx.fillStyle = colors.text;
ctx.fillText('1,056,095,620,280 tokens', W / 2, H * 0.5);
// Subtitle
ctx.font = `500 ${subFS}px 'Inter', system-ui, sans-serif`;
ctx.fillStyle = colors.textMuted;
ctx.fillText(`${TOTAL_BOOKS.toLocaleString()} books across 16 prompt formats`, W / 2, H * 0.5 + bigFS * 0.75);
ctx.globalAlpha = 1;
}
// Scroll hint at bottom
function drawScrollHint(z) {
if (z > 0.05 || !autoPlaying) return;
const alpha = 1 - z / 0.05;
ctx.globalAlpha = alpha * 0.4;
const fs = Math.max(10, Math.min(12, W * 0.012));
ctx.font = `400 ${fs}px 'Inter', system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillStyle = colors.text;
ctx.fillText('scroll to zoom out', W / 2, H - 8);
ctx.globalAlpha = 1;
}
// Offscreen canvas for crossfade compositing
const offCanvas = document.createElement('canvas');
const offCtx = offCanvas.getContext('2d');
// --- Main render ---
function resize() {
dpr = window.devicePixelRatio || 1;
W = container.clientWidth || 800;
H = container.clientHeight || 320;
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
offCanvas.width = canvas.width;
offCanvas.height = canvas.height;
}
function render(timestamp) {
if (!promptDistribution) { requestAnimationFrame(render); return; }
// Auto-play animation (supports resuming from arbitrary zoom via autoResumeZoom)
if (autoPlaying) {
if (!autoStartTime) autoStartTime = timestamp;
const elapsed = timestamp - autoStartTime;
const remaining = 1.0 - autoResumeZoom;
const duration = AUTO_DURATION * remaining;
let t = duration > 0 ? Math.min(1, elapsed / duration) : 1;
t = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
zoom = autoResumeZoom + t * remaining;
if (elapsed >= duration) {
autoPlaying = false;
zoom = 1.0;
}
} else {
// Smooth interpolation toward target
const diff = targetZoom - zoom;
if (Math.abs(diff) > 0.0005) {
zoom += diff * 0.02;
} else {
zoom = targetZoom;
}
}
zoom = Math.max(0, Math.min(1, zoom));
// Clear
ctx.fillStyle = colors.bg;
ctx.fillRect(0, 0, W, H);
// Draw all active levels with crossfade blending
const info = getScaleInfo(zoom);
for (const b of LEVEL_BOUNDS) {
const alpha = getLevelAlpha(zoom, b);
if (alpha <= 0) continue;
if (alpha >= 0.999) {
drawFns[b.name](getLevelInfoForBound(b, zoom));
} else {
// Render to offscreen buffer, then composite with crossfade alpha
offCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
offCtx.clearRect(0, 0, W, H);
const savedCtx = ctx;
ctx = offCtx;
drawFns[b.name](getLevelInfoForBound(b, zoom));
ctx = savedCtx;
ctx.globalAlpha = alpha;
ctx.drawImage(offCanvas, 0, 0);
ctx.globalAlpha = 1;
}
}
// Vignette overlay
const vigGrad = ctx.createRadialGradient(W/2, H/2, Math.min(W,H)*0.3, W/2, H/2, Math.max(W,H)*0.7);
vigGrad.addColorStop(0, 'transparent');
vigGrad.addColorStop(1, colors.vignette);
ctx.fillStyle = vigGrad;
ctx.fillRect(0, 0, W, H);
drawScaleIndicator(zoom);
drawFullZoomAnnotation(zoom);
drawScrollHint(zoom);
requestAnimationFrame(render);
}
// --- Interaction ---
function pauseAutoPlay() {
if (autoPlaying) {
autoPlaying = false;
targetZoom = zoom;
}
clearTimeout(scrollResumeTimer);
scrollResumeTimer = setTimeout(() => {
if (!autoPlaying && zoom < 0.99) {
autoPlaying = true;
autoStartTime = 0;
autoResumeZoom = zoom;
}
}, 10);
}
container.addEventListener('wheel', (e) => {
e.preventDefault();
pauseAutoPlay();
const raw = e.deltaMode === 1 ? e.deltaY * 20 : e.deltaY;
const delta = raw * 0.0002;
targetZoom = Math.max(0, Math.min(1, targetZoom + delta));
}, { passive: false });
// Touch pinch zoom
let lastTouchDist = 0;
container.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
lastTouchDist = Math.sqrt(dx * dx + dy * dy);
pauseAutoPlay();
}
}, { passive: true });
container.addEventListener('touchmove', (e) => {
if (e.touches.length === 2) {
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const dist = Math.sqrt(dx * dx + dy * dy);
const delta = (lastTouchDist - dist) * 0.003;
targetZoom = Math.max(0, Math.min(1, targetZoom + delta));
lastTouchDist = dist;
}
}, { passive: false });
// Click to restart auto-play from the beginning
container.addEventListener('click', () => {
clearTimeout(scrollResumeTimer);
autoPlaying = true;
autoStartTime = 0;
autoResumeZoom = 0;
zoom = 0;
targetZoom = 1;
});
// Cursor style
container.addEventListener('mousedown', () => { container.style.cursor = 'grabbing'; });
container.addEventListener('mouseup', () => { container.style.cursor = 'grab'; });
// Hover tooltip for book level
container.addEventListener('mousemove', (e) => {
if (!promptDistribution) return;
const info = getScaleInfo(zoom);
if (info.level !== 'book') {
tip.style.opacity = '0';
return;
}
const rect = container.getBoundingClientRect();
const mx = e.clientX - rect.left;
const n = info.booksVisible;
const bookIdx = Math.floor(mx / (W / n));
const localRng = mulberry32(7);
let p = null;
for (let i = 0; i <= bookIdx && i < n; i++) {
p = randomPrompt(localRng);
localRng(); localRng(); // consume height + width variance to stay in sync with drawBookLevel
}
if (p) {
tip.innerHTML = `<strong>${p.label}</strong><br><span style="opacity:0.5">${(p.tokens / 1e9).toFixed(1)}B tokens total (${(p.fraction * 100).toFixed(1)}%)</span>`;
tip.style.opacity = '1';
let tx = e.clientX - rect.left + 14;
let ty = e.clientY - rect.top - 40;
if (tx + 180 > W) tx = e.clientX - rect.left - 190;
if (ty < 4) ty = 4;
tip.style.left = tx + 'px';
tip.style.top = ty + 'px';
}
});
container.addEventListener('mouseleave', () => { tip.style.opacity = '0'; });
// --- Init ---
resize();
if (window.ResizeObserver) new ResizeObserver(resize).observe(container);
else window.addEventListener('resize', resize);
fetchFirst(JSON_PATHS).then(raw => {
promptDistribution = buildDistribution(raw);
buildCdf();
rebuildShelfTextures();
requestAnimationFrame(render);
}).catch(err => {
container.innerHTML = `<pre style="color:red;padding:12px">${err.message}</pre>`;
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
} else { bootstrap(); }
})();
</script>