+112
-7
@@ -493,6 +493,7 @@
|
|||||||
const MIN_SPHERE_SIZE = 0.16;
|
const MIN_SPHERE_SIZE = 0.16;
|
||||||
const MAX_SPHERE_SIZE = 0.62;
|
const MAX_SPHERE_SIZE = 0.62;
|
||||||
const HZ_LABEL_FADE_MS = 2000;
|
const HZ_LABEL_FADE_MS = 2000;
|
||||||
|
const POINT_FADE_WINDOW_SEC = 2;
|
||||||
|
|
||||||
function setFrequencyColor(color, fn, an) {
|
function setFrequencyColor(color, fn, an) {
|
||||||
const t = Math.min(Math.max(fn, 0), 1);
|
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 ────────────────────────────────────────
|
// ── Build instanced visualization ────────────────────────────────────────
|
||||||
// All instances are pre-loaded but initially count=0 so nothing is drawn.
|
// All instances are pre-loaded but initially count=0 so nothing is drawn.
|
||||||
// The animation loop reveals them as the audio plays.
|
// The animation loop reveals them as the audio plays.
|
||||||
@@ -543,20 +560,47 @@
|
|||||||
|
|
||||||
const n = data.tn.length;
|
const n = data.tn.length;
|
||||||
const positions = new Float32Array(n * 3);
|
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 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({
|
const sphereMat = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
vertexColors: true,
|
vertexColors: true,
|
||||||
transparent: false,
|
transparent: true,
|
||||||
opacity: 1,
|
opacity: 0.78,
|
||||||
|
depthWrite: false,
|
||||||
fog: false,
|
fog: false,
|
||||||
toneMapped: 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 = new THREE.InstancedMesh(sphereGeo, sphereMat, n);
|
||||||
audioMesh.count = 0;
|
audioMesh.count = n;
|
||||||
|
|
||||||
const lineGeo = new THREE.BufferGeometry();
|
const lineGeo = new THREE.BufferGeometry();
|
||||||
const lineMat = new THREE.LineBasicMaterial({
|
const lineMat = new THREE.LineBasicMaterial({
|
||||||
color: 0xbfd5ff,
|
color: 0xbfd5ff,
|
||||||
|
vertexColors: true,
|
||||||
linewidth: 1,
|
linewidth: 1,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.32,
|
opacity: 0.32,
|
||||||
@@ -594,15 +638,31 @@
|
|||||||
|
|
||||||
setFrequencyColor(color, fn, an);
|
setFrequencyColor(color, fn, an);
|
||||||
audioMesh.setColorAt(i, color);
|
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;
|
audioPositions = positions;
|
||||||
audioMesh.instanceMatrix.needsUpdate = true;
|
audioMesh.instanceMatrix.needsUpdate = true;
|
||||||
audioMesh.instanceColor.needsUpdate = true;
|
if (audioMesh.instanceColor) audioMesh.instanceColor.needsUpdate = true;
|
||||||
lineGeo.setAttribute(
|
lineGeo.setAttribute(
|
||||||
"position",
|
"position",
|
||||||
new THREE.BufferAttribute(positions, 3),
|
new THREE.BufferAttribute(positions, 3),
|
||||||
);
|
);
|
||||||
|
lineGeo.setAttribute(
|
||||||
|
"color",
|
||||||
|
new THREE.BufferAttribute(instanceColors, 3),
|
||||||
|
);
|
||||||
lineGeo.setDrawRange(0, 0);
|
lineGeo.setDrawRange(0, 0);
|
||||||
audioLine = new THREE.Line(lineGeo, lineMat);
|
audioLine = new THREE.Line(lineGeo, lineMat);
|
||||||
|
|
||||||
@@ -632,7 +692,7 @@
|
|||||||
let autoOrbit = true;
|
let autoOrbit = true;
|
||||||
let isPointerHeldInCanvas = false;
|
let isPointerHeldInCanvas = false;
|
||||||
let isControlInteracting = false;
|
let isControlInteracting = false;
|
||||||
let orbitSpeed = 0.6; // radians per second
|
let orbitSpeed = 0.1; // radians per second
|
||||||
const orbitClock = new THREE.Clock();
|
const orbitClock = new THREE.Clock();
|
||||||
|
|
||||||
function syncAutoOrbitState() {
|
function syncAutoOrbitState() {
|
||||||
@@ -726,9 +786,25 @@
|
|||||||
function updateRevealAndTrail(nowMs) {
|
function updateRevealAndTrail(nowMs) {
|
||||||
const visibleCount = getVisibleCountAtCurrentTime();
|
const visibleCount = getVisibleCountAtCurrentTime();
|
||||||
if (!audioMesh || !audioLine) return;
|
if (!audioMesh || !audioLine) return;
|
||||||
|
const cutoffTime = audioEl.currentTime - POINT_FADE_WINDOW_SEC;
|
||||||
|
const times = audioData?.t;
|
||||||
|
let startIndex = 0;
|
||||||
|
|
||||||
audioMesh.count = visibleCount;
|
if (times && cutoffTime > 0) {
|
||||||
audioLine.geometry.setDrawRange(0, Math.max(0, visibleCount));
|
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;
|
const revealIndex = visibleCount - 1;
|
||||||
if (revealIndex !== lastRevealIndex && revealIndex >= 0) {
|
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() {
|
function updateTimeCursorFromPlayback() {
|
||||||
if (!timeCursor.visible || audioEl.duration <= 0) return;
|
if (!timeCursor.visible || audioEl.duration <= 0) return;
|
||||||
|
|
||||||
@@ -768,6 +872,7 @@
|
|||||||
|
|
||||||
orbitControls.update();
|
orbitControls.update();
|
||||||
updateRevealAndTrail(nowMs);
|
updateRevealAndTrail(nowMs);
|
||||||
|
updatePointFadeByPlayback();
|
||||||
updateTimeCursorFromPlayback();
|
updateTimeCursorFromPlayback();
|
||||||
|
|
||||||
updateHzLabel(nowMs);
|
updateHzLabel(nowMs);
|
||||||
|
|||||||
Reference in New Issue
Block a user