joelniklaus HF Staff commited on
Commit
75ef4f9
·
1 Parent(s): 1da160c

add another suggestion for a banner visualization

Browse files
app/src/content/embeds/banner-library.html ADDED
@@ -0,0 +1,756 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="babel-library" style="width:100%;margin:0;aspect-ratio:2.5/1;min-height:300px;position:relative;overflow:hidden;cursor:grab;"></div>
2
+ <script>
3
+ (() => {
4
+ const bootstrap = () => {
5
+ const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
6
+ const container = (mount && mount.classList.contains('babel-library') && mount) ||
7
+ Array.from(document.querySelectorAll('.babel-library')).find(el => el.dataset.mounted !== 'true');
8
+ if (!container || container.dataset.mounted === 'true') return;
9
+ container.dataset.mounted = 'true';
10
+
11
+ const canvas = document.createElement('canvas');
12
+ canvas.style.display = 'block';
13
+ canvas.style.width = '100%';
14
+ canvas.style.height = '100%';
15
+ container.appendChild(canvas);
16
+ let ctx = canvas.getContext('2d');
17
+
18
+ // --- Constants ---
19
+ const TOKENS_PER_BOOK = 125_000;
20
+ const BOOKS_PER_SHELF = 250;
21
+ const SHELVES_PER_WALL = 50;
22
+ const WALLS_PER_CORRIDOR = 20;
23
+ const TOTAL_TOKENS = 1_056_095_620_280;
24
+ const TOTAL_BOOKS = Math.round(TOTAL_TOKENS / TOKENS_PER_BOOK);
25
+
26
+ // Prompt labels and canonical mapping
27
+ const PROMPT_LABELS = {
28
+ article: 'Article', commentary: 'Commentary', discussion: 'Discussion',
29
+ explanation: 'Explanation', faq: 'FAQ', math: 'Math', narrative: 'Narrative',
30
+ table: 'Table', tutorial: 'Tutorial', distill: 'Distill',
31
+ diverse_qa_pairs: 'Diverse QA', extract_knowledge: 'Extract Knowledge',
32
+ knowledge_list: 'Knowledge List', wikipedia_style_rephrasing: 'Wikipedia Style',
33
+ guided_rewrite_original: 'Guided Rewrite', guided_rewrite_improved: 'Guided Rewrite+',
34
+ continue: 'Continue', summarize: 'Summarize'
35
+ };
36
+
37
+ function toCanonical(promptPath) {
38
+ let base = promptPath.split('/').pop().replace('.md', '');
39
+ // Strip model-specific suffixes like -1b-hq, -falcon3-1b-hq, -smollm2-1.7b-hq, etc.
40
+ return base.replace(/-([\w.]+-)?\d+(\.\d+)?[bm]-\w+$/, '');
41
+ }
42
+
43
+ // --- State ---
44
+ let W = 800, H = 320, dpr = 1;
45
+ let zoom = 0.0, targetZoom = 1.0;
46
+ let autoPlaying = true, autoStartTime = 0, autoResumeZoom = 0;
47
+ const AUTO_DURATION = 60000;
48
+ let scrollResumeTimer = null;
49
+ let promptDistribution = null; // [{name, label, tokens, fraction, color}]
50
+ let shelfTextures = []; // offscreen canvases for shelf strips
51
+ let isDark = document.documentElement.getAttribute('data-theme') === 'dark';
52
+
53
+ // Seeded RNG for deterministic book colors
54
+ function mulberry32(a) {
55
+ return () => { a |= 0; a = a + 0x6D2B79F5 | 0;
56
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
57
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
58
+ return ((t ^ t >>> 14) >>> 0) / 4294967296; };
59
+ }
60
+ // Tooltip
61
+ const tip = document.createElement('div');
62
+ Object.assign(tip.style, {
63
+ position: 'absolute', pointerEvents: 'none', padding: '8px 12px', borderRadius: '8px',
64
+ fontSize: '12px', lineHeight: '1.4', border: '1px solid var(--border-color)',
65
+ background: 'var(--surface-bg)', color: 'var(--text-color)',
66
+ boxShadow: '0 4px 24px rgba(0,0,0,.18)', opacity: '0', transition: 'opacity .12s ease',
67
+ whiteSpace: 'nowrap', zIndex: '10', fontFamily: "'Inter', system-ui, sans-serif"
68
+ });
69
+ container.appendChild(tip);
70
+
71
+ // --- Colors ---
72
+ function getColors() {
73
+ isDark = document.documentElement.getAttribute('data-theme') === 'dark';
74
+ // Curated palette that works in both light and dark mode
75
+ return {
76
+ bg: isDark ? '#1a1a2e' : '#faf9f6',
77
+ shelfWood: isDark ? '#3d2b1f' : '#c8a882',
78
+ shelfEdge: isDark ? '#2a1d14' : '#a08060',
79
+ wallBg: isDark ? '#141424' : '#f0ece4',
80
+ text: isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.85)',
81
+ textMuted: isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)',
82
+ shadow: isDark ? 'rgba(0,0,0,0.6)' : 'rgba(0,0,0,0.15)',
83
+ vignette: isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.06)',
84
+ };
85
+ }
86
+ let colors = getColors();
87
+ const observer = new MutationObserver(() => {
88
+ colors = getColors();
89
+ rebuildShelfTextures();
90
+ });
91
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
92
+
93
+ // Prompt colors: use ColorPalettes if available, otherwise fall back
94
+ function getPromptColors(n) {
95
+ if (window.ColorPalettes) return window.ColorPalettes.getColors('categorical', n);
96
+ // Tableau-inspired fallback
97
+ return ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc949',
98
+ '#af7aa1','#ff9da7','#9c755f','#bab0ab','#5fa2ce','#fc7d0b',
99
+ '#a3cce9','#ffc428','#c85200','#8cd17d'].slice(0, n);
100
+ }
101
+
102
+ // --- Data loading ---
103
+ const JSON_PATHS = ['/data/rephrasing_metadata.json', './assets/data/rephrasing_metadata.json',
104
+ '../assets/data/rephrasing_metadata.json'];
105
+ async function fetchFirst(paths) {
106
+ for (const p of paths) {
107
+ try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return r.json(); } catch(_) {}
108
+ }
109
+ throw new Error('Data not found');
110
+ }
111
+
112
+ function buildDistribution(raw) {
113
+ const byPrompt = {};
114
+ for (const entry of raw) {
115
+ const key = toCanonical(entry.prompt);
116
+ byPrompt[key] = (byPrompt[key] || 0) + entry.output_tokens;
117
+ }
118
+ const totalTokens = Object.values(byPrompt).reduce((a, b) => a + b, 0);
119
+ const keys = Object.keys(byPrompt).sort((a, b) => byPrompt[b] - byPrompt[a]);
120
+ const palette = getPromptColors(keys.length);
121
+ return keys.map((key, i) => ({
122
+ name: key,
123
+ label: PROMPT_LABELS[key] || key,
124
+ tokens: byPrompt[key],
125
+ fraction: byPrompt[key] / totalTokens,
126
+ color: palette[i]
127
+ }));
128
+ }
129
+
130
+ // Weighted random prompt selection using precomputed CDF
131
+ let cdf = [];
132
+ function buildCdf() {
133
+ if (!promptDistribution) return;
134
+ cdf = [];
135
+ let acc = 0;
136
+ for (const p of promptDistribution) { acc += p.fraction; cdf.push(acc); }
137
+ }
138
+ function randomPrompt(rand) {
139
+ const r = rand();
140
+ for (let i = 0; i < cdf.length; i++) { if (r <= cdf[i]) return promptDistribution[i]; }
141
+ return promptDistribution[promptDistribution.length - 1];
142
+ }
143
+
144
+ // --- Shelf texture generation ---
145
+ // Each shelf texture is a thin offscreen canvas filled with book spines
146
+ function makeShelfTexture(shelfWidth, shelfHeight, seed) {
147
+ const oc = document.createElement('canvas');
148
+ oc.width = shelfWidth; oc.height = shelfHeight;
149
+ const octx = oc.getContext('2d');
150
+ const localRng = mulberry32(seed);
151
+ let x = 0;
152
+ while (x < shelfWidth) {
153
+ const p = randomPrompt(localRng);
154
+ const spineW = 2 + localRng() * 4;
155
+ octx.fillStyle = p.color;
156
+ octx.fillRect(x, 0, spineW, shelfHeight);
157
+ // Subtle spine highlight
158
+ octx.fillStyle = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.25)';
159
+ octx.fillRect(x, 0, 1, shelfHeight);
160
+ x += spineW + 0.5;
161
+ }
162
+ return oc;
163
+ }
164
+ function rebuildShelfTextures() {
165
+ if (!promptDistribution) return;
166
+ shelfTextures = [];
167
+ for (let i = 0; i < 60; i++) {
168
+ shelfTextures.push(makeShelfTexture(400, 20, 1000 + i));
169
+ }
170
+ }
171
+
172
+ // --- Zoom mapping ---
173
+ // Each level has a core range and overlapping fade zones for crossfade transitions.
174
+ // Format: [start, end, fadeWidth]
175
+ const LEVEL_BOUNDS = [
176
+ { name: 'book', start: 0.00, end: 0.30, fade: 0.06 },
177
+ { name: 'shelf', start: 0.24, end: 0.52, fade: 0.06 },
178
+ { name: 'wall', start: 0.46, end: 0.72, fade: 0.06 },
179
+ { name: 'corridor', start: 0.66, end: 0.90, fade: 0.06 },
180
+ { name: 'library', start: 0.84, end: 1.00, fade: 0.06 },
181
+ ];
182
+
183
+ function getLevelAlpha(z, bound) {
184
+ const fadeIn = Math.min(1, (z - bound.start) / bound.fade);
185
+ // Last level (library) doesn't fade out; first level (book) doesn't fade in
186
+ if (bound.end >= 1.0) return Math.max(0, Math.min(1, fadeIn));
187
+ const fadeOut = Math.min(1, (bound.end - z) / bound.fade);
188
+ if (bound.start <= 0.0) return Math.max(0, Math.min(1, fadeOut));
189
+ return Math.max(0, Math.min(1, fadeIn, fadeOut));
190
+ }
191
+
192
+ // Returns the primary level info (used for scale indicator, tooltips, etc.)
193
+ function getScaleInfo(z) {
194
+ if (z < 0.30) return { level: 'book', t: z / 0.30, booksVisible: Math.round(20 + (z / 0.30) * 480) };
195
+ if (z < 0.52) return { level: 'shelf', t: (z - 0.24) / 0.28, shelvesVisible: Math.round(1 + ((z - 0.24) / 0.28) * 49) };
196
+ if (z < 0.72) return { level: 'wall', t: (z - 0.46) / 0.26, wallsVisible: Math.round(2 + ((z - 0.46) / 0.26) * 18) };
197
+ if (z < 0.90) return { level: 'corridor', t: (z - 0.66) / 0.24, corridorsVisible: Math.round(2 + ((z - 0.66) / 0.24) * 14) };
198
+ return { level: 'library', t: (z - 0.84) / 0.16, corridorsVisible: 33 };
199
+ }
200
+
201
+ // Build per-level info for crossfade rendering
202
+ function getLevelInfoForBound(b, z) {
203
+ const t = Math.max(0, Math.min(1, (z - b.start) / (b.end - b.start)));
204
+ switch (b.name) {
205
+ case 'book': return { level: 'book', t, booksVisible: Math.round(20 + t * 480) };
206
+ case 'shelf': return { level: 'shelf', t, shelvesVisible: Math.round(1 + t * 49) };
207
+ case 'wall': return { level: 'wall', t, wallsVisible: Math.round(2 + t * 18) };
208
+ case 'corridor': return { level: 'corridor', t, corridorsVisible: Math.round(2 + t * 14) };
209
+ case 'library': return { level: 'library', t, corridorsVisible: 33 };
210
+ }
211
+ }
212
+
213
+ // --- Drawing functions ---
214
+
215
+ function drawBookLevel(info) {
216
+ const n = info.booksVisible;
217
+ const bookW = W / (n * 1.05);
218
+ const bookH = H * 0.7;
219
+ const startY = (H - bookH) / 2;
220
+ const shelfY = startY + bookH;
221
+ const localRng = mulberry32(7);
222
+
223
+ // Shelf board
224
+ ctx.fillStyle = colors.shelfWood;
225
+ ctx.fillRect(0, shelfY, W, H * 0.06);
226
+ ctx.fillStyle = colors.shelfEdge;
227
+ ctx.fillRect(0, shelfY, W, 2);
228
+ // Shadow under shelf
229
+ const grad = ctx.createLinearGradient(0, shelfY + H * 0.06, 0, shelfY + H * 0.06 + 10);
230
+ grad.addColorStop(0, colors.shadow);
231
+ grad.addColorStop(1, 'transparent');
232
+ ctx.fillStyle = grad;
233
+ ctx.fillRect(0, shelfY + H * 0.06, W, 10);
234
+
235
+ for (let i = 0; i < n; i++) {
236
+ const p = randomPrompt(localRng);
237
+ const x = i * (W / n) + (W / n - bookW) / 2;
238
+ const heightVar = 0.85 + localRng() * 0.15;
239
+ const bh = bookH * heightVar;
240
+ const by = shelfY - bh;
241
+ const bw = bookW * (0.7 + localRng() * 0.3);
242
+
243
+ ctx.fillStyle = p.color;
244
+ ctx.fillRect(x, by, bw, bh);
245
+
246
+ if (bw > 3) {
247
+ // 3D edge highlight (left)
248
+ ctx.fillStyle = isDark ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.35)';
249
+ ctx.fillRect(x, by, Math.max(1, bw * 0.08), bh);
250
+ // 3D shadow (right)
251
+ ctx.fillStyle = isDark ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.15)';
252
+ ctx.fillRect(x + bw - Math.max(1, bw * 0.06), by, Math.max(1, bw * 0.06), bh);
253
+ // Top edge
254
+ ctx.fillStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.2)';
255
+ ctx.fillRect(x, by, bw, Math.max(1, bh * 0.02));
256
+ }
257
+
258
+ if (bw > 16) {
259
+ ctx.save();
260
+ ctx.translate(x + bw / 2, by + bh / 2);
261
+ ctx.rotate(-Math.PI / 2);
262
+ ctx.fillStyle = isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.6)';
263
+ ctx.font = `600 ${Math.min(bw * 0.5, 11)}px 'Inter', system-ui, sans-serif`;
264
+ ctx.textAlign = 'center';
265
+ ctx.textBaseline = 'middle';
266
+ ctx.fillText(p.label, 0, 0);
267
+ ctx.restore();
268
+ }
269
+ }
270
+ }
271
+
272
+ function drawShelfLevel(info) {
273
+ const nShelves = info.shelvesVisible;
274
+ const shelfH = H / (nShelves + 1);
275
+ const bookAreaH = shelfH * 0.78;
276
+ const woodH = shelfH * 0.08;
277
+ const gapH = shelfH - bookAreaH - woodH;
278
+
279
+ for (let s = 0; s < nShelves; s++) {
280
+ const y = s * shelfH + gapH / 2;
281
+ // Draw books as texture strip
282
+ const texIdx = s % shelfTextures.length;
283
+ if (shelfTextures[texIdx]) {
284
+ ctx.drawImage(shelfTextures[texIdx], 0, 0, shelfTextures[texIdx].width, shelfTextures[texIdx].height,
285
+ 0, y, W, bookAreaH);
286
+ }
287
+ // Shelf board
288
+ ctx.fillStyle = colors.shelfWood;
289
+ ctx.fillRect(0, y + bookAreaH, W, woodH);
290
+ ctx.fillStyle = colors.shelfEdge;
291
+ ctx.fillRect(0, y + bookAreaH, W, 1);
292
+ }
293
+ }
294
+
295
+ function drawWallLevel(info) {
296
+ const nWalls = info.wallsVisible;
297
+ const wallW = W / Math.min(nWalls, 6);
298
+ const wallH = H * 0.85;
299
+ const startY = (H - wallH) / 2;
300
+ const cols = Math.min(nWalls, 6);
301
+ const rows = Math.ceil(nWalls / cols);
302
+
303
+ for (let r = 0; r < rows; r++) {
304
+ for (let c = 0; c < cols; c++) {
305
+ const idx = r * cols + c;
306
+ if (idx >= nWalls) break;
307
+ const wx = c * wallW + 2;
308
+ const wy = startY + r * (wallH / rows);
309
+ const ww = wallW - 4;
310
+ const wh = wallH / rows - 4;
311
+
312
+ // Wall background
313
+ ctx.fillStyle = colors.wallBg;
314
+ ctx.fillRect(wx, wy, ww, wh);
315
+
316
+ // Mini shelves as horizontal stripes (show ~12 visible, implying many more)
317
+ const nStripes = 12;
318
+ const stripeH = wh / nStripes;
319
+ for (let s = 0; s < nStripes; s++) {
320
+ const texIdx = (idx * nStripes + s) % shelfTextures.length;
321
+ if (shelfTextures[texIdx]) {
322
+ ctx.drawImage(shelfTextures[texIdx], 0, 0, shelfTextures[texIdx].width, shelfTextures[texIdx].height,
323
+ wx, wy + s * stripeH, ww, stripeH * 0.85);
324
+ }
325
+ // Tiny shelf line
326
+ ctx.fillStyle = colors.shelfEdge;
327
+ ctx.fillRect(wx, wy + (s + 1) * stripeH - 1, ww, 1);
328
+ }
329
+
330
+ // Wall border
331
+ ctx.strokeStyle = colors.shelfEdge;
332
+ ctx.lineWidth = 1;
333
+ ctx.strokeRect(wx, wy, ww, wh);
334
+ }
335
+ }
336
+ }
337
+
338
+ function drawCorridorLevel(info) {
339
+ const nCorridors = info.corridorsVisible;
340
+ const cols = Math.min(nCorridors, 8);
341
+ const rows = Math.ceil(nCorridors / cols);
342
+ const cellW = W / cols;
343
+ const cellH = H / rows;
344
+
345
+ for (let r = 0; r < rows; r++) {
346
+ for (let c = 0; c < cols; c++) {
347
+ const idx = r * cols + c;
348
+ if (idx >= nCorridors) break;
349
+ const cx_ = c * cellW;
350
+ const cy = r * cellH;
351
+
352
+ // Background
353
+ ctx.fillStyle = colors.wallBg;
354
+ ctx.fillRect(cx_, cy, cellW, cellH);
355
+
356
+ // Perspective tunnel: nested rectangles with shelf textures on sides
357
+ const depth = 5;
358
+ const inset = Math.min(cellW, cellH) * 0.15;
359
+ for (let d = depth; d >= 0; d--) {
360
+ const t = d / depth;
361
+ const dx = cx_ + inset * t;
362
+ const dy = cy + inset * t * 0.5;
363
+ const dw = cellW - inset * t * 2;
364
+ const dh = cellH - inset * t;
365
+
366
+ // Left wall strip with shelf texture
367
+ const wallW = dw * 0.1;
368
+ const texIdx = (idx * depth + d) % shelfTextures.length;
369
+ if (shelfTextures[texIdx]) {
370
+ ctx.globalAlpha = 0.25 + (1 - t) * 0.5;
371
+ ctx.drawImage(shelfTextures[texIdx], 0, 0, shelfTextures[texIdx].width, shelfTextures[texIdx].height,
372
+ dx, dy, wallW, dh);
373
+ // Right wall
374
+ ctx.drawImage(shelfTextures[(texIdx + 7) % shelfTextures.length], 0, 0, shelfTextures[0].width, shelfTextures[0].height,
375
+ dx + dw - wallW, dy, wallW, dh);
376
+ }
377
+ ctx.globalAlpha = 1;
378
+ }
379
+
380
+ // Corridor border
381
+ ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
382
+ ctx.lineWidth = 1;
383
+ ctx.strokeRect(cx_, cy, cellW, cellH);
384
+ }
385
+ }
386
+ }
387
+
388
+ function drawLibraryLevel(info) {
389
+ // Grand cathedral: one-point perspective with vanishing point
390
+ const vpX = W / 2, vpY = H * 0.38;
391
+ const localRng = mulberry32(2026);
392
+
393
+ // Floor gradient
394
+ const floorGrad = ctx.createLinearGradient(0, H * 0.5, 0, H);
395
+ floorGrad.addColorStop(0, isDark ? '#1a1520' : '#e8e0d4');
396
+ floorGrad.addColorStop(1, isDark ? '#0d0a12' : '#d4cab8');
397
+ ctx.fillStyle = floorGrad;
398
+ ctx.fillRect(0, H * 0.5, W, H * 0.5);
399
+
400
+ // Ceiling
401
+ const ceilGrad = ctx.createLinearGradient(0, 0, 0, H * 0.38);
402
+ ceilGrad.addColorStop(0, isDark ? '#0d0a18' : '#f5f0e8');
403
+ ceilGrad.addColorStop(1, isDark ? '#151020' : '#ece4d8');
404
+ ctx.fillStyle = ceilGrad;
405
+ ctx.fillRect(0, 0, W, H * 0.38);
406
+
407
+ // Rows of shelving walls receding toward vanishing point
408
+ const nRows = 12;
409
+ for (let i = nRows; i >= 0; i--) {
410
+ const t = i / nRows; // 0 = closest, 1 = farthest
411
+ const perspective = 1 - t * 0.92;
412
+
413
+ const rowW = W * perspective;
414
+ const rowH = H * 0.7 * perspective;
415
+ const rowX = vpX - rowW / 2;
416
+ const rowY = vpY - rowH * 0.4 + (1 - perspective) * H * 0.1;
417
+
418
+ // Left wall
419
+ const wallW = rowW * 0.08;
420
+ ctx.fillStyle = isDark ? `rgba(30,20,40,${0.3 + t * 0.5})` : `rgba(180,160,130,${0.2 + t * 0.4})`;
421
+ ctx.fillRect(rowX, rowY, wallW, rowH);
422
+ // Book colors on wall
423
+ const stripes = 12;
424
+ const sH = rowH / stripes;
425
+ for (let s = 0; s < stripes; s++) {
426
+ const p = randomPrompt(localRng);
427
+ ctx.fillStyle = p.color;
428
+ ctx.globalAlpha = 0.15 + perspective * 0.35;
429
+ ctx.fillRect(rowX + 1, rowY + s * sH, wallW - 2, sH * 0.8);
430
+ }
431
+ ctx.globalAlpha = 1;
432
+
433
+ // Right wall (mirror)
434
+ const rwX = rowX + rowW - wallW;
435
+ ctx.fillStyle = isDark ? `rgba(30,20,40,${0.3 + t * 0.5})` : `rgba(180,160,130,${0.2 + t * 0.4})`;
436
+ ctx.fillRect(rwX, rowY, wallW, rowH);
437
+ for (let s = 0; s < stripes; s++) {
438
+ const p = randomPrompt(localRng);
439
+ ctx.fillStyle = p.color;
440
+ ctx.globalAlpha = 0.15 + perspective * 0.35;
441
+ ctx.fillRect(rwX + 1, rowY + s * sH, wallW - 2, sH * 0.8);
442
+ }
443
+ ctx.globalAlpha = 1;
444
+
445
+ // Floor tile line
446
+ ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)';
447
+ ctx.lineWidth = 1;
448
+ const floorY = rowY + rowH;
449
+ ctx.beginPath();
450
+ ctx.moveTo(rowX, floorY);
451
+ ctx.lineTo(rowX + rowW, floorY);
452
+ ctx.stroke();
453
+ }
454
+
455
+ // Vanishing point glow
456
+ const vpGrad = ctx.createRadialGradient(vpX, vpY, 0, vpX, vpY, H * 0.3);
457
+ vpGrad.addColorStop(0, isDark ? 'rgba(180,160,220,0.25)' : 'rgba(255,240,200,0.4)');
458
+ vpGrad.addColorStop(1, 'transparent');
459
+ ctx.fillStyle = vpGrad;
460
+ ctx.beginPath();
461
+ ctx.arc(vpX, vpY, H * 0.3, 0, Math.PI * 2);
462
+ ctx.fill();
463
+ }
464
+
465
+ const drawFns = { book: drawBookLevel, shelf: drawShelfLevel, wall: drawWallLevel, corridor: drawCorridorLevel, library: drawLibraryLevel };
466
+
467
+ // --- Scale indicator & annotations ---
468
+ function drawScaleIndicator(z) {
469
+ const info = getScaleInfo(z);
470
+ let line1 = '', line2 = '';
471
+ if (info.level === 'book') {
472
+ line1 = `~${info.booksVisible} books in view`;
473
+ line2 = `1 book = ${(TOKENS_PER_BOOK).toLocaleString()} tokens`;
474
+ } else if (info.level === 'shelf') {
475
+ line1 = `~${info.shelvesVisible} shelves`;
476
+ line2 = `1 shelf = ${BOOKS_PER_SHELF} books = ${(BOOKS_PER_SHELF * TOKENS_PER_BOOK / 1e6).toFixed(1)}M tokens`;
477
+ } else if (info.level === 'wall') {
478
+ line1 = `~${info.wallsVisible} walls`;
479
+ line2 = `1 wall = ${SHELVES_PER_WALL} shelves = ${(SHELVES_PER_WALL * BOOKS_PER_SHELF * TOKENS_PER_BOOK / 1e9).toFixed(2)}B tokens`;
480
+ } else if (info.level === 'corridor') {
481
+ line1 = `~${info.corridorsVisible} corridors`;
482
+ line2 = `1 corridor = ${WALLS_PER_CORRIDOR} walls = ${(WALLS_PER_CORRIDOR * SHELVES_PER_WALL * BOOKS_PER_SHELF * TOKENS_PER_BOOK / 1e9).toFixed(0)}B tokens`;
483
+ } else {
484
+ line1 = 'The Full Library';
485
+ line2 = `${TOTAL_BOOKS.toLocaleString()} books \u00B7 ${(TOTAL_TOKENS / 1e12).toFixed(2)}T tokens \u00B7 16 prompt formats`;
486
+ }
487
+
488
+ const fs1 = Math.max(10, Math.min(14, W * 0.014));
489
+ const fs2 = Math.max(9, Math.min(12, W * 0.012));
490
+ const pad = 10;
491
+
492
+ ctx.textAlign = 'left';
493
+ ctx.textBaseline = 'top';
494
+
495
+ // Background pill
496
+ ctx.font = `600 ${fs1}px 'Inter', system-ui, sans-serif`;
497
+ const w1 = ctx.measureText(line1).width;
498
+ ctx.font = `400 ${fs2}px 'Inter', system-ui, sans-serif`;
499
+ const w2 = ctx.measureText(line2).width;
500
+ const boxW = Math.max(w1, w2) + pad * 2;
501
+ const boxH = fs1 + fs2 + 8 + pad * 2;
502
+
503
+ ctx.fillStyle = isDark ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.7)';
504
+ ctx.beginPath();
505
+ const bx = 8, by = 8, br = 6;
506
+ ctx.moveTo(bx + br, by);
507
+ ctx.lineTo(bx + boxW - br, by);
508
+ ctx.quadraticCurveTo(bx + boxW, by, bx + boxW, by + br);
509
+ ctx.lineTo(bx + boxW, by + boxH - br);
510
+ ctx.quadraticCurveTo(bx + boxW, by + boxH, bx + boxW - br, by + boxH);
511
+ ctx.lineTo(bx + br, by + boxH);
512
+ ctx.quadraticCurveTo(bx, by + boxH, bx, by + boxH - br);
513
+ ctx.lineTo(bx, by + br);
514
+ ctx.quadraticCurveTo(bx, by, bx + br, by);
515
+ ctx.fill();
516
+
517
+ ctx.font = `600 ${fs1}px 'Inter', system-ui, sans-serif`;
518
+ ctx.fillStyle = colors.text;
519
+ ctx.fillText(line1, bx + pad, by + pad);
520
+
521
+ ctx.font = `400 ${fs2}px 'Inter', system-ui, sans-serif`;
522
+ ctx.fillStyle = colors.textMuted;
523
+ ctx.fillText(line2, bx + pad, by + pad + fs1 + 6);
524
+ }
525
+
526
+ function drawFullZoomAnnotation(z) {
527
+ if (z < 0.8) return;
528
+ const alpha = Math.min(1, (z - 0.8) / 0.2);
529
+ ctx.globalAlpha = alpha;
530
+
531
+ const bigFS = Math.min(W * 0.06, H * 0.14, 56);
532
+ const subFS = Math.max(10, bigFS * 0.28);
533
+
534
+ ctx.textAlign = 'center';
535
+ ctx.textBaseline = 'middle';
536
+
537
+ // Main number
538
+ ctx.font = `900 ${bigFS}px 'Inter', system-ui, sans-serif`;
539
+ ctx.fillStyle = colors.text;
540
+ ctx.fillText('1,056,095,620,280 tokens', W / 2, H * 0.5);
541
+
542
+ // Subtitle
543
+ ctx.font = `500 ${subFS}px 'Inter', system-ui, sans-serif`;
544
+ ctx.fillStyle = colors.textMuted;
545
+ ctx.fillText(`${TOTAL_BOOKS.toLocaleString()} books across 16 prompt formats`, W / 2, H * 0.5 + bigFS * 0.75);
546
+
547
+ ctx.globalAlpha = 1;
548
+ }
549
+
550
+ // Scroll hint at bottom
551
+ function drawScrollHint(z) {
552
+ if (z > 0.05 || !autoPlaying) return;
553
+ const alpha = 1 - z / 0.05;
554
+ ctx.globalAlpha = alpha * 0.4;
555
+ const fs = Math.max(10, Math.min(12, W * 0.012));
556
+ ctx.font = `400 ${fs}px 'Inter', system-ui, sans-serif`;
557
+ ctx.textAlign = 'center';
558
+ ctx.textBaseline = 'bottom';
559
+ ctx.fillStyle = colors.text;
560
+ ctx.fillText('scroll to zoom out', W / 2, H - 8);
561
+ ctx.globalAlpha = 1;
562
+ }
563
+
564
+ // Offscreen canvas for crossfade compositing
565
+ const offCanvas = document.createElement('canvas');
566
+ const offCtx = offCanvas.getContext('2d');
567
+
568
+ // --- Main render ---
569
+ function resize() {
570
+ dpr = window.devicePixelRatio || 1;
571
+ W = container.clientWidth || 800;
572
+ H = container.clientHeight || 320;
573
+ canvas.width = W * dpr;
574
+ canvas.height = H * dpr;
575
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
576
+ offCanvas.width = canvas.width;
577
+ offCanvas.height = canvas.height;
578
+ }
579
+
580
+ function render(timestamp) {
581
+ if (!promptDistribution) { requestAnimationFrame(render); return; }
582
+
583
+ // Auto-play animation (supports resuming from arbitrary zoom via autoResumeZoom)
584
+ if (autoPlaying) {
585
+ if (!autoStartTime) autoStartTime = timestamp;
586
+ const elapsed = timestamp - autoStartTime;
587
+ const remaining = 1.0 - autoResumeZoom;
588
+ const duration = AUTO_DURATION * remaining;
589
+ let t = duration > 0 ? Math.min(1, elapsed / duration) : 1;
590
+ t = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
591
+ zoom = autoResumeZoom + t * remaining;
592
+ if (elapsed >= duration) {
593
+ autoPlaying = false;
594
+ zoom = 1.0;
595
+ }
596
+ } else {
597
+ // Smooth interpolation toward target
598
+ const diff = targetZoom - zoom;
599
+ if (Math.abs(diff) > 0.0005) {
600
+ zoom += diff * 0.02;
601
+ } else {
602
+ zoom = targetZoom;
603
+ }
604
+ }
605
+ zoom = Math.max(0, Math.min(1, zoom));
606
+
607
+ // Clear
608
+ ctx.fillStyle = colors.bg;
609
+ ctx.fillRect(0, 0, W, H);
610
+
611
+ // Draw all active levels with crossfade blending
612
+ const info = getScaleInfo(zoom);
613
+ for (const b of LEVEL_BOUNDS) {
614
+ const alpha = getLevelAlpha(zoom, b);
615
+ if (alpha <= 0) continue;
616
+ if (alpha >= 0.999) {
617
+ drawFns[b.name](getLevelInfoForBound(b, zoom));
618
+ } else {
619
+ // Render to offscreen buffer, then composite with crossfade alpha
620
+ offCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
621
+ offCtx.clearRect(0, 0, W, H);
622
+ const savedCtx = ctx;
623
+ ctx = offCtx;
624
+ drawFns[b.name](getLevelInfoForBound(b, zoom));
625
+ ctx = savedCtx;
626
+ ctx.globalAlpha = alpha;
627
+ ctx.drawImage(offCanvas, 0, 0);
628
+ ctx.globalAlpha = 1;
629
+ }
630
+ }
631
+
632
+ // Vignette overlay
633
+ const vigGrad = ctx.createRadialGradient(W/2, H/2, Math.min(W,H)*0.3, W/2, H/2, Math.max(W,H)*0.7);
634
+ vigGrad.addColorStop(0, 'transparent');
635
+ vigGrad.addColorStop(1, colors.vignette);
636
+ ctx.fillStyle = vigGrad;
637
+ ctx.fillRect(0, 0, W, H);
638
+
639
+ drawScaleIndicator(zoom);
640
+ drawFullZoomAnnotation(zoom);
641
+ drawScrollHint(zoom);
642
+
643
+ requestAnimationFrame(render);
644
+ }
645
+
646
+ // --- Interaction ---
647
+ function pauseAutoPlay() {
648
+ if (autoPlaying) {
649
+ autoPlaying = false;
650
+ targetZoom = zoom;
651
+ }
652
+ clearTimeout(scrollResumeTimer);
653
+ scrollResumeTimer = setTimeout(() => {
654
+ if (!autoPlaying && zoom < 0.99) {
655
+ autoPlaying = true;
656
+ autoStartTime = 0;
657
+ autoResumeZoom = zoom;
658
+ }
659
+ }, 10);
660
+ }
661
+
662
+ container.addEventListener('wheel', (e) => {
663
+ e.preventDefault();
664
+ pauseAutoPlay();
665
+ const raw = e.deltaMode === 1 ? e.deltaY * 20 : e.deltaY;
666
+ const delta = raw * 0.0002;
667
+ targetZoom = Math.max(0, Math.min(1, targetZoom + delta));
668
+ }, { passive: false });
669
+
670
+ // Touch pinch zoom
671
+ let lastTouchDist = 0;
672
+ container.addEventListener('touchstart', (e) => {
673
+ if (e.touches.length === 2) {
674
+ const dx = e.touches[0].clientX - e.touches[1].clientX;
675
+ const dy = e.touches[0].clientY - e.touches[1].clientY;
676
+ lastTouchDist = Math.sqrt(dx * dx + dy * dy);
677
+ pauseAutoPlay();
678
+ }
679
+ }, { passive: true });
680
+ container.addEventListener('touchmove', (e) => {
681
+ if (e.touches.length === 2) {
682
+ e.preventDefault();
683
+ const dx = e.touches[0].clientX - e.touches[1].clientX;
684
+ const dy = e.touches[0].clientY - e.touches[1].clientY;
685
+ const dist = Math.sqrt(dx * dx + dy * dy);
686
+ const delta = (lastTouchDist - dist) * 0.003;
687
+ targetZoom = Math.max(0, Math.min(1, targetZoom + delta));
688
+ lastTouchDist = dist;
689
+ }
690
+ }, { passive: false });
691
+
692
+ // Click to restart auto-play from the beginning
693
+ container.addEventListener('click', () => {
694
+ clearTimeout(scrollResumeTimer);
695
+ autoPlaying = true;
696
+ autoStartTime = 0;
697
+ autoResumeZoom = 0;
698
+ zoom = 0;
699
+ targetZoom = 1;
700
+ });
701
+
702
+ // Cursor style
703
+ container.addEventListener('mousedown', () => { container.style.cursor = 'grabbing'; });
704
+ container.addEventListener('mouseup', () => { container.style.cursor = 'grab'; });
705
+
706
+ // Hover tooltip for book level
707
+ container.addEventListener('mousemove', (e) => {
708
+ if (!promptDistribution) return;
709
+ const info = getScaleInfo(zoom);
710
+ if (info.level !== 'book') {
711
+ tip.style.opacity = '0';
712
+ return;
713
+ }
714
+ const rect = container.getBoundingClientRect();
715
+ const mx = e.clientX - rect.left;
716
+ const n = info.booksVisible;
717
+ const bookIdx = Math.floor(mx / (W / n));
718
+ const localRng = mulberry32(7);
719
+ let p = null;
720
+ for (let i = 0; i <= bookIdx && i < n; i++) {
721
+ p = randomPrompt(localRng);
722
+ localRng(); localRng(); // consume height + width variance to stay in sync with drawBookLevel
723
+ }
724
+ if (p) {
725
+ 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>`;
726
+ tip.style.opacity = '1';
727
+ let tx = e.clientX - rect.left + 14;
728
+ let ty = e.clientY - rect.top - 40;
729
+ if (tx + 180 > W) tx = e.clientX - rect.left - 190;
730
+ if (ty < 4) ty = 4;
731
+ tip.style.left = tx + 'px';
732
+ tip.style.top = ty + 'px';
733
+ }
734
+ });
735
+ container.addEventListener('mouseleave', () => { tip.style.opacity = '0'; });
736
+
737
+ // --- Init ---
738
+ resize();
739
+ if (window.ResizeObserver) new ResizeObserver(resize).observe(container);
740
+ else window.addEventListener('resize', resize);
741
+
742
+ fetchFirst(JSON_PATHS).then(raw => {
743
+ promptDistribution = buildDistribution(raw);
744
+ buildCdf();
745
+ rebuildShelfTextures();
746
+ requestAnimationFrame(render);
747
+ }).catch(err => {
748
+ container.innerHTML = `<pre style="color:red;padding:12px">${err.message}</pre>`;
749
+ });
750
+ };
751
+
752
+ if (document.readyState === 'loading') {
753
+ document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
754
+ } else { bootstrap(); }
755
+ })();
756
+ </script>