From f49e7131c59ff3dd4dea02c34e713227011f7683 Mon Sep 17 00:00:00 2001 From: skal Date: Thu, 5 Feb 2026 21:21:59 +0100 Subject: fix(timeline-editor): Fix effect stacking and add beats mode with snap-to-beat Bugs fixed: - Effects within sequences now stack vertically instead of overlapping - Sequence height now dynamically adjusts based on effect count - Effects positioned with proper 40px vertical spacing Features added: - Seconds/Beats toggle checkbox with BPM display - Time markers show beats (0b, 1b, etc.) when in beat mode - Snap-to-beat when dragging in beat mode - Effect/sequence time labels show beats when enabled - BPM tracking from loaded demo.seq file The effect stacking bug was caused by all effects using the same vertical position formula (seqIndex * 80 + 25). Fixed by adding effectIndex * 40 to stack effects properly. Sequences now grow in height to accommodate multiple stacked effects. --- tools/timeline_editor/index.html | 105 +++++++++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 14 deletions(-) (limited to 'tools') diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html index 96c2b17..b84908a 100644 --- a/tools/timeline_editor/index.html +++ b/tools/timeline_editor/index.html @@ -39,9 +39,23 @@ display: flex; gap: 10px; flex-wrap: wrap; + align-items: center; margin-bottom: 20px; } + .checkbox-label { + display: flex; + align-items: center; + gap: 8px; + color: #d4d4d4; + cursor: pointer; + user-select: none; + } + + .checkbox-label input[type="checkbox"] { + cursor: pointer; + } + button { background: #0e639c; color: white; @@ -240,6 +254,10 @@ 100% +
@@ -263,6 +281,8 @@ let currentFile = null; let selectedItem = null; let pixelsPerSecond = 100; + let showBeats = false; + let bpm = 120; let isDragging = false; let dragOffset = { x: 0, y: 0 }; @@ -371,14 +391,14 @@ } } - return sequences; + return { sequences, bpm }; } // Serializer: JavaScript objects → demo.seq function serializeSeqFile(sequences) { let output = '# Demo Timeline\n'; output += '# Generated by Timeline Editor\n'; - output += '# BPM 120\n\n'; + output += `# BPM ${bpm}\n\n`; for (const seq of sequences) { const seqLine = `SEQUENCE ${seq.startTime.toFixed(2)} ${seq.priority}`; @@ -419,12 +439,27 @@ const timelineWidth = maxTime * pixelsPerSecond; timeline.style.width = `${timelineWidth}px`; - for (let t = 0; t <= maxTime; t += 1) { - const marker = document.createElement('div'); - marker.className = 'time-marker'; - marker.style.left = `${t * pixelsPerSecond}px`; - marker.textContent = `${t}s`; - timeMarkers.appendChild(marker); + if (showBeats) { + // Show beats + const beatDuration = 60.0 / bpm; // seconds per beat + const maxBeats = Math.ceil(maxTime / beatDuration); + for (let beat = 0; beat <= maxBeats; beat++) { + const timeSec = beat * beatDuration; + const marker = document.createElement('div'); + marker.className = 'time-marker'; + marker.style.left = `${timeSec * pixelsPerSecond}px`; + marker.textContent = `${beat}b`; + timeMarkers.appendChild(marker); + } + } else { + // Show seconds + for (let t = 0; t <= maxTime; t += 1) { + const marker = document.createElement('div'); + marker.className = 'time-marker'; + marker.style.left = `${t * pixelsPerSecond}px`; + marker.textContent = `${t}s`; + timeMarkers.appendChild(marker); + } } // Render sequences @@ -439,14 +474,28 @@ seqDuration = Math.max(...seq.effects.map(e => e.endTime)); } + // Calculate sequence height based on number of effects (stacked vertically) + const numEffects = seq.effects.length; + const seqHeight = Math.max(70, 25 + numEffects * 40 + 5); + seqDiv.style.left = `${seq.startTime * pixelsPerSecond}px`; seqDiv.style.top = `${seqIndex * 80}px`; seqDiv.style.width = `${seqDuration * pixelsPerSecond}px`; - seqDiv.style.height = '70px'; + seqDiv.style.height = `${seqHeight}px`; + + // Format time display based on mode + let seqTimeDisplay; + if (showBeats) { + const beatDuration = 60.0 / bpm; + const startBeat = (seq.startTime / beatDuration).toFixed(1); + seqTimeDisplay = `Start: ${startBeat}b`; + } else { + seqTimeDisplay = `Start: ${seq.startTime.toFixed(2)}s`; + } seqDiv.innerHTML = ` Sequence ${seqIndex + 1}
- Start: ${seq.startTime.toFixed(2)}s | Priority: ${seq.priority} + ${seqTimeDisplay} | Priority: ${seq.priority} `; if (selectedItem && selectedItem.type === 'sequence' && selectedItem.index === seqIndex) { @@ -472,13 +521,24 @@ const effectWidth = (effect.endTime - effect.startTime) * pixelsPerSecond; effectDiv.style.left = `${effectStart}px`; - effectDiv.style.top = `${seqIndex * 80 + 25}px`; + effectDiv.style.top = `${seqIndex * 80 + 25 + effectIndex * 40}px`; effectDiv.style.width = `${effectWidth}px`; effectDiv.style.height = '35px'; + // Format time display based on mode + let timeDisplay; + if (showBeats) { + const beatDuration = 60.0 / bpm; + const startBeat = (effect.startTime / beatDuration).toFixed(1); + const endBeat = (effect.endTime / beatDuration).toFixed(1); + timeDisplay = `${startBeat}-${endBeat}b`; + } else { + timeDisplay = `${effect.startTime.toFixed(1)}-${effect.endTime.toFixed(1)}s`; + } + effectDiv.innerHTML = ` ${effect.className}
- ${effect.startTime.toFixed(1)}-${effect.endTime.toFixed(1)}s + ${timeDisplay} `; if (selectedItem && selectedItem.type === 'effect' && @@ -524,7 +584,14 @@ const timelineRect = timeline.getBoundingClientRect(); const newX = e.clientX - timelineRect.left - dragOffset.x; - const newTime = Math.max(0, newX / pixelsPerSecond); + let newTime = Math.max(0, newX / pixelsPerSecond); + + // Snap to beat when in beat mode + if (showBeats) { + const beatDuration = 60.0 / bpm; + const nearestBeat = Math.round(newTime / beatDuration); + newTime = nearestBeat * beatDuration; + } if (selectedItem.type === 'sequence') { sequences[selectedItem.index].startTime = Math.round(newTime * 100) / 100; @@ -633,7 +700,10 @@ reader.onload = (e) => { try { - sequences = parseSeqFile(e.target.result); + const parsed = parseSeqFile(e.target.result); + sequences = parsed.sequences; + bpm = parsed.bpm; + document.getElementById('currentBPM').textContent = bpm; renderTimeline(); saveBtn.disabled = false; addSequenceBtn.disabled = false; @@ -694,6 +764,13 @@ renderTimeline(); }); + // Beats toggle + const showBeatsCheckbox = document.getElementById('showBeatsCheckbox'); + showBeatsCheckbox.addEventListener('change', (e) => { + showBeats = e.target.checked; + renderTimeline(); + }); + // Click outside to deselect timeline.addEventListener('click', () => { selectedItem = null; -- cgit v1.2.3