Files
u-desktop/web/themes/audio-viz.html

263 lines
8.2 KiB
HTML

<canvas id="canvas" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;"></canvas>
<script>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var audioBins = new Array(128).fill(0);
var vizStyle = window.__audioVizStyle || 'bars';
var sensitivity = window.__audioVizSensitivity || 1.5;
var colorScheme = window.__audioVizColors || 'neon';
var idleTime = 0;
var lastBinSum = 0;
// 暴露配置热更新接口
window.setAudioVizStyle = function(s) { vizStyle = s; };
window.setAudioSensitivity = function(s) { sensitivity = s; };
window.setAudioColorScheme = function(s) { colorScheme = s; };
window.updateAudioDataFromGo = function(data) {
for (var i = 0; i < Math.min(data.length, audioBins.length); i++) {
audioBins[i] = data[i];
}
};
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// 配色方案
function getColor(i, total, alpha) {
var t = i / total;
switch (colorScheme) {
case 'ocean':
return 'hsla(' + (180 + t * 60) + ',70%,60%,' + alpha + ')';
case 'fire':
return 'hsla(' + (t * 60) + ',90%,55%,' + alpha + ')';
case 'rainbow':
return 'hsla(' + (t * 360) + ',80%,60%,' + alpha + ')';
default: // neon
return 'hsla(' + (220 + t * 80) + ',80%,60%,' + alpha + ')';
}
}
function getColorRGB(i, total) {
var t = i / total;
switch (colorScheme) {
case 'ocean':
return {r: 0, g: Math.floor(128 + t * 127), b: Math.floor(200 + t * 55)};
case 'fire':
return {r: Math.floor(200 + t * 55), g: Math.floor(t * 180), b: 0};
case 'rainbow':
var h = t * 360, s = 0.8, l = 0.6;
var c = (1 - Math.abs(2*l - 1)) * s;
var x = c * (1 - Math.abs((h/60)%2 - 1));
var m = l - c/2;
var r=0, g=0, b=0;
if(h<60){r=c;g=x;}else if(h<120){r=x;g=c;}else if(h<180){g=c;b=x;}
else if(h<240){g=x;b=c;}else if(h<300){r=x;b=c;}else{r=c;b=x;}
return {r:Math.floor((r+m)*255), g:Math.floor((g+m)*255), b:Math.floor((b+m)*255)};
default:
return {r: Math.floor(80 + t * 100), g: Math.floor(50 + t * 80), b: Math.floor(180 + t * 75)};
}
}
// === Bars ===
function renderBars(w, h) {
var count = audioBins.length;
var barW = (w * 0.85) / count;
var gap = barW * 0.15;
var actualW = barW - gap;
var offsetX = w * 0.075;
for (var i = 0; i < count; i++) {
var val = Math.min(1, audioBins[i] * sensitivity);
if (val < 0.01) continue;
var barH = val * h * 0.65;
var x = offsetX + i * barW;
var y = h - barH;
var grad = ctx.createLinearGradient(x, y, x, h);
grad.addColorStop(0, getColor(i, count, 0.95));
grad.addColorStop(1, getColor(i, count, 0.15));
ctx.fillStyle = grad;
ctx.fillRect(x, y, actualW, barH);
// 顶部高光
ctx.shadowColor = getColor(i, count, 0.6);
ctx.shadowBlur = 6;
ctx.fillRect(x, y, actualW, 2);
ctx.shadowBlur = 0;
// 底部反射
var refGrad = ctx.createLinearGradient(x, h, x, h + barH * 0.3);
refGrad.addColorStop(0, getColor(i, count, 0.15));
refGrad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = refGrad;
ctx.fillRect(x, h, actualW, barH * 0.3);
}
}
// === Waveform ===
function renderWaveform(w, h) {
var count = audioBins.length;
var midY = h * 0.5;
var amplitude = h * 0.35 * sensitivity;
ctx.beginPath();
ctx.moveTo(0, midY);
for (var i = 0; i < count; i++) {
var x = (i / (count - 1)) * w;
var val = audioBins[i] * amplitude;
var y = midY - val;
if (i === 0) {
ctx.moveTo(x, y);
} else {
var prevX = ((i - 1) / (count - 1)) * w;
var cpx = (prevX + x) / 2;
ctx.quadraticCurveTo(prevX, midY - audioBins[i-1] * amplitude, cpx, (midY - audioBins[i-1] * amplitude + y) / 2);
}
}
ctx.lineTo(w, midY);
// 镜像
ctx.moveTo(0, midY);
for (var i = 0; i < count; i++) {
var x = (i / (count - 1)) * w;
var val = audioBins[i] * amplitude * 0.6;
var y = midY + val;
if (i === 0) {
ctx.lineTo(x, y);
} else {
var prevX = ((i - 1) / (count - 1)) * w;
var cpx = (prevX + x) / 2;
ctx.quadraticCurveTo(prevX, midY + audioBins[i-1] * amplitude * 0.6, cpx, (midY + audioBins[i-1] * amplitude * 0.6 + y) / 2);
}
}
ctx.lineTo(w, midY);
ctx.strokeStyle = getColor(Math.floor(count / 2), count, 0.8);
ctx.lineWidth = 2;
ctx.shadowColor = getColor(Math.floor(count / 2), count, 0.5);
ctx.shadowBlur = 12;
ctx.stroke();
ctx.shadowBlur = 0;
// 填充区域
ctx.lineTo(w, midY);
ctx.lineTo(0, midY);
ctx.closePath();
ctx.fillStyle = getColor(Math.floor(count / 2), count, 0.08);
ctx.fill();
}
// === Circular ===
function renderCircular(w, h) {
var cx = w / 2;
var cy = h / 2;
var count = audioBins.length;
var baseR = Math.min(w, h) * 0.15;
var maxR = Math.min(w, h) * 0.4 * sensitivity;
for (var i = 0; i < count; i++) {
var angle = (i / count) * Math.PI * 2 - Math.PI / 2;
var val = Math.min(1, audioBins[i]);
var r = baseR + val * maxR;
var x1 = cx + Math.cos(angle) * baseR;
var y1 = cy + Math.sin(angle) * baseR;
var x2 = cx + Math.cos(angle) * r;
var y2 = cy + Math.sin(angle) * r;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = getColor(i, count, 0.7 + val * 0.3);
ctx.lineWidth = Math.max(1, (Math.PI * 2 * baseR / count) * 0.6);
ctx.shadowColor = getColor(i, count, 0.4);
ctx.shadowBlur = val > 0.3 ? 8 : 0;
ctx.stroke();
ctx.shadowBlur = 0;
}
// 中心光晕
var avgBin = 0;
for (var i = 0; i < count; i++) avgBin += audioBins[i];
avgBin /= count;
if (avgBin > 0.05) {
var glowR = baseR * (0.8 + avgBin * 0.5);
var glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, glowR);
glow.addColorStop(0, getColor(Math.floor(count/2), count, avgBin * 0.3));
glow.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(cx, cy, glowR, 0, Math.PI * 2);
ctx.fill();
}
}
// === Idle 呼吸动画 ===
function renderIdle(w, h, ts) {
var cx = w / 2;
var cy = h / 2;
var t = ts / 1000;
var breathR = Math.min(w, h) * 0.15 * (1 + 0.15 * Math.sin(t * 0.8));
var grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, breathR);
grad.addColorStop(0, getColor(0, 1, 0.15 + 0.05 * Math.sin(t)));
grad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(cx, cy, breathR, 0, Math.PI * 2);
ctx.fill();
// 微弱粒子
for (var i = 0; i < 6; i++) {
var angle = (i / 6) * Math.PI * 2 + t * 0.3;
var dist = breathR * (1.5 + 0.3 * Math.sin(t * 1.2 + i));
var px = cx + Math.cos(angle) * dist;
var py = cy + Math.sin(angle) * dist;
ctx.beginPath();
ctx.arc(px, py, 2, 0, Math.PI * 2);
ctx.fillStyle = getColor(i, 6, 0.2 + 0.1 * Math.sin(t + i));
ctx.fill();
}
}
var FPS = 30, lastFrame = 0;
function render(ts) {
requestAnimationFrame(render);
if (ts - lastFrame < 1000 / FPS) return;
lastFrame = ts;
var w = canvas.width, h = canvas.height;
// 半透明背景实现拖尾效果
ctx.fillStyle = 'rgba(5, 5, 15, 0.25)';
ctx.fillRect(0, 0, w, h);
// 检测空闲
var binSum = 0;
for (var i = 0; i < audioBins.length; i++) binSum += audioBins[i];
if (binSum < 0.5) {
idleTime++;
} else {
idleTime = 0;
}
if (idleTime > 60) { // ~2 seconds at 30fps
renderIdle(w, h, ts);
return;
}
switch (vizStyle) {
case 'waveform': renderWaveform(w, h); break;
case 'circular': renderCircular(w, h); break;
default: renderBars(w, h);
}
}
requestAnimationFrame(render);
</script>