Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
F04C
2026-05-03 12:31:25 +08:00
parent 30aa032990
commit 3926c6e7ab
+112 -7
View File
@@ -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);