console
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>平滑过渡音频可视化器</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: linear-gradient(135deg, #121212, #1a1a2e);
color: #f5f5f5;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 20px;
overflow-x: hidden;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
max-width: 900px;
width: 100%;
}
header {
text-align: center;
margin-bottom: 15px;
width: 100%;
}
.title {
font-size: 2.8rem;
background: linear-gradient(to right, #FF5722, #FF9800, #FFC107);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 800;
letter-spacing: 1px;
margin-bottom: 8px;
text-shadow: 0 0 15px rgba(255, 152, 0, 0.3);
}
.subtitle {
color: #90caf9;
font-size: 1.1rem;
max-width: 600px;
margin: 0 auto;
line-height: 1.6;
}
.visualizer-container {
position: relative;
border-radius: 16px;
width: 100%;
height: 300px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(20, 25, 45, 0.7);
overflow: hidden;
margin-bottom: 10px;
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.5),
inset 0 0 20px rgba(255, 152, 0, 0.1);
border: 1px solid rgba(255, 152, 0, 0.2);
}
.center-line {
position: absolute;
width: 100%;
height: 1px;
background: rgba(255, 255, 255, 0.15);
top: 50%;
left: 0;
z-index: 1;
}
#audioPlayer {
width: 100%;
border-radius: 50px;
background: rgba(30, 40, 70, 0.7);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
outline: none;
}
.control-panel {
display: flex;
flex-wrap: wrap;
gap: 15px;
background: rgba(20, 25, 45, 0.8);
padding: 25px;
border-radius: 16px;
width: 100%;
margin-top: 15px;
border: 1px solid rgba(255, 152, 0, 0.3);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
}
.control-column {
flex: 1;
min-width: 280px;
display: flex;
flex-direction: column;
gap: 15px;
}
.control-group {
display: flex;
flex-direction: column;
background: rgba(30, 35, 55, 0.6);
padding: 15px;
border-radius: 12px;
border: 1px solid rgba(255, 152, 0, 0.2);
}
.group-header {
display: flex;
align-items: center;
margin-bottom: 12px;
color: #FF9800;
font-size: 1.2rem;
font-weight: 600;
}
.control-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.control-row:last-child {
margin-bottom: 0;
}
label {
flex: 1;
font-size: 1rem;
color: #e0e0e0;
min-width: 120px;
}
input[type="range"] {
flex: 2;
height: 8px;
background: rgba(255, 152, 0, 0.15);
border-radius: 4px;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: #FF9800;
cursor: pointer;
box-shadow: 0 0 10px rgba(255, 152, 0, 0.7);
transition: all 0.2s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.15);
box-shadow: 0 0 15px rgba(255, 152, 0, 0.9);
}
.value-display {
min-width: 70px;
text-align: center;
background: rgba(255, 152, 0, 0.15);
padding: 8px 12px;
border-radius: 20px;
font-size: 0.95rem;
font-weight: 600;
color: #FFD180;
margin-left: 15px;
}
.toggle-group {
display: flex;
align-items: center;
gap: 15px;
}
.color-picker-group {
display: flex;
gap: 15px;
align-items: center;
}
.color-picker {
width: 45px;
height: 45px;
border-radius: 10px;
border: 2px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
background: #FF9800;
transition: all 0.3s;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.color-picker:hover {
transform: scale(1.1);
box-shadow: 0 0 15px rgba(255, 152, 0, 0.7);
}
.color-picker input {
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.preview-box {
width: 100%;
height: 80px;
background: rgba(30, 35, 55, 0.7);
border-radius: 12px;
margin-top: 15px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.preview-bar {
width: 60%;
height: 40px;
background: #FF9800;
position: relative;
z-index: 2;
transition: all 0.4s ease;
}
.presets {
display: flex;
gap: 12px;
margin-top: 15px;
flex-wrap: wrap;
}
.preset-btn {
padding: 10px 18px;
background: rgba(255, 152, 0, 0.1);
border: 1px solid rgba(255, 152, 0, 0.3);
color: #FF9800;
border-radius: 30px;
cursor: pointer;
transition: all 0.3s;
font-size: 0.9rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.preset-btn:hover {
background: rgba(255, 152, 0, 0.25);
transform: translateY(-2px);
}
.mode-toggle {
display: flex;
gap: 15px;
margin-top: 10px;
flex-wrap: wrap;
}
.mode-btn {
flex: 1;
padding: 14px;
background: rgba(255, 152, 0, 0.08);
border: 1px solid rgba(255, 152, 0, 0.3);
color: #FF9800;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
font-size: 1.05rem;
text-align: center;
min-width: 150px;
}
.mode-btn:hover {
background: rgba(255, 152, 0, 0.2);
}
.mode-btn.active {
background: linear-gradient(to right, rgba(255, 87, 34, 0.3), rgba(255, 193, 7, 0.3));
box-shadow: 0 0 15px rgba(255, 152, 0, 0.4);
border-color: rgba(255, 152, 0, 0.5);
}
.footer {
margin-top: 25px;
color: #82b1ff;
font-size: 0.9rem;
text-align: center;
padding: 15px;
width: 100%;
border-top: 1px solid rgba(255, 152, 0, 0.1);
}
canvas {
display: block;
}
.bubble {
position: absolute;
border-radius: 50%;
background: rgba(255, 152, 0, 0.05);
z-index: -1;
}
@media (max-width: 768px) {
.control-column {
min-width: 100%;
}
.title {
font-size: 2.2rem;
}
.visualizer-container {
height: 250px;
}
.mode-btn {
min-width: 130px;
padding: 12px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="title">平滑过渡音频可视化器</h1>
<p class="subtitle">优化的渐变色过渡与精细控制</p>
</header>
<div class="visualizer-container" id="visualizerContainer">
<div class="center-line"></div>
<canvas id="visualizer"></canvas>
</div>
<audio id="audioPlayer" controls>
<source src="./mp3/Ugress - Undead Funeral March.mp3" type="audio/mp3">
您的浏览器不支持 audio 元素
</audio>
<div class="control-panel">
<div class="control-column">
<div class="control-group">
<div class="group-header">形状控制</div>
<div class="control-row">
<label>柱子数量:</label>
<input type="range" id="barCountSlider" min="6" max="64" value="32">
<span id="barCountValue" class="value-display">32</span>
</div>
<div class="control-row">
<label>柱子间距:</label>
<input type="range" id="spacingSlider" min="0" max="20" value="2">
<span id="spacingValue" class="value-display">2px</span>
</div>
<div class="control-row">
<label>基础高度:</label>
<input type="range" id="baseHeightSlider" min="0" max="40" value="8">
<span id="baseHeightValue" class="value-display">8px</span>
</div>
</div>
<div class="control-group">
<div class="group-header">圆角效果</div>
<div class="control-row">
<label>圆角半径:</label>
<input type="range" id="borderRadiusSlider" min="0" max="50" value="4">
<span id="borderRadiusValue" class="value-display">4px</span>
</div>
<div class="control-row">
<label>圆端效果:</label>
<input type="range" id="roundnessSlider" min="0" max="100" value="0">
<span id="roundnessValue" class="value-display">0%</span>
</div>
<div class="preview-box">
<div class="preview-bar" id="previewBar"></div>
</div>
</div>
</div>
<div class="control-column">
<div class="control-group">
<div class="group-header">渐变控制</div>
<div class="control-row">
<label>主色:</label>
<div class="color-picker" style="background: #FF9800;">
<input type="color" id="primaryColorPicker" value="#FF9800">
</div>
</div>
<div class="control-row">
<label>次色:</label>
<div class="color-picker" style="background: #FF6D00;">
<input type="color" id="secondaryColorPicker" value="#FF6D00">
</div>
</div>
<div class="control-row">
<label>渐变过渡:</label>
<input type="range" id="gradientSmoothnessSlider" min="1" max="10" value="5">
<span id="gradientSmoothnessValue" class="value-display">5</span>
</div>
<div class="control-row">
<label>渐变偏移:</label>
<input type="range" id="gradientOffsetSlider" min="-50" max="50" value="0">
<span id="gradientOffsetValue" class="value-display">0%</span>
</div>
<div class="presets">
<button class="preset-btn" data-preset="smoothSunset">
�� 日落实景
</button>
<button class="preset-btn" data-preset="oceanDepth">
�� 深海渐变
</button>
<button class="preset-btn" data-preset="forestCanopy">
�� 森林冠层
</button>
<button class="preset-btn" data-preset="neonGlow">
�� 霓虹发光
</button>
</div>
</div>
<div class="control-group">
<div class="group-header">可视化模式</div>
<div class="mode-toggle">
<button id="modeBar" class="mode-btn active">
▮ 柱状模式
</button>
<button id="modeRoundBar" class="mode-btn">
⬤ 圆端模式
</button>
<button id="modeWave" class="mode-btn">
⌇ 波形模式
</button>
</div>
<div class="control-row" style="margin-top: 15px;">
<label>动态灵敏度:</label>
<input type="range" id="sensitivitySlider" min="0.1" max="2" step="0.1" value="1">
<span id="sensitivityValue" class="value-display">1.0x</span>
</div>
</div>
</div>
</div>
<div class="footer">
<p>平滑过渡音频可视化器 © 2023 | 使用Web Audio API实现</p>
</div>
</div>
<script>
function createBubbles() {
const container = document.querySelector('.visualizer-container');
const bubbleCount = 12;
for (let i = 0; i < bubbleCount; i++) {
const bubble = document.createElement('div');
bubble.className = 'bubble';
const size = Math.random() * 80 + 20;
const posX = Math.random() * 100;
const posY = Math.random() * 100;
bubble.style.width = `${size}px`;
bubble.style.height = `${size}px`;
bubble.style.left = `${posX}%`;
bubble.style.top = `${posY}%`;
const duration = Math.random() * 20 + 20;
bubble.style.animation = `float ${duration}s infinite ease-in-out`;
container.appendChild(bubble);
}
}
const container = document.querySelector(".visualizer-container");
const canvas = document.getElementById("visualizer");
const ctx = canvas.getContext("2d");
const audio = document.getElementById("audioPlayer");
const previewBar = document.getElementById("previewBar");
const barCountSlider = document.getElementById("barCountSlider");
const spacingSlider = document.getElementById("spacingSlider");
const baseHeightSlider = document.getElementById("baseHeightSlider");
const borderRadiusSlider = document.getElementById("borderRadiusSlider");
const roundnessSlider = document.getElementById("roundnessSlider");
const primaryColorPicker = document.getElementById("primaryColorPicker");
const secondaryColorPicker = document.getElementById("secondaryColorPicker");
const gradientSmoothnessSlider = document.getElementById("gradientSmoothnessSlider");
const gradientOffsetSlider = document.getElementById("gradientOffsetSlider");
const sensitivitySlider = document.getElementById("sensitivitySlider");
const modeBarBtn = document.getElementById("modeBar");
const modeRoundBarBtn = document.getElementById("modeRoundBar");
const modeWaveBtn = document.getElementById("modeWave");
const barCountValue = document.getElementById("barCountValue");
const spacingValue = document.getElementById("spacingValue");
const baseHeightValue = document.getElementById("baseHeightValue");
const borderRadiusValue = document.getElementById("borderRadiusValue");
const roundnessValue = document.getElementById("roundnessValue");
const gradientSmoothnessValue = document.getElementById("gradientSmoothnessValue");
const gradientOffsetValue = document.getElementById("gradientOffsetValue");
const sensitivityValue = document.getElementById("sensitivityValue");
const presetButtons = document.querySelectorAll('.preset-btn');
let analyser = null;
let barCount = 32;
let barSpacing = 2;
let barBaseHeight = 8;
let barBorderRadius = 4;
let roundness = 0;
let gradientSmoothness = 5;
let gradientOffset = 0;
let primaryColor = "#FF9800";
let secondaryColor = "#FF6D00";
let audioContext = null;
let visualizationMode = "bar";
let sensitivity = 1.0;
let history = new Array(64).fill(0);
function resizeCanvas() {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
createBubbles();
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
function updateUIValues() {
barCountValue.textContent = barCount;
spacingValue.textContent = `${barSpacing}px`;
baseHeightValue.textContent = `${barBaseHeight}px`;
borderRadiusValue.textContent = `${barBorderRadius}px`;
roundnessValue.textContent = `${roundness}%`;
gradientSmoothnessValue.textContent = gradientSmoothness;
gradientOffsetValue.textContent = `${gradientOffset}%`;
sensitivityValue.textContent = `${sensitivity.toFixed(1)}x`;
const actualRadius = barBorderRadius * (1 + roundness/50);
previewBar.style.borderRadius = `${actualRadius}px`;
if (roundness > 50) {
previewBar.style.borderRadius = `50%`;
}
const smoothnessFactor = gradientSmoothness / 10;
const offset = gradientOffset / 100;
previewBar.style.background = `linear-gradient(
to bottom,
${primaryColor},
${mixColors(primaryColor, secondaryColor, 0.5 + offset/2)},
${mixColors(primaryColor, secondaryColor, 0.5 - offset/2)},
${secondaryColor}
)`;
}
barCountSlider.addEventListener('input', function() {
barCount = parseInt(this.value);
updateUIValues();
});
spacingSlider.addEventListener('input', function() {
barSpacing = parseInt(this.value);
updateUIValues();
});
baseHeightSlider.addEventListener('input', function() {
barBaseHeight = parseInt(this.value);
updateUIValues();
});
borderRadiusSlider.addEventListener('input', function() {
barBorderRadius = parseInt(this.value);
updateUIValues();
});
roundnessSlider.addEventListener('input', function() {
roundness = parseInt(this.value);
updateUIValues();
});
gradientSmoothnessSlider.addEventListener('input', function() {
gradientSmoothness = parseInt(this.value);
updateUIValues();
});
gradientOffsetSlider.addEventListener('input', function() {
gradientOffset = parseInt(this.value);
updateUIValues();
});
sensitivitySlider.addEventListener('input', function() {
sensitivity = parseFloat(this.value);
updateUIValues();
});
primaryColorPicker.addEventListener('input', function() {
primaryColor = this.value;
updateUIValues();
});
secondaryColorPicker.addEventListener('input', function() {
secondaryColor = this.value;
updateUIValues();
});
modeBarBtn.addEventListener('click', function() {
visualizationMode = "bar";
modeBarBtn.classList.add('active');
modeRoundBarBtn.classList.remove('active');
modeWaveBtn.classList.remove('active');
});
modeRoundBarBtn.addEventListener('click', function() {
visualizationMode = "roundBar";
modeBarBtn.classList.remove('active');
modeRoundBarBtn.classList.add('active');
modeWaveBtn.classList.remove('active');
});
modeWaveBtn.addEventListener('click', function() {
visualizationMode = "wave";
modeBarBtn.classList.remove('active');
modeRoundBarBtn.classList.remove('active');
modeWaveBtn.classList.add('active');
});
function mixColors(color1, color2, weight) {
function hexToRgb(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return [r, g, b];
}
function rgbToHex(r, g, b) {
return "#" + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('');
}
const [r1, g1, b1] = hexToRgb(color1);
const [r2, g2, b2] = hexToRgb(color2);
const r = Math.round(r1 * (1 - weight) + r2 * weight);
const g = Math.round(g1 * (1 - weight) + g2 * weight);
const b = Math.round(b1 * (1 - weight) + b2 * weight);
return rgbToHex(r, g, b);
}
function createOptimizedGradient(x, topY, bottomY, barHeight, barCount) {
const smoothnessFactor = gradientSmoothness / 8;
const offset = gradientOffset / 100;
const gradientHeight = Math.max(barHeight * 2, 100);
const positionOffset = Math.sin(x / canvas.width * Math.PI * 0.5) * offset * 0.1;
const gradient = ctx.createLinearGradient(
0, topY - barHeight * positionOffset,
0, bottomY + barHeight * positionOffset
);
gradient.addColorStop(0, primaryColor);
for (let i = 1; i <= smoothnessFactor; i++) {
const position = i / (smoothnessFactor + 1);
gradient.addColorStop(
position,
mixColors(primaryColor, secondaryColor, position + positionOffset)
);
}
gradient.addColorStop(1, secondaryColor);
return gradient;
}
function createRoundEndedRect(x, y, width, height, radius, roundness) {
const path = new Path2D();
const actualRadius = Math.min(width/2, height/2, radius * (1 + roundness/50));
const finalRadius = roundness > 50 ? Math.min(width/2, height/2) : actualRadius;
if (finalRadius <= 0) {
path.rect(x, y, width, height);
return path;
}
path.moveTo(x + finalRadius, y);
path.lineTo(x + width - finalRadius, y);
path.arcTo(x + width, y, x + width, y + finalRadius, finalRadius);
path.lineTo(x + width, y + height - finalRadius);
path.arcTo(x + width, y + height, x + width - finalRadius, y + height, finalRadius);
path.lineTo(x + finalRadius, y + height);
path.arcTo(x, y + height, x, y + height - finalRadius, finalRadius);
path.lineTo(x, y + finalRadius);
path.arcTo(x, y, x + finalRadius, y, finalRadius);
path.closePath();
return path;
}
function generateWeights(count, intensity) {
const weights = [];
const centerIndex = (count - 1) / 2;
const factor = intensity * 0.1;
for (let i = 0; i < count; i++) {
const distanceFromCenter = Math.abs(i - centerIndex);
const weight = Math.exp(
-Math.pow(distanceFromCenter, 2) / (factor * count)
);
weights.push(weight);
}
return weights;
}
function drawBarVisualizer() {
if (!analyser) return;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, 'rgba(20, 30, 50, 0.6)');
gradient.addColorStop(1, 'rgba(10, 20, 40, 0.8)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width - barSpacing * (barCount - 1)) / barCount;
const centerY = canvas.height / 2;
const weights = generateWeights(barCount, 6);
for (let i = 0; i < barCount; i++) {
const dataIndex = Math.floor((i / barCount) * bufferLength * 0.5);
let value = dataArray[dataIndex] / 255;
value = Math.min(1, value * sensitivity);
history[i] = history[i] * 0.7 + value * 0.3;
value = history[i];
const weightedValue = value * weights[i];
const barHeight = Math.max(barBaseHeight, weightedValue * centerY);
const x = i * (barWidth + barSpacing);
ctx.fillStyle = createOptimizedGradient(x, centerY - barHeight, centerY + barHeight, barHeight, barCount);
const y = centerY - barHeight;
const totalHeight = barHeight * 2;
const barPath = createRoundEndedRect(
x,
y,
barWidth,
totalHeight,
barBorderRadius,
roundness
);
ctx.fill(barPath);
ctx.globalCompositeOperation = 'screen';
const highlight = ctx.createLinearGradient(
x, y,
x, y + totalHeight
);
highlight.addColorStop(0, 'rgba(255, 255, 255, 0.3)');
highlight.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
highlight.addColorStop(0.5, 'transparent');
ctx.fillStyle = highlight;
ctx.fill(barPath);
ctx.globalCompositeOperation = 'source-over';
}
}
function drawRoundBarVisualizer() {
return drawBarVisualizer();
}
function drawWaveVisualizer() {
if (!analyser) return;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const centerY = canvas.height / 2;
const sliceWidth = canvas.width / barCount;
const weights = generateWeights(barCount, 6);
ctx.beginPath();
for (let i = 0; i < barCount; i++) {
const dataIndex = Math.floor((i / barCount) * bufferLength * 0.5);
let value = dataArray[dataIndex] / 255;
value = Math.min(1, value * sensitivity);
history[i] = history[i] * 0.7 + value * 0.3;
value = history[i];
const weightedValue = value * weights[i];
const barHeight = Math.max(barBaseHeight, weightedValue * centerY);
const x = i * sliceWidth;
const yTop = centerY - barHeight;
const yBottom = centerY + barHeight;
if (i === 0) {
ctx.moveTo(x, yTop);
} else {
ctx.lineTo(x, yTop);
}
}
for (let i = barCount - 1; i >= 0; i--) {
const dataIndex = Math.floor((i / barCount) * bufferLength * 0.5);
let value = dataArray[dataIndex] / 255;
value = Math.min(1, value * sensitivity);
history[i] = history[i] * 0.7 + value * 0.3;
value = history[i];
const weightedValue = value * weights[i];
const barHeight = Math.max(barBaseHeight, weightedValue * centerY);
const x = i * sliceWidth;
const yBottom = centerY + barHeight;
ctx.lineTo(x, yBottom);
}
ctx.closePath();
const gradient = ctx.createLinearGradient(
0, centerY - centerY,
0, centerY + centerY
);
const smoothnessFactor = gradientSmoothness / 8;
gradient.addColorStop(0, primaryColor);
for (let i = 1; i <= smoothnessFactor; i++) {
const position = i / (smoothnessFactor + 1);
gradient.addColorStop(
position,
mixColors(primaryColor, secondaryColor, position)
);
}
gradient.addColorStop(1, secondaryColor);
ctx.fillStyle = gradient;
ctx.fill();
ctx.strokeStyle = mixColors(primaryColor, secondaryColor, 0.5);
ctx.lineWidth = 2;
ctx.stroke();
}
function drawVisualizer() {
requestAnimationFrame(drawVisualizer);
if (visualizationMode === "bar") {
drawBarVisualizer();
} else if (visualizationMode === "roundBar") {
drawRoundBarVisualizer();
} else {
drawWaveVisualizer();
}
}
function initAudio() {
audio.onplay = () => {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
const source = audioContext.createMediaElementSource(audio);
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
source.connect(analyser);
analyser.connect(audioContext.destination);
}
};
}
function init() {
initAudio();
drawVisualizer();
updateUIValues();
}
window.addEventListener("load", init);
</script>
</body>
</html>