.wrap{position:relative;width:92%;height:84%;display:flex;align-items:center;justify-content:center}
canvas{width:100%;height:100%;display:block}
.play{position:absolute;right:0;bottom:0;width:clamp(28px,15%,40px);aspect-ratio:1;border-radius:50%;border:1px solid var(--line);background:var(--surface);display:grid;place-items:center;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.12)}
.play .tri{width:0;height:0;border-style:solid;border-width:6px 0 6px 9px;border-color:transparent transparent transparent var(--fg);margin-left:2px}
.play:active{transform:scale(.92)}
const DATA = [0.3, 0.5, 0.35, 0.7, 0.55, 0.85, 0.6, 0.95];
const cv = document.getElementById('chart');
const g2d = cv.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const ACCENT = cs.getPropertyValue('--accent').trim() || '#5b5bf7';
const LINEC = cs.getPropertyValue('--line').trim() || '#ddd';
let w, h;
function resize() {
const dpr = Math.min(devicePixelRatio || 1, 2);
w = cv.clientWidth; h = cv.clientHeight;
cv.width = w * dpr; cv.height = h * dpr;
g2d.setTransform(dpr, 0, 0, dpr, 0, 0);
}
addEventListener('resize', resize);
resize();
function xAt(i) { return (i / (DATA.length - 1)) * (w - 20) + 10; }
function yAt(v) { return h - 16 - v * (h - 32); }
function drawChart(playhead) {
g2d.clearRect(0, 0, w, h);
g2d.beginPath();
DATA.forEach((v, i) => {
const x = xAt(i), y = yAt(v);
if (i === 0) g2d.moveTo(x, y); else g2d.lineTo(x, y);
});
g2d.strokeStyle = LINEC;
g2d.lineWidth = 1.5;
g2d.stroke();
DATA.forEach((v, i) => {
g2d.beginPath();
g2d.arc(xAt(i), yAt(v), 3, 0, Math.PI * 2);
g2d.fillStyle = ACCENT;
g2d.fill();
});
const x = 10 + playhead * (w - 20);
g2d.beginPath();
g2d.moveTo(x, 6);
g2d.lineTo(x, h - 6);
g2d.strokeStyle = ACCENT;
g2d.lineWidth = 1.5;
g2d.globalAlpha = 0.6;
g2d.stroke();
g2d.globalAlpha = 1;
}
const LOOP_MS = 2600;
function idle(t) {
drawChart((t % LOOP_MS) / LOOP_MS);
requestAnimationFrame(idle);
}
requestAnimationFrame(idle);
document.getElementById('play').addEventListener('click', () => {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const now = ctx.currentTime;
const step = 0.13;
DATA.forEach((v, i) => {
const t = now + i * step;
const freq = 300 + v * 500;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0, t);
gain.gain.linearRampToValueAtTime(0.12, t + 0.015);
gain.gain.exponentialRampToValueAtTime(0.0001, t + step - 0.01);
osc.connect(gain).connect(ctx.destination);
osc.start(t);
osc.stop(t + step);
});
setTimeout(() => ctx.close(), DATA.length * step * 1000 + 300);
});