scrolling, animation fixes
This commit is contained in:
parent
7a6e92b31c
commit
1aa72d6412
@ -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> <kbd>2</kbd> <kbd>3</kbd> to select.
|
<kbd>←</kbd> <kbd>→</kbd> or <kbd>1</kbd> <kbd>2</kbd> <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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user