Files

995 lines
28 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>音频可视化 - Audio Visualizer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
overflow: hidden;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
color: #fff;
}
canvas {
display: block;
position: fixed;
top: 0;
left: 0;
}
/* 控制面板 */
.controls {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 6px;
align-items: center;
z-index: 100;
padding: 6px 12px;
}
.btn {
padding: 6px 14px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 400;
transition: all 0.2s ease;
background: transparent;
color: rgba(255, 255, 255, 0.55);
outline: none;
letter-spacing: 0.5px;
}
.btn:hover {
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.08);
}
.btn:active {
color: #fff;
}
.btn.active {
color: #fff;
background: rgba(255, 255, 255, 0.12);
}
.btn.mic-btn.active {
color: #f5576c;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
input[type="file"] {
display: none;
}
/* 模式切换 */
.mode-switch {
display: flex;
gap: 2px;
align-items: center;
}
.mode-label {
font-size: 12px;
opacity: 0.35;
margin-right: 6px;
letter-spacing: 1px;
}
.mode-btn {
padding: 6px 10px;
border: none;
border-radius: 4px;
background: transparent;
color: rgba(255, 255, 255, 0.4);
cursor: pointer;
font-size: 12px;
font-weight: 400;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
letter-spacing: 0.5px;
}
.mode-btn:hover {
color: rgba(255, 255, 255, 0.75);
background: rgba(255, 255, 255, 0.06);
}
.mode-btn.active {
color: #fff;
background: rgba(255, 255, 255, 0.1);
}
/* 标题 */
.title {
position: fixed;
top: 24px;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
font-weight: 400;
letter-spacing: 6px;
text-transform: uppercase;
z-index: 100;
color: rgba(255, 255, 255, 0.3);
user-select: none;
}
/* 音量条 */
.volume-bar {
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 120px;
background: rgba(255, 255, 255, 0.08);
border-radius: 2px;
overflow: hidden;
z-index: 100;
}
.volume-fill {
position: absolute;
bottom: 0;
width: 100%;
background: rgba(255, 255, 255, 0.4);
border-radius: 2px;
transition: height 0.1s ease;
}
/* 提示文字 */
.hint {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 14px;
opacity: 0.25;
text-align: center;
z-index: 50;
pointer-events: none;
transition: opacity 0.5s ease;
letter-spacing: 3px;
font-weight: 300;
}
.hint.hidden {
opacity: 0;
}
/* 音频信息 */
.track-info {
position: fixed;
top: 55px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
opacity: 0;
z-index: 100;
text-align: center;
transition: opacity 0.5s ease;
color: rgba(255, 255, 255, 0.4);
}
.track-info.visible {
opacity: 1;
}
/* 播放进度 */
.progress-container {
position: fixed;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
width: 240px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s ease;
}
.progress-container.visible {
opacity: 1;
}
.progress-bar {
width: 100%;
height: 2px;
background: rgba(255, 255, 255, 0.1);
border-radius: 1px;
cursor: pointer;
position: relative;
}
.progress-fill {
height: 100%;
background: rgba(255, 255, 255, 0.5);
border-radius: 1px;
width: 0%;
transition: width 0.1s linear;
}
.time-display {
display: flex;
justify-content: space-between;
font-size: 10px;
opacity: 0.3;
margin-top: 4px;
}
/* 分割线 */
.divider {
width: 1px;
height: 14px;
background: rgba(255, 255, 255, 0.12);
margin: 0 4px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="title">AUDIO VISUALIZER</div>
<div class="track-info" id="trackInfo"></div>
<div class="hint" id="hint">
请选择音频文件或开启麦克风
</div>
<div class="volume-bar">
<div class="volume-fill" id="volumeFill"></div>
</div>
<div class="progress-container" id="progressContainer">
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="time-display">
<span id="currentTime">0:00</span>
<span id="totalTime">0:00</span>
</div>
</div>
<div class="controls">
<input type="file" id="fileInput" accept="audio/*">
<button class="btn" id="uploadBtn">打开文件</button>
<button class="btn" id="micBtn">麦克风</button>
<div class="divider"></div>
<button class="btn" id="playBtn" style="display:none;">播放</button>
<div class="divider"></div>
<div class="mode-switch">
<button class="mode-btn active" data-mode="0">频谱</button>
<button class="mode-btn" data-mode="1">波纹</button>
<button class="mode-btn" data-mode="2">粒子</button>
<button class="mode-btn" data-mode="3">组合</button>
</div>
<div class="divider"></div>
<button class="btn" id="fullscreenBtn">全屏</button>
</div>
<script>
// ==================== 初始化 ====================
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let width, height, centerX, centerY;
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
centerX = width / 2;
centerY = height / 2;
}
resize();
window.addEventListener('resize', resize);
// ==================== 音频相关 ====================
let audioContext;
let analyser;
let source;
let audio = new Audio();
let dataArray;
let bufferLength;
let isPlaying = false;
let isMicActive = false;
let micStream;
let currentMode = 0;
let animationId;
// 粒子系统
let particles = [];
let stars = [];
// 初始化星空背景
function initStars() {
stars = [];
const count = 200;
for (let i = 0; i < count; i++) {
stars.push({
x: Math.random() * width,
y: Math.random() * height,
radius: Math.random() * 1.5 + 0.5,
opacity: Math.random() * 0.8 + 0.2,
speed: Math.random() * 0.5 + 0.1,
twinkleSpeed: Math.random() * 0.02 + 0.01
});
}
}
initStars();
// 初始化音频上下文
function initAudio() {
if (audioContext) return;
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
analyser.smoothingTimeConstant = 0.82;
bufferLength = analyser.frequencyBinCount;
dataArray = new Uint8Array(bufferLength);
}
// 播放音频文件
function playFile(file) {
initAudio();
if (audioContext.state === 'suspended') {
audioContext.resume();
}
stopMic();
const url = URL.createObjectURL(file);
audio.src = url;
if (source) {
source.disconnect();
}
source = audioContext.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioContext.destination);
audio.play();
isPlaying = true;
document.getElementById('playBtn').style.display = 'inline-block';
document.getElementById('playBtn').textContent = '暂停';
document.getElementById('hint').classList.add('hidden');
document.getElementById('progressContainer').classList.add('visible');
document.getElementById('trackInfo').classList.add('visible');
document.getElementById('trackInfo').textContent = file.name.replace(/\.[^/.]+$/, '');
audio.addEventListener('loadedmetadata', () => {
document.getElementById('totalTime').textContent = formatTime(audio.duration);
});
}
// 使用麦克风
async function toggleMic() {
if (isMicActive) {
stopMic();
return;
}
initAudio();
if (audioContext.state === 'suspended') {
audioContext.resume();
}
try {
micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
audio.pause();
isPlaying = false;
if (source) source.disconnect();
source = audioContext.createMediaStreamSource(micStream);
source.connect(analyser);
isMicActive = true;
document.getElementById('micBtn').classList.add('active');
document.getElementById('hint').classList.add('hidden');
document.getElementById('playBtn').style.display = 'none';
document.getElementById('progressContainer').classList.remove('visible');
document.getElementById('trackInfo').classList.add('visible');
document.getElementById('trackInfo').textContent = '麦克风输入';
} catch (err) {
alert('无法访问麦克风,请检查权限设置。');
console.error(err);
}
}
function stopMic() {
if (micStream) {
micStream.getTracks().forEach(track => track.stop());
micStream = null;
}
isMicActive = false;
document.getElementById('micBtn').classList.remove('active');
}
// 格式化时间
function formatTime(seconds) {
if (isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// ==================== 粒子类 ====================
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.color = color;
this.vx = (Math.random() - 0.5) * 5;
this.vy = (Math.random() - 0.5) * 5;
this.life = 1;
this.decay = Math.random() * 0.02 + 0.012;
this.size = Math.random() * 3 + 1.5;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vx *= 0.98;
this.vy *= 0.98;
this.life -= this.decay;
this.size *= 0.99;
}
draw() {
ctx.save();
ctx.globalAlpha = this.life;
ctx.fillStyle = this.color;
ctx.shadowBlur = 8;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
// ==================== 颜色工具 ====================
function hslToColor(h, s, l, alpha) {
return `hsla(${h}, ${s}%, ${l}%, ${alpha})`;
}
// ==================== 可视化模式 ====================
// 模式0频谱柱状图居中对称 + 镜像)
function drawSpectrum() {
const barCount = bufferLength; // 使用全部频段 bin保留高频
const barWidth = (width * 0.8) / barCount;
const gap = 2;
const maxBarHeight = height * 0.35;
const startX = width * 0.1;
const baseY = centerY + 50;
let totalVolume = 0;
for (let i = 0; i < barCount; i++) {
const value = dataArray[i];
const percent = value / 255;
totalVolume += percent;
const barHeight = percent * maxBarHeight;
const x = startX + i * barWidth;
const hue = (i / barCount) * 280 + 180; // 蓝到紫到红
const saturation = 80 + percent * 20;
const lightness = 50 + percent * 15;
const color = hslToColor(hue, saturation, lightness, 0.9);
const glowColor = hslToColor(hue, saturation, lightness, 0.3);
// 上半部分
ctx.save();
ctx.fillStyle = color;
ctx.shadowBlur = 15;
ctx.shadowColor = glowColor;
const w = barWidth - gap;
ctx.fillRect(x, baseY - barHeight, w, barHeight);
// 镜像(下半部分,稍暗)
ctx.globalAlpha = 0.3;
ctx.fillRect(x, baseY, w, barHeight * 0.5);
ctx.restore();
// 顶部亮块
if (barHeight > 5) {
ctx.save();
ctx.fillStyle = hslToColor(hue, saturation, lightness + 20, 1);
ctx.shadowBlur = 20;
ctx.shadowColor = hslToColor(hue, 90, 70, 1);
ctx.fillRect(x, baseY - barHeight - 3, w, 3);
ctx.restore();
}
}
// 粒子喷发
const avgVolume = totalVolume / barCount;
if (avgVolume > 0.5 && Math.random() < avgVolume * 0.2) {
const randomI = Math.floor(Math.random() * barCount);
const rx = startX + randomI * barWidth;
const rhue = (randomI / barCount) * 280 + 180;
particles.push(new Particle(rx, baseY - (dataArray[randomI] / 255) * maxBarHeight,
hslToColor(rhue, 90, 60, 1)));
}
return totalVolume / barCount;
}
// 模式1圆形波纹
function drawCircle() {
const radius = Math.min(width, height) * 0.2;
const bars = 180;
const step = (Math.PI * 2) / bars;
let totalVolume = 0;
for (let i = 0; i < bars; i++) {
const dataIndex = Math.floor((i / bars) * (bufferLength / 2));
const value = dataArray[dataIndex];
const percent = value / 255;
totalVolume += percent;
const barLength = percent * radius * 1.2 + 5;
const angle = i * step - Math.PI / 2;
const innerX = centerX + Math.cos(angle) * radius;
const innerY = centerY + Math.sin(angle) * radius;
const outerX = centerX + Math.cos(angle) * (radius + barLength);
const outerY = centerY + Math.sin(angle) * (radius + barLength);
const hue = (i / bars) * 360;
const color = hslToColor(hue, 90, 50 + percent * 20, 0.8);
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.shadowBlur = 10;
ctx.shadowColor = color;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(innerX, innerY);
ctx.lineTo(outerX, outerY);
ctx.stroke();
ctx.restore();
}
// 内圆
const avgVolume = totalVolume / bars;
const innerRadius = radius * 0.7 + avgVolume * 30;
ctx.save();
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, innerRadius);
gradient.addColorStop(0, 'rgba(102, 126, 234, 0.1)');
gradient.addColorStop(0.5, 'rgba(118, 75, 162, 0.05)');
gradient.addColorStop(1, 'rgba(102, 126, 234, 0.01)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(centerX, centerY, innerRadius, 0, Math.PI * 2);
ctx.fill();
// 内圆环
ctx.strokeStyle = `rgba(102, 126, 234, ${0.3 + avgVolume * 0.4})`;
ctx.lineWidth = 2;
ctx.shadowBlur = 20;
ctx.shadowColor = 'rgba(102, 126, 234, 0.8)';
ctx.beginPath();
ctx.arc(centerX, centerY, innerRadius, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
// 波形环
ctx.save();
ctx.strokeStyle = 'rgba(240, 147, 251, 0.4)';
ctx.lineWidth = 2;
ctx.shadowBlur = 15;
ctx.shadowColor = 'rgba(240, 147, 251, 0.6)';
ctx.beginPath();
for (let i = 0; i <= bufferLength; i++) {
const angle = (i / bufferLength) * Math.PI * 2 - Math.PI / 2;
const waveR = innerRadius * 0.85 + (dataArray[i % bufferLength] / 255) * 20;
const wx = centerX + Math.cos(angle) * waveR;
const wy = centerY + Math.sin(angle) * waveR;
if (i === 0) ctx.moveTo(wx, wy);
else ctx.lineTo(wx, wy);
}
ctx.closePath();
ctx.stroke();
ctx.restore();
// 粒子
if (avgVolume > 0.4 && Math.random() < avgVolume * 0.15) {
const angle = Math.random() * Math.PI * 2;
const pr = innerRadius + Math.random() * 20;
const px = centerX + Math.cos(angle) * pr;
const py = centerY + Math.sin(angle) * pr;
const hue = (angle / (Math.PI * 2)) * 360;
particles.push(new Particle(px, py, hslToColor(hue, 90, 60, 1)));
}
return avgVolume;
}
// 模式2粒子星空
function drawParticles() {
let totalVolume = 0;
let bass = 0, mid = 0, treble = 0;
for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 255;
totalVolume += v;
if (i < bufferLength * 0.15) bass += v;
else if (i < bufferLength * 0.5) mid += v;
else treble += v;
}
const avgVolume = totalVolume / bufferLength;
bass /= bufferLength * 0.15;
mid /= bufferLength * 0.35;
treble /= bufferLength * 0.5;
// 中心光晕
const glowRadius = 100 + bass * 200;
ctx.save();
const glow = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, glowRadius);
glow.addColorStop(0, `rgba(245, 87, 108, ${bass * 0.3})`);
glow.addColorStop(0.3, `rgba(240, 147, 251, ${mid * 0.15})`);
glow.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = glow;
ctx.fillRect(0, 0, width, height);
ctx.restore();
// 生成新粒子(跟随低音)
const particleCount = Math.floor(bass * 4) + 1;
for (let i = 0; i < particleCount; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 2 + bass * 6 + Math.random() * 3;
const hue = treble > mid ? 180 + Math.random() * 120 : 260 + Math.random() * 100;
const p = new Particle(centerX, centerY, hslToColor(hue, 90, 60, 1));
p.vx = Math.cos(angle) * speed;
p.vy = Math.sin(angle) * speed;
p.decay = 0.012 + Math.random() * 0.012;
p.size = 2 + bass * 3 + Math.random() * 2;
particles.push(p);
}
// 连接线(中音强时,限制检测数量优化性能)
if (mid > 0.3) {
const maxDist = 80 + mid * 60;
const maxDistSq = maxDist * maxDist;
const lineLimit = Math.min(particles.length, 80);
ctx.save();
for (let i = 0; i < lineLimit; i++) {
// 只与后续少量粒子比较,避免 O(n²) 过大
const checkLimit = Math.min(i + 12, lineLimit);
for (let j = i + 1; j < checkLimit; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distSq = dx * dx + dy * dy;
if (distSq < maxDistSq) {
const dist = Math.sqrt(distSq);
ctx.strokeStyle = `rgba(102, 126, 234, ${(1 - dist / maxDist) * mid * 0.25})`;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
}
ctx.restore();
}
return avgVolume;
}
// 模式3组合模式圆形 + 频谱 + 粒子)
function drawCombined() {
// 底部频谱
const barCount = 64;
const barWidth = width / barCount;
const maxHeight = height * 0.2;
let totalVolume = 0;
for (let i = 0; i < barCount; i++) {
const dataIndex = Math.floor((i / barCount) * (bufferLength / 2));
const value = dataArray[dataIndex];
const percent = value / 255;
totalVolume += percent;
const barHeight = percent * maxHeight;
const x = i * barWidth;
const hue = (i / barCount) * 60 + 200;
ctx.save();
const gradient = ctx.createLinearGradient(0, height - barHeight, 0, height);
gradient.addColorStop(0, hslToColor(hue, 90, 60, 0.8));
gradient.addColorStop(1, hslToColor(hue, 90, 30, 0.2));
ctx.fillStyle = gradient;
ctx.shadowBlur = 10;
ctx.shadowColor = hslToColor(hue, 90, 60, 0.5);
ctx.fillRect(x + 1, height - barHeight, barWidth - 2, barHeight);
ctx.restore();
}
// 中心圆形
const radius = Math.min(width, height) * 0.12;
const avgVolume = totalVolume / barCount;
const circleBars = 120;
ctx.save();
for (let i = 0; i < circleBars; i++) {
const dataIndex = Math.floor((i / circleBars) * (bufferLength / 2));
const value = dataArray[dataIndex];
const percent = value / 255;
const barLength = percent * radius * 1.5 + 3;
const angle = (i / circleBars) * Math.PI * 2 - Math.PI / 2 + performance.now() * 0.0003;
const x1 = centerX + Math.cos(angle) * radius;
const y1 = centerY - 30 + Math.sin(angle) * radius;
const x2 = centerX + Math.cos(angle) * (radius + barLength);
const y2 = centerY - 30 + Math.sin(angle) * (radius + barLength);
const hue = (i / circleBars) * 360 + performance.now() * 0.05;
ctx.strokeStyle = hslToColor(hue % 360, 90, 55 + percent * 15, 0.7);
ctx.lineWidth = 2;
ctx.shadowBlur = 8;
ctx.shadowColor = hslToColor(hue % 360, 90, 60, 0.5);
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.restore();
// 中心圆
const coreR = radius * 0.6 + avgVolume * 25;
ctx.save();
const coreGrad = ctx.createRadialGradient(centerX, centerY - 30, 0, centerX, centerY - 30, coreR);
coreGrad.addColorStop(0, `rgba(255, 255, 255, ${0.3 + avgVolume * 0.3})`);
coreGrad.addColorStop(0.3, 'rgba(240, 147, 251, 0.3)');
coreGrad.addColorStop(1, 'rgba(102, 126, 234, 0)');
ctx.fillStyle = coreGrad;
ctx.beginPath();
ctx.arc(centerX, centerY - 30, coreR, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 粒子
if (avgVolume > 0.4 && Math.random() < avgVolume * 0.2) {
const angle = Math.random() * Math.PI * 2;
const pr = radius + Math.random() * 30;
const px = centerX + Math.cos(angle) * pr;
const py = centerY - 30 + Math.sin(angle) * pr;
const hue = Math.random() * 360;
particles.push(new Particle(px, py, hslToColor(hue, 90, 65, 1)));
}
return avgVolume;
}
// ==================== 绘制星空背景 ====================
function drawStars(time) {
for (const star of stars) {
const twinkle = Math.sin(time * star.twinkleSpeed + star.x) * 0.3 + 0.7;
ctx.save();
ctx.globalAlpha = star.opacity * twinkle;
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 缓慢移动
star.y += star.speed;
if (star.y > height) {
star.y = 0;
star.x = Math.random() * width;
}
}
}
// ==================== 主渲染循环 ====================
function render(time) {
animationId = requestAnimationFrame(render);
// 清屏(带拖影效果)
ctx.fillStyle = 'rgba(0, 0, 8, 0.15)';
ctx.fillRect(0, 0, width, height);
// 星空
drawStars(time);
// 获取音频数据
let avgVolume = 0;
if (analyser) {
analyser.getByteFrequencyData(dataArray);
} else {
// 无音频时的默认动画
dataArray = new Uint8Array(bufferLength || 128);
for (let i = 0; i < dataArray.length; i++) {
dataArray[i] = Math.sin(time * 0.002 + i * 0.1) * 30 + 30;
}
bufferLength = dataArray.length;
}
// 绘制当前模式
switch (currentMode) {
case 0: avgVolume = drawSpectrum(); break;
case 1: avgVolume = drawCircle(); break;
case 2: avgVolume = drawParticles(); break;
case 3: avgVolume = drawCombined(); break;
}
// 更新和绘制粒子
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
particles[i].draw();
if (particles[i].life <= 0) {
particles.splice(i, 1);
}
}
// 限制粒子数量
if (particles.length > 200) {
particles = particles.slice(-200);
}
// 更新音量条
const fill = document.getElementById('volumeFill');
if (fill) {
fill.style.height = `${Math.min(avgVolume * 100 * 2.5, 100)}%`;
}
// 更新进度条
if (!isMicActive && audio.duration) {
const progress = (audio.currentTime / audio.duration) * 100;
document.getElementById('progressFill').style.width = `${progress}%`;
document.getElementById('currentTime').textContent = formatTime(audio.currentTime);
}
}
// ==================== 事件绑定 ====================
// 文件上传
document.getElementById('uploadBtn').addEventListener('click', () => {
document.getElementById('fileInput').click();
});
document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
playFile(file);
}
});
// 麦克风
document.getElementById('micBtn').addEventListener('click', toggleMic);
// 播放/暂停
document.getElementById('playBtn').addEventListener('click', () => {
if (isPlaying) {
audio.pause();
isPlaying = false;
document.getElementById('playBtn').textContent = '播放';
} else {
audio.play();
isPlaying = true;
document.getElementById('playBtn').textContent = '暂停';
}
});
audio.addEventListener('ended', () => {
isPlaying = false;
document.getElementById('playBtn').textContent = '播放';
});
// 模式切换
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentMode = parseInt(btn.dataset.mode);
particles = []; // 切换模式时清空粒子
});
});
// 进度条点击跳转
document.getElementById('progressBar').addEventListener('click', (e) => {
if (!audio.duration) return;
const rect = e.currentTarget.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
audio.currentTime = percent * audio.duration;
});
// 拖拽文件
document.addEventListener('dragover', (e) => {
e.preventDefault();
});
document.addEventListener('drop', (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('audio/')) {
playFile(file);
}
});
// 全屏切换
const fullscreenBtn = document.getElementById('fullscreenBtn');
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => { });
fullscreenBtn.textContent = '退出全屏';
} else {
document.exitFullscreen();
fullscreenBtn.textContent = '全屏';
}
}
fullscreenBtn.addEventListener('click', toggleFullscreen);
document.addEventListener('fullscreenchange', () => {
fullscreenBtn.textContent = document.fullscreenElement ? '退出全屏' : '全屏';
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
if (!isMicActive && audio.src) {
document.getElementById('playBtn').click();
}
} else if (e.code === 'KeyF') {
toggleFullscreen();
} else if (e.code >= 'Digit1' && e.code <= 'Digit4') {
const mode = parseInt(e.code.slice(-1)) - 1;
document.querySelectorAll('.mode-btn')[mode]?.click();
}
});
// 启动渲染
render(0);
</script>
</body>
</html>