From 174b318c4f2a896f3d2d30bc5a7eee2b876e0369 Mon Sep 17 00:00:00 2001 From: skal Date: Sun, 15 Feb 2026 11:38:56 +0100 Subject: fix(timeline-editor): align waveform and timeline tick positions Waveform and timeline were using different width calculations, causing beat markers and timeline ticks to misalign when BPM changed. Waveform now uses same maxTime calculation as timeline (including sequence padding). Also replaced magic constants (16, 0.4) with named constants for clarity: - SEQUENCE_DEFAULT_DURATION = 16 beats - WAVEFORM_AMPLITUDE_SCALE = 0.4 Co-Authored-By: Claude Sonnet 4.5 --- tools/timeline_editor/index.html | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) (limited to 'tools/timeline_editor') diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html index 4d6c81e..363c5cb 100644 --- a/tools/timeline_editor/index.html +++ b/tools/timeline_editor/index.html @@ -188,12 +188,14 @@ const VERTICAL_SCROLL_SPEED = 0.3; const SEQUENCE_GAP = 10; const SEQUENCE_DEFAULT_WIDTH = 10; + const SEQUENCE_DEFAULT_DURATION = 16; const SEQUENCE_MIN_HEIGHT = 70; const SEQUENCE_COLLAPSED_HEIGHT = 35; const SEQUENCE_TOP_PADDING = 20; const SEQUENCE_BOTTOM_PADDING = 5; const EFFECT_SPACING = 30; const EFFECT_HEIGHT = 26; + const WAVEFORM_AMPLITUDE_SCALE = 0.4; // State const state = { @@ -354,17 +356,28 @@ function renderWaveform() { if (!state.audioBuffer) return; const canvas = dom.waveformCanvas, ctx = canvas.getContext('2d'); - const w = timeToBeats(state.audioDuration) * state.pixelsPerSecond, h = 80; + + // Calculate maxTime same as timeline to ensure alignment + let maxTime = 60; + for (const seq of state.sequences) { + maxTime = Math.max(maxTime, seq.startTime + SEQUENCE_DEFAULT_DURATION); + for (const effect of seq.effects) maxTime = Math.max(maxTime, seq.startTime + effect.endTime); + } + if (state.audioDuration > 0) maxTime = Math.max(maxTime, state.audioDuration * state.bpm / 60.0); + + const w = maxTime * state.pixelsPerSecond, h = 80; canvas.width = w; canvas.height = h; canvas.style.width = `${w}px`; canvas.style.height = `${h}px`; ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; ctx.fillRect(0, 0, w, h); const channelData = state.audioBuffer.getChannelData(0); - const samplesPerPixel = Math.ceil(channelData.length / w); - const centerY = h / 2, amplitudeScale = h * 0.4; + const audioBeats = timeToBeats(state.audioDuration); + const audioPixelWidth = audioBeats * state.pixelsPerSecond; + const samplesPerPixel = Math.ceil(channelData.length / audioPixelWidth); + const centerY = h / 2, amplitudeScale = h * WAVEFORM_AMPLITUDE_SCALE; ctx.strokeStyle = '#4ec9b0'; ctx.lineWidth = 1; ctx.beginPath(); - for (let x = 0; x < w; x++) { + for (let x = 0; x < audioPixelWidth; x++) { const start = Math.floor(x * samplesPerPixel); const end = Math.min(start + samplesPerPixel, channelData.length); let min = 1.0, max = -1.0; @@ -378,13 +391,12 @@ } ctx.stroke(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; - ctx.beginPath(); ctx.moveTo(0, centerY); ctx.lineTo(w, centerY); ctx.stroke(); + ctx.beginPath(); ctx.moveTo(0, centerY); ctx.lineTo(audioPixelWidth, centerY); ctx.stroke(); - // Draw beat markers - const maxBeats = timeToBeats(state.audioDuration); + // Draw beat markers across full maxTime width ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; ctx.lineWidth = 1; - for (let beat = 0; beat <= maxBeats; beat++) { + for (let beat = 0; beat <= maxTime; beat++) { const x = beat * state.pixelsPerSecond; ctx.beginPath(); ctx.moveTo(x, 0); @@ -564,7 +576,7 @@ dom.timeline.innerHTML = ''; document.getElementById('timeMarkers').innerHTML = ''; let maxTime = 60; for (const seq of state.sequences) { - maxTime = Math.max(maxTime, seq.startTime + 16); + maxTime = Math.max(maxTime, seq.startTime + SEQUENCE_DEFAULT_DURATION); for (const effect of seq.effects) maxTime = Math.max(maxTime, seq.startTime + effect.endTime); } if (state.audioDuration > 0) maxTime = Math.max(maxTime, state.audioDuration * state.bpm / 60.0); -- cgit v1.2.3