+112
-7
@@ -493,6 +493,7 @@
|
||||
const MIN_SPHERE_SIZE = 0.16;
|
||||
const MAX_SPHERE_SIZE = 0.62;
|
||||
const HZ_LABEL_FADE_MS = 2000;
|
||||
const POINT_FADE_WINDOW_SEC = 2;
|
||||
|
||||
function setFrequencyColor(color, fn, an) {
|
||||
const t = Math.min(Math.max(fn, 0), 1);
|
||||
@@ -509,6 +510,22 @@
|
||||
);
|
||||
}
|
||||
|
||||
function syncFrequencyLegend() {
|
||||
const gradStrip = document.querySelector(".grad-strip");
|
||||
if (!gradStrip) return;
|
||||
|
||||
const samples = [0, 0.25, 0.5, 0.75, 1];
|
||||
const c = new THREE.Color();
|
||||
const stops = samples.map((fn) => {
|
||||
setFrequencyColor(c, fn, 0.85);
|
||||
return `#${c.getHexString()} ${Math.round(fn * 100)}%`;
|
||||
});
|
||||
|
||||
gradStrip.style.background = `linear-gradient(to right, ${stops.join(", ")})`;
|
||||
}
|
||||
|
||||
syncFrequencyLegend();
|
||||
|
||||
// ── Build instanced visualization ────────────────────────────────────────
|
||||
// All instances are pre-loaded but initially count=0 so nothing is drawn.
|
||||
// The animation loop reveals them as the audio plays.
|
||||
@@ -543,20 +560,47 @@
|
||||
|
||||
const n = data.tn.length;
|
||||
const positions = new Float32Array(n * 3);
|
||||
const instanceColors = new Float32Array(n * 3);
|
||||
const instanceAlphas = new Float32Array(n);
|
||||
const sphereGeo = new THREE.SphereGeometry(1, 10, 8);
|
||||
const baseVertexColors = new Float32Array(
|
||||
sphereGeo.attributes.position.count * 3,
|
||||
);
|
||||
baseVertexColors.fill(1);
|
||||
sphereGeo.setAttribute(
|
||||
"color",
|
||||
new THREE.Float32BufferAttribute(baseVertexColors, 3),
|
||||
);
|
||||
const sphereMat = new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
vertexColors: true,
|
||||
transparent: false,
|
||||
opacity: 1,
|
||||
transparent: true,
|
||||
opacity: 0.78,
|
||||
depthWrite: false,
|
||||
fog: false,
|
||||
toneMapped: false,
|
||||
});
|
||||
sphereMat.onBeforeCompile = (shader) => {
|
||||
shader.vertexShader =
|
||||
"attribute float instanceAlpha; varying float vInstanceAlpha;\n" +
|
||||
shader.vertexShader.replace(
|
||||
"#include <begin_vertex>",
|
||||
"#include <begin_vertex>\n vInstanceAlpha = instanceAlpha;",
|
||||
);
|
||||
shader.fragmentShader =
|
||||
"varying float vInstanceAlpha;\n" +
|
||||
shader.fragmentShader.replace(
|
||||
"vec4 diffuseColor = vec4( diffuse, opacity );",
|
||||
"vec4 diffuseColor = vec4( diffuse, opacity * vInstanceAlpha );",
|
||||
);
|
||||
};
|
||||
audioMesh = new THREE.InstancedMesh(sphereGeo, sphereMat, n);
|
||||
audioMesh.count = 0;
|
||||
audioMesh.count = n;
|
||||
|
||||
const lineGeo = new THREE.BufferGeometry();
|
||||
const lineMat = new THREE.LineBasicMaterial({
|
||||
color: 0xbfd5ff,
|
||||
vertexColors: true,
|
||||
linewidth: 1,
|
||||
transparent: true,
|
||||
opacity: 0.32,
|
||||
@@ -594,15 +638,31 @@
|
||||
|
||||
setFrequencyColor(color, fn, an);
|
||||
audioMesh.setColorAt(i, color);
|
||||
instanceColors[p3] = color.r;
|
||||
instanceColors[p3 + 1] = color.g;
|
||||
instanceColors[p3 + 2] = color.b;
|
||||
instanceAlphas[i] = 0;
|
||||
}
|
||||
|
||||
audioMesh.instanceColor = new THREE.InstancedBufferAttribute(
|
||||
instanceColors,
|
||||
3,
|
||||
);
|
||||
audioMesh.geometry.setAttribute(
|
||||
"instanceAlpha",
|
||||
new THREE.InstancedBufferAttribute(instanceAlphas, 1),
|
||||
);
|
||||
audioPositions = positions;
|
||||
audioMesh.instanceMatrix.needsUpdate = true;
|
||||
audioMesh.instanceColor.needsUpdate = true;
|
||||
if (audioMesh.instanceColor) audioMesh.instanceColor.needsUpdate = true;
|
||||
lineGeo.setAttribute(
|
||||
"position",
|
||||
new THREE.BufferAttribute(positions, 3),
|
||||
);
|
||||
lineGeo.setAttribute(
|
||||
"color",
|
||||
new THREE.BufferAttribute(instanceColors, 3),
|
||||
);
|
||||
lineGeo.setDrawRange(0, 0);
|
||||
audioLine = new THREE.Line(lineGeo, lineMat);
|
||||
|
||||
@@ -632,7 +692,7 @@
|
||||
let autoOrbit = true;
|
||||
let isPointerHeldInCanvas = false;
|
||||
let isControlInteracting = false;
|
||||
let orbitSpeed = 0.6; // radians per second
|
||||
let orbitSpeed = 0.1; // radians per second
|
||||
const orbitClock = new THREE.Clock();
|
||||
|
||||
function syncAutoOrbitState() {
|
||||
@@ -726,9 +786,25 @@
|
||||
function updateRevealAndTrail(nowMs) {
|
||||
const visibleCount = getVisibleCountAtCurrentTime();
|
||||
if (!audioMesh || !audioLine) return;
|
||||
const cutoffTime = audioEl.currentTime - POINT_FADE_WINDOW_SEC;
|
||||
const times = audioData?.t;
|
||||
let startIndex = 0;
|
||||
|
||||
audioMesh.count = visibleCount;
|
||||
audioLine.geometry.setDrawRange(0, Math.max(0, visibleCount));
|
||||
if (times && cutoffTime > 0) {
|
||||
let lo = 0;
|
||||
let hi = visibleCount;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
if (times[mid] < cutoffTime) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
startIndex = lo;
|
||||
}
|
||||
|
||||
audioLine.geometry.setDrawRange(
|
||||
startIndex,
|
||||
Math.max(0, visibleCount - startIndex),
|
||||
);
|
||||
|
||||
const revealIndex = visibleCount - 1;
|
||||
if (revealIndex !== lastRevealIndex && revealIndex >= 0) {
|
||||
@@ -737,6 +813,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updatePointFadeByPlayback() {
|
||||
if (!audioMesh || !audioData || audioEl.duration <= 0) return;
|
||||
const alphaAttr = audioMesh.geometry.getAttribute("instanceAlpha");
|
||||
if (!alphaAttr) return;
|
||||
|
||||
const currentT = audioEl.currentTime;
|
||||
const isAtStart = audioEl.paused && currentT === 0;
|
||||
const times = audioData.t;
|
||||
const alphas = alphaAttr.array;
|
||||
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
if (isAtStart) {
|
||||
alphas[i] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
const age = currentT - times[i];
|
||||
if (age < 0 || age > POINT_FADE_WINDOW_SEC) {
|
||||
alphas[i] = 0;
|
||||
} else {
|
||||
const fade = 1 - age / POINT_FADE_WINDOW_SEC;
|
||||
alphas[i] = fade;
|
||||
}
|
||||
}
|
||||
|
||||
alphaAttr.needsUpdate = true;
|
||||
}
|
||||
|
||||
function updateTimeCursorFromPlayback() {
|
||||
if (!timeCursor.visible || audioEl.duration <= 0) return;
|
||||
|
||||
@@ -768,6 +872,7 @@
|
||||
|
||||
orbitControls.update();
|
||||
updateRevealAndTrail(nowMs);
|
||||
updatePointFadeByPlayback();
|
||||
updateTimeCursorFromPlayback();
|
||||
|
||||
updateHzLabel(nowMs);
|
||||
|
||||
Reference in New Issue
Block a user