Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
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>
|