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;
}
.controls {
display: flex;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 16px;
padding: 12px 0;
column-gap: 16px;
row-gap: 10px;
padding: 14px 0;
border-top: 1px solid var(--hair);
border-bottom: 1px solid var(--hair);
margin-bottom: 24px;
@ -73,7 +75,6 @@
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
min-width: 88px;
}
.ctl-value {
font-family: "JetBrains Mono", "SF Mono", Menlo, Monaco, monospace;
@ -83,8 +84,8 @@
text-align: right;
font-variant-numeric: tabular-nums;
}
#n-slider {
flex: 1;
.controls input[type="range"] {
width: 100%;
accent-color: var(--accent);
height: 4px;
}
@ -98,20 +99,41 @@
color: var(--text);
}
.gallery {
display: grid;
grid-template-columns: repeat(3, minmax(0, 260px));
display: flex;
gap: 20px;
justify-content: start;
margin-bottom: 28px;
overflow-x: auto;
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) {
.gallery {
grid-template-columns: minmax(0, 320px);
justify-content: center;
}
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 {
flex: 0 0 260px;
scroll-snap-align: start;
border: 1px solid var(--hair);
background: var(--panel);
cursor: pointer;
@ -271,13 +293,25 @@
<p class="lede">
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>
<div class="controls">
<label class="ctl-label" for="n-slider">n samples</label>
<input type="range" id="n-slider" min="100" max="5000" step="100" value="500">
<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 class="gallery" id="gallery">
@ -338,9 +372,24 @@ function normalize(points) {
out[i*3+1] = (points[i][1] - my) / 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) {
const n = labels.length;
const colors = new Float32Array(n * 3);
@ -362,9 +411,12 @@ function buildColors(labels, kind) {
}
function createScene(container, dataset) {
const positions = normalize(dataset.points);
const { positions: basePositions, scaleFactor } = normalize(dataset.points);
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();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
@ -398,7 +450,18 @@ function createScene(container, dataset) {
controls.minDistance = 1.5;
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) {
@ -468,7 +531,14 @@ async function main() {
card.classList.add('selected');
selectedId = id;
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.
@ -484,15 +554,43 @@ async function main() {
slider.addEventListener('input', (e) => applyN(parseInt(e.target.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) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
const idx = { '1': 0, '2': 1, '3': 2 }[e.key];
if (idx === undefined) return;
const [id, ds] = order[idx] || [];
if (!id) return;
const card = gallery.children[idx];
selectCard(id, card, ds);
if (/^[1-9]$/.test(e.key)) {
selectByIndex(parseInt(e.key, 10) - 1);
return;
}
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
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.
@ -512,7 +610,32 @@ async function main() {
// Render loop.
function tick() {
requestAnimationFrame(tick);
const now = performance.now();
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.renderer.render(s.scene, s.camera);
}