summaryrefslogtreecommitdiff
path: root/tools/timeline_editor
diff options
context:
space:
mode:
Diffstat (limited to 'tools/timeline_editor')
-rw-r--r--tools/timeline_editor/README.md17
-rw-r--r--tools/timeline_editor/index.html95
2 files changed, 66 insertions, 46 deletions
diff --git a/tools/timeline_editor/README.md b/tools/timeline_editor/README.md
index 4861a88..dd1f38b 100644
--- a/tools/timeline_editor/README.md
+++ b/tools/timeline_editor/README.md
@@ -5,16 +5,17 @@ Interactive web-based editor for `timeline.seq` files.
## Features
- 📂 Load/save `timeline.seq` files
-- 📊 Visual Gantt-style timeline with sticky time markers
+- 📊 Visual Gantt-style timeline with sticky time markers (beat-based)
- 🎯 Drag & drop sequences and effects
- 🎯 Resize effects with handles
- 📦 Collapsible sequences (double-click to collapse)
- 📏 Vertical grid lines synchronized with time ticks
-- ⏱️ Edit timing and properties
+- ⏱️ Edit timing and properties (in beats)
- ⚙️ Stack-order based priority system
-- 🔍 Zoom (10%-500%) with mouse wheel + Ctrl/Cmd
-- 🎵 Audio waveform visualization
-- 🎼 Snap-to-beat mode
+- 🔍 Zoom (10%-200%) with mouse wheel + Ctrl/Cmd
+- 🎵 Audio waveform visualization (aligned to beats)
+- 🎼 Snap-to-beat mode (enabled by default)
+- 🎛️ BPM slider (60-200 BPM)
- 🔄 Re-order sequences by time
- 🗑️ Delete sequences/effects
@@ -73,8 +74,12 @@ SEQUENCE 2.5s 0 "Explicit seconds" # Rare: start at 2.5 physical seconds
## Technical Notes
- Pure HTML/CSS/JavaScript (no dependencies, works offline)
-- Sequences have absolute times, effects are relative to parent sequence
+- **Internal representation uses beats** (not seconds)
+- Sequences have absolute times (beats), effects are relative to parent sequence
+- BPM used for seconds conversion (tooltips, audio waveform alignment)
- Priority determines render order (higher = on top)
- Collapsed sequences show 35px title bar, expanded show full effect stack
+- Time markers show beats by default (4-beat/bar increments)
- Time markers sticky at top when scrolling
- Vertical grid lines aid alignment
+- Snap-to-beat enabled by default for musical alignment
diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html
index 15e92a6..62b426f 100644
--- a/tools/timeline_editor/index.html
+++ b/tools/timeline_editor/index.html
@@ -477,9 +477,11 @@
<div class="zoom-controls">
<label>Zoom: <input type="range" id="zoomSlider" min="10" max="200" value="100" step="10"></label>
<span id="zoomLevel">100%</span>
+ <label style="margin-left: 20px">BPM: <input type="range" id="bpmSlider" min="60" max="200" value="120" step="1"></label>
+ <span id="currentBPM">120</span>
<label class="checkbox-label" style="margin-left: 20px">
- <input type="checkbox" id="showBeatsCheckbox">
- Show Beats (BPM: <span id="currentBPM">120</span>)
+ <input type="checkbox" id="showBeatsCheckbox" checked>
+ Show Beats
</label>
</div>
@@ -510,7 +512,7 @@
let currentFile = null;
let selectedItem = null;
let pixelsPerSecond = 100;
- let showBeats = false;
+ let showBeats = true;
let bpm = 120;
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
@@ -547,13 +549,18 @@
let bpm = 120; // Default BPM
let currentPriority = 0; // Track priority for + = - modifiers
- // Helper: Convert time notation to seconds
+ // Helper: Parse time notation (returns beats)
function parseTime(timeStr) {
+ if (timeStr.endsWith('s')) {
+ // Explicit seconds: "2.5s" = convert to beats
+ const seconds = parseFloat(timeStr.slice(0, -1));
+ return seconds * bpm / 60.0;
+ }
if (timeStr.endsWith('b')) {
- // Beat notation: "4b" = 4 beats
- const beats = parseFloat(timeStr.slice(0, -1));
- return beats * (60.0 / bpm);
+ // Explicit beats: "4b" = 4 beats
+ return parseFloat(timeStr.slice(0, -1));
}
+ // Default: beats
return parseFloat(timeStr);
}
@@ -633,7 +640,7 @@
return { sequences, bpm };
}
- // Serializer: JavaScript objects → timeline.seq
+ // Serializer: JavaScript objects → timeline.seq (outputs beats)
function serializeSeqFile(sequences) {
let output = '# Demo Timeline\n';
output += '# Generated by Timeline Editor\n';
@@ -687,8 +694,9 @@
const canvas = waveformCanvas;
const ctx = canvas.getContext('2d');
- // Set canvas size based on audio duration and zoom
- const canvasWidth = audioDuration * pixelsPerSecond;
+ // Set canvas size based on audio duration (convert to beats) and zoom
+ const audioDurationBeats = audioDuration * bpm / 60.0;
+ const canvasWidth = audioDurationBeats * pixelsPerSecond;
const canvasHeight = 80;
// Set actual canvas resolution (for sharp rendering)
@@ -767,10 +775,10 @@
const timeMarkers = document.getElementById('timeMarkers');
timeMarkers.innerHTML = '';
- // Calculate max time
- let maxTime = 30; // Default 30 seconds
+ // Calculate max time (in beats)
+ let maxTime = 60; // Default 60 beats (15 bars)
for (const seq of sequences) {
- const seqEnd = seq.startTime + 10; // Default sequence duration
+ const seqEnd = seq.startTime + 16; // Default 4 bars
maxTime = Math.max(maxTime, seqEnd);
for (const effect of seq.effects) {
@@ -780,7 +788,8 @@
// Extend timeline to fit audio if loaded
if (audioDuration > 0) {
- maxTime = Math.max(maxTime, audioDuration);
+ const audioBeats = audioDuration * bpm / 60.0;
+ maxTime = Math.max(maxTime, audioBeats);
}
// Render time markers
@@ -788,23 +797,22 @@
timeline.style.width = `${timelineWidth}px`;
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;
+ // Show beats (default)
+ for (let beat = 0; beat <= maxTime; beat += 4) {
const marker = document.createElement('div');
marker.className = 'time-marker';
- marker.style.left = `${timeSec * pixelsPerSecond}px`;
+ marker.style.left = `${beat * pixelsPerSecond}px`;
marker.textContent = `${beat}b`;
timeMarkers.appendChild(marker);
}
} else {
// Show seconds
- for (let t = 0; t <= maxTime; t += 1) {
+ const maxSeconds = maxTime * 60.0 / bpm;
+ for (let t = 0; t <= maxSeconds; t += 1) {
+ const beatPos = t * bpm / 60.0;
const marker = document.createElement('div');
marker.className = 'time-marker';
- marker.style.left = `${t * pixelsPerSecond}px`;
+ marker.style.left = `${beatPos * pixelsPerSecond}px`;
marker.textContent = `${t}s`;
timeMarkers.appendChild(marker);
}
@@ -927,16 +935,14 @@
effectDiv.style.width = `${effectWidth}px`;
effectDiv.style.height = '26px';
- // Format time display based on mode (for tooltip)
- 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`;
- }
+ // Format time display (beats primary, seconds in tooltip)
+ const startBeat = effect.startTime.toFixed(1);
+ const endBeat = effect.endTime.toFixed(1);
+ const startSec = (effect.startTime * 60.0 / bpm).toFixed(1);
+ const endSec = (effect.endTime * 60.0 / bpm).toFixed(1);
+ const timeDisplay = showBeats
+ ? `${startBeat}-${endBeat}b (${startSec}-${endSec}s)`
+ : `${startSec}-${endSec}s (${startBeat}-${endBeat}b)`;
// Show only class name, full info on hover
effectDiv.innerHTML = `
@@ -1012,11 +1018,9 @@
const newX = e.clientX - timelineRect.left - dragOffset.x;
let newTime = Math.max(0, newX / pixelsPerSecond);
- // Snap to beat when in beat mode
+ // Snap to beat when enabled
if (showBeats) {
- const beatDuration = 60.0 / bpm;
- const nearestBeat = Math.round(newTime / beatDuration);
- newTime = nearestBeat * beatDuration;
+ newTime = Math.round(newTime);
}
if (selectedItem.type === 'sequence') {
@@ -1063,11 +1067,9 @@
const newX = e.clientX - timelineRect.left;
let newTime = Math.max(0, newX / pixelsPerSecond);
- // Snap to beat when in beat mode
+ // Snap to beat when enabled
if (showBeats) {
- const beatDuration = 60.0 / bpm;
- const nearestBeat = Math.round(newTime / beatDuration);
- newTime = nearestBeat * beatDuration;
+ newTime = Math.round(newTime);
}
const seq = sequences[selectedItem.seqIndex];
@@ -1239,6 +1241,7 @@
sequences = parsed.sequences;
bpm = parsed.bpm;
document.getElementById('currentBPM').textContent = bpm;
+ document.getElementById('bpmSlider').value = bpm;
renderTimeline();
saveBtn.disabled = false;
addSequenceBtn.disabled = false;
@@ -1338,6 +1341,18 @@
renderTimeline();
});
+ // BPM slider
+ const bpmSlider = document.getElementById('bpmSlider');
+ const currentBPMDisplay = document.getElementById('currentBPM');
+ bpmSlider.addEventListener('input', (e) => {
+ bpm = parseInt(e.target.value);
+ currentBPMDisplay.textContent = bpm;
+ if (audioBuffer) {
+ renderWaveform();
+ }
+ renderTimeline();
+ });
+
// Beats toggle
const showBeatsCheckbox = document.getElementById('showBeatsCheckbox');
showBeatsCheckbox.addEventListener('change', (e) => {