let _context; function getContext() { if (!_context) { _context = new AudioContext(); } return _context; } async function loadSample(url) { const res = await fetch(url); const buffer = res.arrayBuffer(); return getContext().decodeAudioData(buffer); } function playSoundSample(sample, sampleNote, noteToPlay) { const ctx = getContext(); const source = ctx.createBufferSource(); source.buffer = sample; source.playbackRate.value = 2 ** ((noteToPlay - sampleNote) / 12); source.connect(ctx.destination); source.start(0); } function sigmoid(x) { return 1 / (1 + Math.exp(-x)); } function drawNoteLines(canvas) { const ctx = canvas.getContext("2d"); let clicks = []; function render() { const W = 256; const H = 512; ctx.clearRect(0, 0, W, H); const L = 1.0; ctx.lineWidth = L; ctx.strokeStyle = "#ccc"; for (let note = 0; note <= 12; note++) { ctx.beginPath(); const y = (note / 12) * (H - L) + L / 2; ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); } let now = Date.now(); const R = 10.0; for (const [x, y, t] of clicks) { ctx.beginPath(); ctx.arc(x - R, y - R, 10, 0, 2 * Math.PI); const tt = (now - t) / 1000; // [0, 1] const alpha = sigmoid(5 - 10 * tt); ctx.fillStyle = `rgb(0, 99, 228, ${alpha})`; ctx.fill(); } clicks = clicks.filter((o) => now < o[2] + 1_000); if (clicks.length > 0) { requestAnimationFrame(render); } } render(); canvas.addEventListener("mousedown", (e) => { const { layerX, layerY } = e; clicks.push([layerX, layerY, Date.now()]); render(); }); } window.drawNoteLines = drawNoteLines;