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 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);