scrolling, animation fixes

This commit is contained in:
Michael Pilosov 2026-04-21 18:23:01 -06:00
parent 7a6e92b31c
commit 1aa72d6412

View File

@ -59,10 +59,12 @@
font-size: 15px; font-size: 15px;
} }
.controls { .controls {
display: flex; display: grid;
grid-template-columns: auto 1fr auto;
align-items: center; align-items: center;
gap: 16px; column-gap: 16px;
padding: 12px 0; row-gap: 10px;
padding: 14px 0;
border-top: 1px solid var(--hair); border-top: 1px solid var(--hair);
border-bottom: 1px solid var(--hair); border-bottom: 1px solid var(--hair);
margin-bottom: 24px; margin-bottom: 24px;
@ -73,7 +75,6 @@
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
color: var(--muted); color: var(--muted);
min-width: 88px;
} }
.ctl-value { .ctl-value {
font-family: "JetBrains Mono", "SF Mono", Menlo, Monaco, monospace; font-family: "JetBrains Mono", "SF Mono", Menlo, Monaco, monospace;
@ -83,8 +84,8 @@
text-align: right; text-align: right;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
#n-slider { .controls input[type="range"] {
flex: 1; width: 100%;
accent-color: var(--accent); accent-color: var(--accent);
height: 4px; height: 4px;
} }
@ -98,20 +99,41 @@
color: var(--text); color: var(--text);
} }
.gallery { .gallery {
display: grid; display: flex;
grid-template-columns: repeat(3, minmax(0, 260px));
gap: 20px; gap: 20px;
justify-content: start; overflow-x: auto;
margin-bottom: 28px; overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
padding: 2px 2px 14px;
margin: 0 -2px 20px;
scrollbar-width: thin;
scrollbar-color: var(--hair) transparent;
} }
.gallery::-webkit-scrollbar { height: 6px; }
.gallery::-webkit-scrollbar-track { background: transparent; }
.gallery::-webkit-scrollbar-thumb {
background: var(--hair);
border-radius: 3px;
}
.gallery::-webkit-scrollbar-thumb:hover { background: var(--muted); }
@media (max-width: 880px) { @media (max-width: 880px) {
.gallery {
grid-template-columns: minmax(0, 320px);
justify-content: center;
}
body { padding: 28px 20px; } body { padding: 28px 20px; }
} }
@media (max-width: 520px) {
header .crumb { display: none; }
h1 { font-size: 22px; }
.footer {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.continue { width: 100%; }
}
.card { .card {
flex: 0 0 260px;
scroll-snap-align: start;
border: 1px solid var(--hair); border: 1px solid var(--hair);
background: var(--panel); background: var(--panel);
cursor: pointer; cursor: pointer;
@ -271,13 +293,25 @@
<p class="lede"> <p class="lede">
Three candidate generators for the embedding pipeline. Drag to rotate, scroll to zoom, Three candidate generators for the embedding pipeline. Drag to rotate, scroll to zoom,
<kbd>1</kbd>&nbsp;<kbd>2</kbd>&nbsp;<kbd>3</kbd> to select. <kbd></kbd>&nbsp;<kbd></kbd> or <kbd>1</kbd>&nbsp;<kbd>2</kbd>&nbsp;<kbd>3</kbd> to select.
</p> </p>
<div class="controls"> <div class="controls">
<label class="ctl-label" for="n-slider">n samples</label> <label class="ctl-label" for="n-slider">n samples</label>
<input type="range" id="n-slider" min="100" max="5000" step="100" value="500"> <input type="range" id="n-slider" min="100" max="5000" step="100" value="500">
<span class="ctl-value" id="n-value">500</span> <span class="ctl-value" id="n-value">500</span>
<label class="ctl-label" for="j-slider">noise σ</label>
<input type="range" id="j-slider" min="0" max="5" step="1" value="1" list="j-levels">
<datalist id="j-levels">
<option value="0"></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
</datalist>
<span class="ctl-value" id="j-value">0.01</span>
</div> </div>
<div class="gallery" id="gallery"> <div class="gallery" id="gallery">
@ -338,9 +372,24 @@ function normalize(points) {
out[i*3+1] = (points[i][1] - my) / maxAbs; out[i*3+1] = (points[i][1] - my) / maxAbs;
out[i*3+2] = (points[i][2] - mz) / maxAbs; out[i*3+2] = (points[i][2] - mz) / maxAbs;
} }
return out; // scaleFactor converts raw-unit jitter to normalized-unit jitter
// so a slider value in raw coords lands correctly after normalize().
return { positions: out, scaleFactor: 1 / maxAbs };
} }
// Box-Muller: fills an array with standard-normal samples.
function fillRandn(arr) {
for (let i = 0; i < arr.length; i += 2) {
const u1 = Math.random() || 1e-12;
const u2 = Math.random();
const mag = Math.sqrt(-2.0 * Math.log(u1));
arr[i] = mag * Math.cos(2 * Math.PI * u2);
if (i + 1 < arr.length) arr[i + 1] = mag * Math.sin(2 * Math.PI * u2);
}
return arr;
}
function randnArray(n) { return fillRandn(new Float32Array(n)); }
function buildColors(labels, kind) { function buildColors(labels, kind) {
const n = labels.length; const n = labels.length;
const colors = new Float32Array(n * 3); const colors = new Float32Array(n * 3);
@ -362,9 +411,12 @@ function buildColors(labels, kind) {
} }
function createScene(container, dataset) { function createScene(container, dataset) {
const positions = normalize(dataset.points); const { positions: basePositions, scaleFactor } = normalize(dataset.points);
const colors = buildColors(dataset.labels, dataset.kind); const colors = buildColors(dataset.labels, dataset.kind);
// mutable copy — the render loop writes jittered positions here.
const positions = new Float32Array(basePositions);
const geometry = new THREE.BufferGeometry(); const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
@ -398,7 +450,18 @@ function createScene(container, dataset) {
controls.minDistance = 1.5; controls.minDistance = 1.5;
controls.maxDistance = 6; controls.maxDistance = 6;
return { scene, camera, renderer, controls, container, geometry }; const noiseLen = basePositions.length;
const SNAPSHOT_MS = 900;
return {
scene, camera, renderer, controls, container, geometry,
basePositions,
scaleFactor,
noiseA: randnArray(noiseLen),
noiseB: randnArray(noiseLen),
// random phase so the three scenes don't click over in lockstep
interpStartMs: performance.now() - Math.random() * SNAPSHOT_MS,
snapshotMs: SNAPSHOT_MS,
};
} }
function sizeScene(s) { function sizeScene(s) {
@ -468,7 +531,14 @@ async function main() {
card.classList.add('selected'); card.classList.add('selected');
selectedId = id; selectedId = id;
document.getElementById('selected-path').textContent = ds.path; document.getElementById('selected-path').textContent = ds.path;
document.getElementById('continue-btn').disabled = false; updateContinue();
}
function updateContinue() {
const btn = document.getElementById('continue-btn');
btn.disabled = !(selectedId && jitterScale > 0);
btn.title = !selectedId
? 'pick a dataset first'
: (jitterScale <= 0 ? 'noise σ must be > 0 to simulate' : '');
} }
// Sample-count slider → cheap: just change draw range per geometry. // Sample-count slider → cheap: just change draw range per geometry.
@ -484,15 +554,43 @@ async function main() {
slider.addEventListener('input', (e) => applyN(parseInt(e.target.value, 10))); slider.addEventListener('input', (e) => applyN(parseInt(e.target.value, 10)));
applyN(parseInt(slider.value, 10)); applyN(parseInt(slider.value, 10));
// Keyboard: 1/2/3 select. // Noise slider → rewrites positions each frame in the render loop.
let jitterScale = 0;
const jSlider = document.getElementById('j-slider');
const jValue = document.getElementById('j-value');
function applyJ(v) {
jitterScale = v;
jValue.textContent = v.toFixed(2);
updateContinue();
}
jSlider.addEventListener('input', (e) => applyJ(parseFloat(e.target.value)));
applyJ(parseFloat(jSlider.value));
// Keyboard: digits jump directly, arrows step.
function selectByIndex(idx, { scroll = true } = {}) {
const entry = order[idx];
if (!entry) return;
const [id, ds] = entry;
const card = gallery.children[idx];
if (!card) return;
selectCard(id, card, ds);
if (scroll) card.scrollIntoView({ behavior: 'smooth', inline: 'nearest', block: 'nearest' });
}
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
const idx = { '1': 0, '2': 1, '3': 2 }[e.key]; if (/^[1-9]$/.test(e.key)) {
if (idx === undefined) return; selectByIndex(parseInt(e.key, 10) - 1);
const [id, ds] = order[idx] || []; return;
if (!id) return; }
const card = gallery.children[idx]; if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
selectCard(id, card, ds); e.preventDefault();
const currentIdx = order.findIndex(([id]) => id === selectedId);
const n = order.length;
const nextIdx = e.key === 'ArrowRight'
? (currentIdx < 0 ? 0 : Math.min(currentIdx + 1, n - 1))
: (currentIdx < 0 ? n - 1 : Math.max(currentIdx - 1, 0));
selectByIndex(nextIdx);
}
}); });
// Continue button → for now, just echo the selection. // Continue button → for now, just echo the selection.
@ -512,7 +610,32 @@ async function main() {
// Render loop. // Render loop.
function tick() { function tick() {
requestAnimationFrame(tick); requestAnimationFrame(tick);
const now = performance.now();
for (const s of scenes) { for (const s of scenes) {
// Advance the A→B snapshot transition; regenerate B when we roll over.
let t = (now - s.interpStartMs) / s.snapshotMs;
if (t >= 1) {
const tmp = s.noiseA; s.noiseA = s.noiseB; s.noiseB = tmp;
fillRandn(s.noiseB);
s.interpStartMs = now;
t = 0;
}
const total = s.basePositions.length / 3;
const drawCount = s.geometry.drawRange.count;
const visibleN = Number.isFinite(drawCount) ? Math.min(drawCount, total) : total;
const limit = visibleN * 3;
const pos = s.geometry.attributes.position.array;
const base = s.basePositions;
const a = s.noiseA, b = s.noiseB;
// Jitter is applied in normalized space so σ means the same thing
// across datasets — independent of the raw feature magnitudes.
const scale = jitterScale;
const u = 1 - t;
for (let i = 0; i < limit; i++) {
pos[i] = base[i] + (a[i] * u + b[i] * t) * scale;
}
s.geometry.attributes.position.needsUpdate = true;
s.controls.update(); s.controls.update();
s.renderer.render(s.scene, s.camera); s.renderer.render(s.scene, s.camera);
} }