654 lines
18 KiB
HTML
654 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>CYBER AUDIO // NEURAL VISUALIZER</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html, body {
|
|
width: 100%; height: 100%; overflow: hidden;
|
|
background: #05010d;
|
|
font-family: 'Courier New', 'Consolas', monospace;
|
|
color: #00ffff;
|
|
cursor: default;
|
|
}
|
|
canvas { display: block; position: fixed; top: 0; left: 0; z-index: 1; }
|
|
|
|
/* HUD 控制面板 */
|
|
#hud {
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 10;
|
|
display: flex; justify-content: space-between; align-items: flex-start;
|
|
padding: 20px 30px; pointer-events: none;
|
|
}
|
|
#hud-left, #hud-right { pointer-events: auto; }
|
|
|
|
.panel {
|
|
background: rgba(5, 1, 13, 0.75);
|
|
border: 1px solid rgba(0, 255, 255, 0.3);
|
|
padding: 12px 18px;
|
|
position: relative;
|
|
}
|
|
.panel::before, .panel::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 10px; height: 10px;
|
|
border: 1px solid #ff00ff;
|
|
}
|
|
.panel::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
|
|
.panel::after { bottom: -1px; right: -1px; border-left: none; border-top: none; }
|
|
|
|
.title {
|
|
font-size: 11px; letter-spacing: 3px; color: #ff00ff;
|
|
text-shadow: 0 0 8px #ff00ff;
|
|
margin-bottom: 8px;
|
|
}
|
|
.status {
|
|
font-size: 10px; letter-spacing: 2px; color: #00ffff;
|
|
text-shadow: 0 0 6px #00ffff;
|
|
}
|
|
.status.off { color: #444; text-shadow: none; }
|
|
.status.rec { color: #ff0040; text-shadow: 0 0 8px #ff0040; animation: blink 1s infinite; }
|
|
@keyframes blink { 50% { opacity: 0.3; } }
|
|
|
|
/* 按钮 */
|
|
#controls {
|
|
position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
|
|
z-index: 10; display: flex; gap: 40px;
|
|
}
|
|
.btn {
|
|
background: none; border: none; outline: none;
|
|
font-family: inherit; font-size: 13px; letter-spacing: 4px;
|
|
color: #00ffff; text-shadow: 0 0 8px #00ffff;
|
|
cursor: pointer; padding: 8px 4px; position: relative;
|
|
text-transform: uppercase;
|
|
transition: color 0.2s, text-shadow 0.2s;
|
|
}
|
|
.btn::after {
|
|
content: ''; position: absolute; bottom: 0; left: 50%;
|
|
width: 0; height: 1px;
|
|
background: #ff00ff; box-shadow: 0 0 6px #ff00ff;
|
|
transition: width 0.3s, left 0.3s;
|
|
}
|
|
.btn:hover { color: #ff00ff; text-shadow: 0 0 12px #ff00ff; }
|
|
.btn:hover::after { width: 100%; left: 0; }
|
|
.btn:active { transform: scale(0.97); }
|
|
|
|
/* 角落装饰 */
|
|
.corner {
|
|
position: fixed; width: 60px; height: 60px; z-index: 10;
|
|
border-color: #ff00ff; pointer-events: none;
|
|
}
|
|
.corner.tl { top: 10px; left: 10px; border-top: 1px solid; border-left: 1px solid; }
|
|
.corner.tr { top: 10px; right: 10px; border-top: 1px solid; border-right: 1px solid; }
|
|
.corner.bl { bottom: 10px; left: 10px; border-bottom: 1px solid; border-left: 1px solid; }
|
|
.corner.br { bottom: 10px; right: 10px; border-bottom: 1px solid; border-right: 1px solid; }
|
|
|
|
/* 故障文字层 */
|
|
#glitch-title {
|
|
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
z-index: 5; font-size: 72px; letter-spacing: 12px; font-weight: bold;
|
|
color: rgba(0, 255, 255, 0.04);
|
|
text-shadow: none;
|
|
pointer-events: none;
|
|
white-space: nowrap;
|
|
transition: opacity 0.5s;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<canvas id="cv"></canvas>
|
|
|
|
<div id="glitch-title">N E U R O S Y N C</div>
|
|
|
|
<div class="corner tl"></div>
|
|
<div class="corner tr"></div>
|
|
<div class="corner bl"></div>
|
|
<div class="corner br"></div>
|
|
|
|
<div id="hud">
|
|
<div class="panel" id="hud-left">
|
|
<div class="title">// NEURAL VISUALIZER v2.077</div>
|
|
<div class="status" id="status-mic">MIC: OFFLINE</div>
|
|
</div>
|
|
<div class="panel" id="hud-right">
|
|
<div class="title">// SIGNAL DATA</div>
|
|
<div class="status" id="status-fps">FPS: --</div>
|
|
<div class="status" id="status-vol">VOL: --</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="controls">
|
|
<button class="btn" id="btn-mic">启动麦克风</button>
|
|
<button class="btn" id="btn-fullscreen">全屏</button>
|
|
</div>
|
|
|
|
<script>
|
|
// ============================================================
|
|
// CYBER AUDIO VISUALIZER
|
|
// ============================================================
|
|
|
|
const canvas = document.getElementById('cv');
|
|
const ctx = canvas.getContext('2d');
|
|
const btnMic = document.getElementById('btn-mic');
|
|
const btnFs = document.getElementById('btn-fullscreen');
|
|
const statusMic = document.getElementById('status-mic');
|
|
const statusFps = document.getElementById('status-fps');
|
|
const statusVol = document.getElementById('status-vol');
|
|
const glitchTitle = document.getElementById('glitch-title');
|
|
|
|
let W, H;
|
|
function resize() {
|
|
W = canvas.width = window.innerWidth;
|
|
H = canvas.height = window.innerHeight;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
// --- 音频系统 ---
|
|
let audioCtx = null;
|
|
let analyser = null;
|
|
let source = null;
|
|
let dataArray = null;
|
|
let freqArray = null;
|
|
let micActive = false;
|
|
let micStream = null;
|
|
|
|
async function toggleMic() {
|
|
if (micActive) {
|
|
stopMic();
|
|
} else {
|
|
await startMic();
|
|
}
|
|
}
|
|
|
|
async function startMic() {
|
|
try {
|
|
micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
source = audioCtx.createMediaStreamSource(micStream);
|
|
analyser = audioCtx.createAnalyser();
|
|
analyser.fftSize = 512;
|
|
analyser.smoothingTimeConstant = 0.82;
|
|
source.connect(analyser);
|
|
dataArray = new Uint8Array(analyser.fftSize);
|
|
freqArray = new Uint8Array(analyser.frequencyBinCount);
|
|
micActive = true;
|
|
btnMic.textContent = '停止麦克风';
|
|
statusMic.textContent = 'MIC: ACTIVE';
|
|
statusMic.classList.add('rec');
|
|
glitchTitle.style.opacity = '0';
|
|
initParticles();
|
|
} catch (e) {
|
|
statusMic.textContent = 'MIC: ACCESS DENIED';
|
|
statusMic.classList.add('off');
|
|
}
|
|
}
|
|
|
|
function stopMic() {
|
|
if (micStream) {
|
|
micStream.getTracks().forEach(t => t.stop());
|
|
micStream = null;
|
|
}
|
|
if (audioCtx) {
|
|
audioCtx.close();
|
|
audioCtx = null;
|
|
}
|
|
analyser = null;
|
|
source = null;
|
|
dataArray = null;
|
|
freqArray = null;
|
|
micActive = false;
|
|
btnMic.textContent = '启动麦克风';
|
|
statusMic.textContent = 'MIC: OFFLINE';
|
|
statusMic.classList.remove('rec');
|
|
glitchTitle.style.opacity = '1';
|
|
}
|
|
|
|
btnMic.addEventListener('click', toggleMic);
|
|
|
|
// --- 全屏 ---
|
|
btnFs.addEventListener('click', () => {
|
|
if (!document.fullscreenElement) {
|
|
document.documentElement.requestFullscreen();
|
|
btnFs.textContent = '退出全屏';
|
|
} else {
|
|
document.exitFullscreen();
|
|
btnFs.textContent = '全屏';
|
|
}
|
|
});
|
|
document.addEventListener('fullscreenchange', () => {
|
|
btnFs.textContent = document.fullscreenElement ? '退出全屏' : '全屏';
|
|
});
|
|
|
|
// --- 辅助函数 ---
|
|
function neonLine(x1, y1, x2, y2, color, glow) {
|
|
ctx.shadowBlur = glow;
|
|
ctx.shadowColor = color;
|
|
ctx.strokeStyle = color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
function neonCircle(x, y, r, color, glow, lw) {
|
|
ctx.shadowBlur = glow;
|
|
ctx.shadowColor = color;
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = lw;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
function lerp(a, b, t) { return a + (b - a) * t; }
|
|
|
|
// --- 粒子系统 ---
|
|
let particles = [];
|
|
class Particle {
|
|
constructor() { this.reset(); this.y = Math.random() * H; }
|
|
reset() {
|
|
this.x = Math.random() * W;
|
|
this.y = H + 10;
|
|
this.speed = 0.5 + Math.random() * 2.5;
|
|
this.size = 0.5 + Math.random() * 2;
|
|
this.life = 1;
|
|
this.freqIndex = Math.floor(Math.random() * 60);
|
|
this.color = Math.random() > 0.5 ? '#ff00ff' : '#00ffff';
|
|
this.vx = (Math.random() - 0.5) * 0.5;
|
|
}
|
|
update(intensity) {
|
|
let boost = 1;
|
|
if (freqArray && freqArray.length) {
|
|
boost = 1 + (freqArray[this.freqIndex] / 255) * intensity;
|
|
}
|
|
this.y -= this.speed * boost;
|
|
this.x += this.vx + Math.sin(this.y * 0.01) * 0.3;
|
|
if (this.y < -10) this.reset();
|
|
}
|
|
draw() {
|
|
ctx.shadowBlur = 8;
|
|
ctx.shadowColor = this.color;
|
|
ctx.fillStyle = this.color;
|
|
ctx.globalAlpha = 0.6;
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
}
|
|
|
|
function initParticles() {
|
|
particles = [];
|
|
for (let i = 0; i < 120; i++) particles.push(new Particle());
|
|
}
|
|
initParticles();
|
|
|
|
// --- 扫描线效果 ---
|
|
let scanY = 0;
|
|
|
|
// --- 静态噪声 ---
|
|
function drawNoise() {
|
|
const imgData = ctx.createImageData(W, 2);
|
|
for (let i = 0; i < imgData.data.length; i += 4) {
|
|
const v = Math.random() * 30;
|
|
imgData.data[i] = v;
|
|
imgData.data[i + 1] = v;
|
|
imgData.data[i + 2] = v;
|
|
imgData.data[i + 3] = Math.random() * 15;
|
|
}
|
|
ctx.putImageData(imgData, 0, Math.floor(Math.random() * H));
|
|
}
|
|
|
|
// --- 绘制透视网格 ---
|
|
function drawGrid(time) {
|
|
const horizon = H * 0.55;
|
|
const vanishX = W / 2;
|
|
ctx.strokeStyle = 'rgba(0, 255, 255, 0.08)';
|
|
ctx.lineWidth = 1;
|
|
ctx.shadowBlur = 0;
|
|
|
|
// 横线(透视)
|
|
for (let i = 0; i < 20; i++) {
|
|
const t = i / 20;
|
|
const perspective = t * t;
|
|
const y = horizon + (H - horizon) * perspective;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, y);
|
|
ctx.lineTo(W, y);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// 竖线(向消失点汇聚)
|
|
const numV = 24;
|
|
for (let i = 0; i <= numV; i++) {
|
|
const spread = (i / numV - 0.5) * 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(W / 2 + spread * W * 0.6, H);
|
|
ctx.lineTo(vanishX, horizon);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// 地平线上的霓虹线
|
|
ctx.shadowBlur = 15;
|
|
ctx.shadowColor = '#ff00ff';
|
|
ctx.strokeStyle = 'rgba(255, 0, 255, 0.5)';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, horizon);
|
|
ctx.lineTo(W, horizon);
|
|
ctx.stroke();
|
|
|
|
// 天空/远景的光柱
|
|
const numBeams = 8;
|
|
for (let i = 0; i < numBeams; i++) {
|
|
const bx = (W / (numBeams + 1)) * (i + 1);
|
|
const h = 80 + Math.sin(time * 0.001 + i) * 30 + (freqArray ? (freqArray[i * 8] / 255) * 100 : 0);
|
|
const grad = ctx.createLinearGradient(bx, horizon, bx, horizon - h);
|
|
grad.addColorStop(0, 'rgba(0, 255, 255, 0.3)');
|
|
grad.addColorStop(1, 'rgba(0, 255, 255, 0)');
|
|
ctx.shadowBlur = 20;
|
|
ctx.shadowColor = '#00ffff';
|
|
ctx.fillStyle = grad;
|
|
ctx.fillRect(bx - 2, horizon - h, 4, h);
|
|
}
|
|
}
|
|
|
|
// --- 底部频谱柱状图(霓虹城市天际线风格)---
|
|
function drawSkyline() {
|
|
if (!freqArray) return;
|
|
const binCount = analyser.frequencyBinCount;
|
|
const usable = binCount; // 覆盖全部频段,保留高频
|
|
const barCount = 80;
|
|
const barW = W / barCount;
|
|
const baseY = H * 0.9;
|
|
|
|
for (let i = 0; i < barCount; i++) {
|
|
const idx = Math.floor((i / barCount) * usable);
|
|
const val = freqArray[idx] / 255;
|
|
const barH = val * H * 0.45;
|
|
|
|
// 主柱
|
|
const hue = (i / barCount) * 60 + 260; // 紫到粉
|
|
const color1 = `hsl(${hue}, 100%, 55%)`;
|
|
ctx.shadowBlur = 15;
|
|
ctx.shadowColor = color1;
|
|
|
|
const grad = ctx.createLinearGradient(0, baseY - barH, 0, baseY);
|
|
grad.addColorStop(0, color1);
|
|
grad.addColorStop(1, 'rgba(0,0,0,0.5)');
|
|
ctx.fillStyle = grad;
|
|
ctx.fillRect(i * barW + 1, baseY - barH, barW - 2, barH);
|
|
|
|
// 顶部亮点
|
|
ctx.shadowBlur = 20;
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(i * barW + 1, baseY - barH - 2, barW - 2, 2);
|
|
}
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
// --- 中心旋转多边形核心 ---
|
|
function drawCore(time, bass, mid, treb) {
|
|
const cx = W / 2;
|
|
const cy = H * 0.45;
|
|
const baseR = 60;
|
|
const pulse = baseR + bass * 40;
|
|
|
|
ctx.save();
|
|
ctx.translate(cx, cy);
|
|
|
|
// 外层旋转三角环
|
|
for (let ring = 0; ring < 3; ring++) {
|
|
const r = pulse + ring * 30 + Math.sin(time * 0.002 + ring) * 10;
|
|
const sides = 3 + ring * 2;
|
|
const rot = time * 0.0005 * (ring % 2 === 0 ? 1 : -1);
|
|
|
|
ctx.shadowBlur = 20;
|
|
ctx.shadowColor = ring % 2 === 0 ? '#ff00ff' : '#00ffff';
|
|
ctx.strokeStyle = ring % 2 === 0 ? `rgba(255,0,255,${0.6 - ring * 0.15})` : `rgba(0,255,255,${0.6 - ring * 0.15})`;
|
|
ctx.lineWidth = 1.5;
|
|
ctx.beginPath();
|
|
for (let i = 0; i <= sides; i++) {
|
|
const angle = (i / sides) * Math.PI * 2 + rot;
|
|
const rr = r + Math.sin(angle * 3 + time * 0.003) * (5 + mid * 15);
|
|
const x = Math.cos(angle) * rr;
|
|
const y = Math.sin(angle) * rr;
|
|
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// --- 圆形频谱环 ---
|
|
function drawSpectrumRing(time, bass, mid, treb) {
|
|
const cx = W / 2;
|
|
const cy = H * 0.45;
|
|
if (!freqArray) {
|
|
neonCircle(cx, cy, 140, 'rgba(0, 255, 255, 0.15)', 5, 1);
|
|
return;
|
|
}
|
|
|
|
const binCount = analyser.frequencyBinCount;
|
|
const count = 128;
|
|
const baseR = 140;
|
|
const rot = time * 0.0003;
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const angle = (i / count) * Math.PI * 2 + rot;
|
|
const idx = Math.floor((i / count) * binCount * 0.5);
|
|
const val = freqArray[idx] / 255;
|
|
const len = val * 80;
|
|
|
|
const x1 = cx + Math.cos(angle) * baseR;
|
|
const y1 = cy + Math.sin(angle) * baseR;
|
|
const x2 = cx + Math.cos(angle) * (baseR + len);
|
|
const y2 = cy + Math.sin(angle) * (baseR + len);
|
|
|
|
const color = i % 2 === 0 ? '#00ffff' : '#ff00ff';
|
|
ctx.shadowBlur = 10;
|
|
ctx.shadowColor = color;
|
|
ctx.strokeStyle = color;
|
|
ctx.globalAlpha = 0.4 + val * 0.6;
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.stroke();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
// --- 顶部波形 ---
|
|
function drawWaveform() {
|
|
if (!dataArray) return;
|
|
analyser.getByteTimeDomainData(dataArray);
|
|
const waveY = H * 0.08;
|
|
const waveH = 40;
|
|
|
|
ctx.shadowBlur = 10;
|
|
ctx.shadowColor = '#00ffff';
|
|
ctx.strokeStyle = '#00ffff';
|
|
ctx.lineWidth = 1.5;
|
|
ctx.beginPath();
|
|
const step = W / dataArray.length;
|
|
for (let i = 0; i < dataArray.length; i++) {
|
|
const v = dataArray[i] / 128.0 - 1;
|
|
const x = i * step;
|
|
const y = waveY + v * waveH;
|
|
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
}
|
|
ctx.stroke();
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
// --- 侧边频谱柱 ---
|
|
function drawSideBars() {
|
|
if (!freqArray) return;
|
|
const count = 24;
|
|
const binCount = analyser.frequencyBinCount;
|
|
const barH = 6;
|
|
const gap = 4;
|
|
const maxW = 100;
|
|
|
|
// 左侧
|
|
for (let i = 0; i < count; i++) {
|
|
const idx = Math.floor((i / count) * binCount * 0.4);
|
|
const val = freqArray[idx] / 255;
|
|
const w = val * maxW;
|
|
const y = H * 0.15 + i * (barH + gap);
|
|
ctx.shadowBlur = 8;
|
|
ctx.shadowColor = '#ff00ff';
|
|
ctx.fillStyle = `rgba(255, 0, 255, ${0.3 + val * 0.7})`;
|
|
ctx.fillRect(20, y, w, barH);
|
|
}
|
|
|
|
// 右侧(镜像)
|
|
for (let i = 0; i < count; i++) {
|
|
const idx = Math.floor((i / count) * binCount * 0.4) + 10;
|
|
const val = freqArray[idx] / 255;
|
|
const w = val * maxW;
|
|
const y = H * 0.15 + i * (barH + gap);
|
|
ctx.shadowBlur = 8;
|
|
ctx.shadowColor = '#00ffff';
|
|
ctx.fillStyle = `rgba(0, 255, 255, ${0.3 + val * 0.7})`;
|
|
ctx.fillRect(W - 20 - w, y, w, barH);
|
|
}
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
// --- 扫描线 ---
|
|
function drawScanlines() {
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.04)';
|
|
for (let y = 0; y < H; y += 3) {
|
|
ctx.fillRect(0, y, W, 1);
|
|
}
|
|
// 移动扫描线
|
|
scanY = (scanY + 2) % H;
|
|
const grad = ctx.createLinearGradient(0, scanY - 80, 0, scanY + 80);
|
|
grad.addColorStop(0, 'rgba(0, 255, 255, 0)');
|
|
grad.addColorStop(0.5, 'rgba(0, 255, 255, 0.04)');
|
|
grad.addColorStop(1, 'rgba(0, 255, 255, 0)');
|
|
ctx.fillStyle = grad;
|
|
ctx.fillRect(0, scanY - 80, W, 160);
|
|
}
|
|
|
|
// --- 故障效果(偶尔触发)---
|
|
let glitchTimer = 0;
|
|
let glitchActive = false;
|
|
let glitchDuration = 0;
|
|
function drawGlitch(time) {
|
|
glitchTimer++;
|
|
if (!glitchActive && Math.random() < 0.001) {
|
|
glitchActive = true;
|
|
glitchDuration = 5 + Math.floor(Math.random() * 15);
|
|
}
|
|
if (glitchActive) {
|
|
glitchDuration--;
|
|
if (glitchDuration <= 0) glitchActive = false;
|
|
// RGB 偏移切片
|
|
const slices = 3;
|
|
for (let s = 0; s < slices; s++) {
|
|
const sy = Math.floor(Math.random() * H);
|
|
const sh = 5 + Math.floor(Math.random() * 30);
|
|
const offset = (Math.random() - 0.5) * 20;
|
|
const slice = ctx.getImageData(0, sy, W, sh);
|
|
ctx.putImageData(slice, offset, sy);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 计算频段能量 ---
|
|
function getEnergy() {
|
|
if (!freqArray) return { bass: 0, mid: 0, treb: 0, vol: 0 };
|
|
const binCount = freqArray.length;
|
|
let bass = 0, mid = 0, treb = 0, vol = 0;
|
|
const bassEnd = Math.floor(binCount * 0.08);
|
|
const midEnd = Math.floor(binCount * 0.35);
|
|
for (let i = 0; i < bassEnd; i++) bass += freqArray[i];
|
|
for (let i = bassEnd; i < midEnd; i++) mid += freqArray[i];
|
|
for (let i = midEnd; i < binCount; i++) treb += freqArray[i];
|
|
bass /= (bassEnd * 255);
|
|
mid /= ((midEnd - bassEnd) * 255);
|
|
treb /= ((binCount - midEnd) * 255);
|
|
vol = (bass + mid + treb) / 3;
|
|
return { bass, mid, treb, vol };
|
|
}
|
|
|
|
// --- FPS 计算 ---
|
|
let lastTime = performance.now();
|
|
let frameCount = 0;
|
|
let fpsValue = 0;
|
|
let fpsTimer = 0;
|
|
|
|
// --- 主循环 ---
|
|
function frame(time) {
|
|
requestAnimationFrame(frame);
|
|
|
|
// FPS
|
|
frameCount++;
|
|
fpsTimer += time - lastTime;
|
|
lastTime = time;
|
|
if (fpsTimer >= 1000) {
|
|
fpsValue = frameCount;
|
|
frameCount = 0;
|
|
fpsTimer = 0;
|
|
statusFps.textContent = 'FPS: ' + fpsValue;
|
|
}
|
|
|
|
// 获取音频数据
|
|
if (analyser && freqArray) {
|
|
analyser.getByteFrequencyData(freqArray);
|
|
}
|
|
|
|
// 清屏(带拖尾)
|
|
ctx.fillStyle = 'rgba(5, 1, 13, 0.25)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
const energy = getEnergy();
|
|
statusVol.textContent = 'VOL: ' + Math.floor(energy.vol * 100) + '%';
|
|
|
|
// 绘制各层
|
|
drawGrid(time);
|
|
drawSkyline();
|
|
|
|
// 粒子
|
|
for (const p of particles) {
|
|
p.update(3);
|
|
p.draw();
|
|
}
|
|
|
|
drawSpectrumRing(time, energy.bass, energy.mid, energy.treb);
|
|
drawCore(time, energy.bass, energy.mid, energy.treb);
|
|
drawWaveform();
|
|
drawSideBars();
|
|
|
|
// 中心十字准星
|
|
const cx = W / 2, cy = H * 0.45;
|
|
ctx.strokeStyle = 'rgba(0, 255, 255, 0.2)';
|
|
ctx.lineWidth = 1;
|
|
ctx.shadowBlur = 0;
|
|
ctx.beginPath();
|
|
ctx.moveTo(cx - 200, cy); ctx.lineTo(cx - 100, cy);
|
|
ctx.moveTo(cx + 100, cy); ctx.lineTo(cx + 200, cy);
|
|
ctx.moveTo(cx, cy - 200); ctx.lineTo(cx, cy - 100);
|
|
ctx.moveTo(cx, cy + 100); ctx.lineTo(cx, cy + 200);
|
|
ctx.stroke();
|
|
|
|
drawScanlines();
|
|
drawNoise();
|
|
drawGlitch(time);
|
|
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
requestAnimationFrame(frame);
|
|
</script>
|
|
</body>
|
|
</html>
|