<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>互動音樂合成器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #1a1a1a;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center; /* 新增居中 */
font-family: Arial, sans-serif;
}
.container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
width: 90%; /* 响应式宽度 */
max-width: 800px;
padding: 20px;
}
.box {
width: 100%;
padding-bottom: 100%; /* 保持正方形 */
background: #4CAF50;
border-radius: 12px;
cursor: pointer;
position: relative;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
transition:
transform 0.1s ease,
filter 0.3s ease;
}
.box-inner {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: white; /* 新增文字 */
font-size: 1.2em;
}
.box.holding {
filter: brightness(1.3) saturate(1.2);
transform: scale(0.95);
box-shadow: 0 0 25px rgba(255,255,255,0.4);
}
/* 動畫效果修正 */
.wave::after {
content: '';
position: absolute;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.8) 10%, transparent 30%);
animation: wave 1.2s linear;
}
.pulse {
animation: pulse 0.8s ease-out infinite;
}
.spin {
animation: spin 1.2s linear;
}
.gradient::before {
content: '';
position: absolute;
width: 200%;
height: 200%;
background: linear-gradient(45deg,
#ff0000 0%,
#00ff00 50%,
#0000ff 100%);
animation: gradient 4s linear infinite;
}
@keyframes wave {
from { transform: scale(0); opacity: 1; }
to { transform: scale(1); opacity: 0; }
}
@keyframes pulse {
50% { transform: scale(1.1); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes gradient {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
</style>
</head>
<body>
<div class="container" id="container"></div>
<script>
// 強制初始化音頻上下文
let audioContext;
function initAudio() {
if(!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
// 自動生成和諧音階
const generateHarmonicFrequencies = () => {
const baseFreq = 130.81; // C3
const ratios = [1, 1.25, 1.5, 1.875]; // C大三和弦+七音
const octaves = [1, 2, 4, 8]; // 4個八度
return octaves.flatMap(oct =>
ratios.map(r => baseFreq * oct * r)
).slice(0, 16);
};
// 創建可視化方塊
const createBox = (freq, index) => {
const box = document.createElement('div');
box.className = 'box';
const inner = document.createElement('div');
inner.className = 'box-inner';
inner.textContent = `${Math.round(freq)}Hz`; // 顯示頻率
box.appendChild(inner);
box.dataset.frequency = freq;
box.dataset.animation = ['wave', 'pulse', 'spin', 'gradient'][index % 4];
box.style.backgroundColor = `hsl(${index * 45}, 70%, 50%)`;
return box;
};
// 初始化界面
const container = document.getElementById('container');
generateHarmonicFrequencies().forEach((freq, i) => {
container.appendChild(createBox(freq, i));
});
// 音頻系統
const activeSynths = new Map();
const createSynth = (freq) => {
initAudio();
const osc = audioContext.createOscillator();
const gain = audioContext.createGain();
osc.type = ['sine', 'square', 'sawtooth'][Math.floor(Math.random()*3)];
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.2, audioContext.currentTime);
osc.connect(gain);
gain.connect(audioContext.destination);
osc.start();
return { osc, gain };
};
// 事件處理
const handleStart = (e) => {
const box = e.target.closest('.box');
if (!box) return;
box.classList.add('holding', box.dataset.animation);
const freq = parseFloat(box.dataset.frequency);
if (!activeSynths.has(box)) {
activeSynths.set(box, createSynth(freq));
}
};
const handleEnd = (e) => {
const box = e.target.closest('.box');
if (!box) return;
box.classList.remove('holding');
box.classList.remove(box.dataset.animation);
const synth = activeSynths.get(box);
if (synth) {
synth.gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.5);
synth.osc.stop(audioContext.currentTime + 0.5);
activeSynths.delete(box);
}
};
// 綁定事件
container.addEventListener('mousedown', handleStart);
container.addEventListener('mouseup', handleEnd);
container.addEventListener('mouseleave', handleEnd);
container.addEventListener('touchstart', (e) => {
e.preventDefault();
handleStart(e.touches[0]);
}, { passive: false });
container.addEventListener('touchend', handleEnd);
</script>
</body>
</html>
index.html
index.html