()
| 767 | // Faders + waveform seek need live pointer drag, so they're wired imperatively |
| 768 | // after each render (the rest of the UI uses click delegation below). |
| 769 | function wireFaders() { |
| 770 | app.querySelectorAll("[data-fader]").forEach((el) => { |
| 771 | el.addEventListener("pointerdown", (e) => { |
| 772 | e.preventDefault(); |
| 773 | const id = el.dataset.id; |
| 774 | const fill = el.querySelector(".fader-fill"); |
| 775 | const knob = el.querySelector(".fader-knob"); |
| 776 | const rect = el.getBoundingClientRect(); |
| 777 | const set = (cx) => { |
| 778 | let v = (cx - rect.left) / rect.width; |
| 779 | v = Math.max(0, Math.min(1, v)); |
| 780 | state.vols[id] = v; |
| 781 | const pct = Math.round(v * 100) + "%"; |
| 782 | fill.style.width = pct; |
| 783 | knob.style.left = pct; |
| 784 | if (engine && laneActive(id)) engine.setGain(id, v); |
| 785 | }; |
| 786 | set(e.clientX); |
| 787 | el.setPointerCapture(e.pointerId); |
| 788 | const mv = (ev) => set(ev.clientX); |
| 789 | const up = () => { |
| 790 | el.removeEventListener("pointermove", mv); |
| 791 | el.removeEventListener("pointerup", up); |
| 792 | }; |
| 793 | el.addEventListener("pointermove", mv); |
| 794 | el.addEventListener("pointerup", up); |
| 795 | }); |
| 796 | }); |
| 797 | |
| 798 | const speedSlider = app.querySelector("[data-speed]"); |
| 799 | if (speedSlider) { |
| 800 | speedSlider.addEventListener("input", () => { |
| 801 | const rate = parseFloat(speedSlider.value); |
| 802 | state.speed = rate; |
| 803 | const valEl = speedSlider.parentElement?.querySelector(".speed-row-val"); |
| 804 | if (valEl) valEl.textContent = `${rate % 1 === 0 ? rate.toFixed(1) : rate}x`; |
| 805 | if (engine) engine.setPlaybackRate(rate); |
| 806 | }); |
| 807 | } |
| 808 | |
| 809 | const bars = app.querySelector("[data-seek]"); |
| 810 | if (bars) { |
| 811 | bars.addEventListener("pointerdown", (e) => { |
| 812 | if (!engine || !curDuration()) return; |
| 813 | e.preventDefault(); |
| 814 | const rect = bars.getBoundingClientRect(); |
| 815 | const head = bars.querySelector(".playhead"); |
| 816 | const seek = (cx) => { |
| 817 | const frac = Math.max(0, Math.min(1, (cx - rect.left) / rect.width)); |
| 818 | if (head) head.style.left = frac * 100 + "%"; |
| 819 | const cur = app.querySelector(".wave-times .cur"); |
| 820 | if (cur) cur.textContent = fmt(frac * curDuration()); |
| 821 | state.progress = frac; |
| 822 | paintWaveProgress(); |
| 823 | seekToFraction(frac); |
| 824 | }; |
| 825 | seek(e.clientX); |
| 826 | bars.setPointerCapture(e.pointerId); |
no test coverage detected