Three.js + Web Audio FFT visualizer with Constellation, Texture, Frequency and Modulation views. Vite build, node --test suite. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JABXBRLs9T6nqHKUVwoTHJ
77 lines
4.0 KiB
JavaScript
77 lines
4.0 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { ConstellationLayer } from '../src/constellation.js';
|
|
|
|
test('rendered links keep correct endpoints after expiry, with labels fixed and fades independent of uploads', () => {
|
|
// Exercise real Three.js buffers without requiring a browser or GPU.
|
|
const originalDocument = globalThis.document;
|
|
globalThis.document = { createElement: () => ({ getContext: () => ({ clearRect() {}, fillText() {} }) }) };
|
|
let layer;
|
|
try {
|
|
layer = new ConstellationLayer();
|
|
const settings = { trail: 3, sensitivity: 1, palette: 'spectrum', colorMode: 'position', glow: 1.35, connections: true, labels: true };
|
|
const frame = time => ({ time, rms: .2, entropy: .4, spread: 900, peaks: [{ frequency: 1500 + time * 10, amplitude: .2 }] });
|
|
const history = Array.from({ length: 60 }, (_, i) => frame(i / 30));
|
|
layer.update(history, 2, settings, 600);
|
|
const label = layer.labelPool.find(label => label.node);
|
|
assert.ok(label);
|
|
const labelNode = label.node;
|
|
const labelPosition = label.sprite.position.clone();
|
|
const uploadVersion = layer.geometry.attributes.position.version;
|
|
layer.animate(2.01);
|
|
assert.equal(layer.uniforms.now.value, 2.01);
|
|
assert.equal(layer.lines.material.uniforms.now.value, 2.01);
|
|
assert.equal(layer.geometry.attributes.position.version, uploadVersion);
|
|
layer.update(history, 2.01, settings, 600);
|
|
assert.equal(layer.geometry.attributes.position.version, uploadVersion);
|
|
history.push(frame(2.05));
|
|
layer.update(history, 2.05, settings, 600);
|
|
assert.equal(label.node, labelNode);
|
|
assert.deepEqual(label.sprite.position, labelPosition);
|
|
|
|
// Several old points expire; line indices must still reference the surviving points.
|
|
history.push(frame(3.1));
|
|
layer.update(history, 3.1, settings, 600);
|
|
assert.ok(layer.model.nodes.every(node => node.time > .1));
|
|
assert.equal(layer.lineGeometry.instanceCount, layer.model.links.length);
|
|
assert.equal(layer.lineGeometry.index.count, 6);
|
|
assert.ok(layer.model.links.some(([, , temporal]) => temporal));
|
|
for (const [i, [a, b]] of layer.model.links.entries()) {
|
|
assert.deepEqual(Array.from(layer.linePositions.slice(i * 6, i * 6 + 6)), Array.from(new Float32Array([...layer.model.nodes[a].position, ...layer.model.nodes[b].position])));
|
|
}
|
|
settings.connections = false;
|
|
layer.update(history, 3.1, settings, 600);
|
|
assert.equal(layer.lines.visible, false);
|
|
layer.update([], 0, settings, 600);
|
|
assert.equal(layer.geometry.drawRange.count, 0);
|
|
assert.equal(layer.lineGeometry.instanceCount, 0);
|
|
assert.ok(layer.labelPool.every(label => !label.node && !label.sprite.visible));
|
|
} finally {
|
|
layer?.traverse(object => { object.geometry?.dispose(); object.material?.map?.dispose(); object.material?.dispose(); });
|
|
if (originalDocument === undefined) delete globalThis.document;
|
|
else globalThis.document = originalDocument;
|
|
}
|
|
});
|
|
|
|
test('old annotations fade gradually to zero without another audio sample', () => {
|
|
const originalDocument=globalThis.document;
|
|
globalThis.document={createElement:()=>({getContext:()=>({clearRect(){},fillText(){}})})};
|
|
let layer;
|
|
try {
|
|
layer=new ConstellationLayer();
|
|
layer.update([{time:0,rms:.2,entropy:.4,spread:900,peaks:[{frequency:1500,amplitude:.2}]}],.2,{trail:6,sensitivity:1,palette:'spectrum',colorMode:'position',glow:1,connections:true,labels:true},600);
|
|
const label=layer.labelPool.find(label=>label.node);
|
|
const fresh=label.sprite.material.opacity;
|
|
layer.animate(3);
|
|
const halfway=label.sprite.material.opacity;
|
|
assert.ok(halfway<fresh*.5);
|
|
layer.animate(5.9);
|
|
assert.ok(label.sprite.material.opacity<.01);
|
|
layer.animate(6);
|
|
assert.equal(label.sprite.material.opacity,0);
|
|
} finally {
|
|
layer?.traverse(o=>{o.geometry?.dispose();o.material?.map?.dispose();o.material?.dispose();});
|
|
if(originalDocument===undefined)delete globalThis.document;else globalThis.document=originalDocument;
|
|
}
|
|
});
|