summaryrefslogtreecommitdiff
path: root/tools/timeline_editor
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-15 15:14:47 +0100
committerskal <pascal.massimino@gmail.com>2026-02-15 15:14:47 +0100
commitf7edf29a4c8208c7654a1c979dba980d741ad52d (patch)
treefb5d24f41843a282bacf283a6de90465fbac338f /tools/timeline_editor
parentf23bd208b38ccfdc814daa99818a7e119f5ee313 (diff)
refactor(timeline-editor): rename variables to reflect beat-based units
Renamed time-related variables for clarity: - pixelsPerSecond → pixelsPerBeat (timeline internally uses beats) - audioDuration → audioDurationSeconds - maxTime → maxTimeBeats - Local variables: newTime → newTimeBeats, duration → durationBeats - Updated stats display to show both beats and seconds All internal state stores beat values; serializer writes beats without suffix per .seq format. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'tools/timeline_editor')
-rw-r--r--tools/timeline_editor/index.html76
1 files changed, 38 insertions, 38 deletions
diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html
index 9b03fa2..14a7791 100644
--- a/tools/timeline_editor/index.html
+++ b/tools/timeline_editor/index.html
@@ -208,10 +208,10 @@
// State
const DEFAULT_BPM = 120;
const state = {
- sequences: [], currentFile: null, selectedItem: null, pixelsPerSecond: 100,
+ sequences: [], currentFile: null, selectedItem: null, pixelsPerBeat: 100,
showBeats: true, quantizeUnit: 1, bpm: DEFAULT_BPM, isDragging: false, dragOffset: { x: 0, y: 0 },
lastActiveSeqIndex: -1, isDraggingHandle: false, handleType: null, handleDragOffset: 0,
- audioBuffer: null, audioDuration: 0, audioSource: null, audioContext: null,
+ audioBuffer: null, audioDurationSeconds: 0, audioSource: null, audioContext: null,
isPlaying: false, playbackStartTime: 0, playbackOffset: 0, playStartPosition: 0, animationFrameId: null,
lastExpandedSeqIndex: -1, dragMoved: false,
...computeBPMValues(DEFAULT_BPM)
@@ -361,12 +361,12 @@
let viewportController, playbackController;
function computeCPULoad() {
- if (state.sequences.length === 0) return { maxTime: 60, loads: [], conflicts: [] };
- let maxTime = Math.max(60, ...state.sequences.flatMap(seq =>
+ if (state.sequences.length === 0) return { maxTimeBeats: 60, loads: [], conflicts: [] };
+ let maxTimeBeats = Math.max(60, ...state.sequences.flatMap(seq =>
seq.effects.map(eff => seq.startTime + eff.endTime)));
- if (state.audioDuration > 0) maxTime = Math.max(maxTime, timeToBeats(state.audioDuration));
+ if (state.audioDurationSeconds > 0) maxTimeBeats = Math.max(maxTimeBeats, timeToBeats(state.audioDurationSeconds));
- const resolution = 0.1, numSamples = Math.ceil(maxTime / resolution);
+ const resolution = 0.1, numSamples = Math.ceil(maxTimeBeats / resolution);
const loads = new Array(numSamples).fill(0);
const conflicts = new Array(numSamples).fill(false);
@@ -417,19 +417,19 @@
});
});
- return { maxTime, loads, conflicts, resolution };
+ return { maxTimeBeats, loads, conflicts, resolution };
}
function renderCPULoad() {
const canvas = dom.cpuLoadCanvas, ctx = canvas.getContext('2d');
- const { maxTime, loads, conflicts, resolution } = computeCPULoad();
- const w = maxTime * state.pixelsPerSecond, h = 10;
+ const { maxTimeBeats, loads, conflicts, resolution } = computeCPULoad();
+ const w = maxTimeBeats * state.pixelsPerBeat, h = 10;
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);
if (loads.length === 0) return;
- const barWidth = resolution * state.pixelsPerSecond;
+ const barWidth = resolution * state.pixelsPerBeat;
loads.forEach((load, i) => {
if (load === 0) return;
const n = Math.min(load / 8, 1.0);
@@ -446,27 +446,27 @@
function renderTimeline() {
renderCPULoad();
dom.timeline.innerHTML = ''; document.getElementById('timeMarkers').innerHTML = '';
- let maxTime = 60;
+ let maxTimeBeats = 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);
+ maxTimeBeats = Math.max(maxTimeBeats, seq.startTime + SEQUENCE_DEFAULT_DURATION);
+ for (const effect of seq.effects) maxTimeBeats = Math.max(maxTimeBeats, seq.startTime + effect.endTime);
}
- if (state.audioDuration > 0) maxTime = Math.max(maxTime, state.audioDuration * state.beatsPerSecond);
- const timelineWidth = maxTime * state.pixelsPerSecond;
+ if (state.audioDurationSeconds > 0) maxTimeBeats = Math.max(maxTimeBeats, state.audioDurationSeconds * state.beatsPerSecond);
+ const timelineWidth = maxTimeBeats * state.pixelsPerBeat;
dom.timeline.style.width = `${timelineWidth}px`;
let totalTimelineHeight = 0;
const timeMarkers = document.getElementById('timeMarkers');
if (state.showBeats) {
- for (let beat = 0; beat <= maxTime; beat += 4) {
+ for (let beat = 0; beat <= maxTimeBeats; beat += 4) {
const marker = document.createElement('div');
- marker.className = 'time-marker'; marker.style.left = `${beat * state.pixelsPerSecond}px`;
+ marker.className = 'time-marker'; marker.style.left = `${beat * state.pixelsPerBeat}px`;
marker.textContent = `${beat}b`; timeMarkers.appendChild(marker);
}
} else {
- const maxSeconds = maxTime * state.secondsPerBeat;
+ const maxSeconds = maxTimeBeats * state.secondsPerBeat;
for (let t = 0; t <= maxSeconds; t += 1) {
const beatPos = t * state.beatsPerSecond, marker = document.createElement('div');
- marker.className = 'time-marker'; marker.style.left = `${beatPos * state.pixelsPerSecond}px`;
+ marker.className = 'time-marker'; marker.style.left = `${beatPos * state.pixelsPerBeat}px`;
marker.textContent = `${t}s`; timeMarkers.appendChild(marker);
}
}
@@ -483,9 +483,9 @@
const numEffects = seq.effects.length;
const fullHeight = Math.max(SEQUENCE_MIN_HEIGHT, SEQUENCE_TOP_PADDING + numEffects * EFFECT_SPACING + SEQUENCE_BOTTOM_PADDING);
const seqHeight = seq._collapsed ? SEQUENCE_COLLAPSED_HEIGHT : fullHeight;
- seqDiv.style.left = `${seqVisualStart * state.pixelsPerSecond}px`;
+ seqDiv.style.left = `${seqVisualStart * state.pixelsPerBeat}px`;
seqDiv.style.top = `${cumulativeY}px`;
- seqDiv.style.width = `${(seqVisualEnd - seqVisualStart) * state.pixelsPerSecond}px`;
+ seqDiv.style.width = `${(seqVisualEnd - seqVisualStart) * state.pixelsPerBeat}px`;
seqDiv.style.height = `${seqHeight}px`; seqDiv.style.minHeight = `${seqHeight}px`; seqDiv.style.maxHeight = `${seqHeight}px`;
seq._yPosition = cumulativeY; cumulativeY += seqHeight + SEQUENCE_GAP; totalTimelineHeight = cumulativeY;
const seqHeaderDiv = document.createElement('div'); seqHeaderDiv.className = 'sequence-header';
@@ -513,9 +513,9 @@
if (conflicts.has(effectIndex)) effectDiv.classList.add('conflict');
Object.assign(effectDiv.dataset, { seqIndex, effectIndex });
Object.assign(effectDiv.style, {
- left: `${(seq.startTime + effect.startTime) * state.pixelsPerSecond}px`,
+ left: `${(seq.startTime + effect.startTime) * state.pixelsPerBeat}px`,
top: `${seq._yPosition + SEQUENCE_TOP_PADDING + effectIndex * EFFECT_SPACING}px`,
- width: `${(effect.endTime - effect.startTime) * state.pixelsPerSecond}px`,
+ width: `${(effect.endTime - effect.startTime) * state.pixelsPerBeat}px`,
height: `${EFFECT_HEIGHT}px`
});
effectDiv.innerHTML = `<div class="effect-handle left"></div><small>${effect.className}</small><div class="effect-handle right"></div>`;
@@ -559,13 +559,13 @@
if (!state.isDragging || !state.selectedItem) return;
state.dragMoved = true;
const containerRect = dom.timelineContent.getBoundingClientRect();
- let newTime = Math.max(0, (e.clientX - containerRect.left + dom.timelineContent.scrollLeft - state.dragOffset.x) / state.pixelsPerSecond);
- if (state.quantizeUnit > 0) newTime = Math.round(newTime * state.quantizeUnit) / state.quantizeUnit;
- if (state.selectedItem.type === 'sequence') state.sequences[state.selectedItem.index].startTime = newTime;
+ let newTimeBeats = Math.max(0, (e.clientX - containerRect.left + dom.timelineContent.scrollLeft - state.dragOffset.x) / state.pixelsPerBeat);
+ if (state.quantizeUnit > 0) newTimeBeats = Math.round(newTimeBeats * state.quantizeUnit) / state.quantizeUnit;
+ if (state.selectedItem.type === 'sequence') state.sequences[state.selectedItem.index].startTime = newTimeBeats;
else if (state.selectedItem.type === 'effect') {
const seq = state.sequences[state.selectedItem.seqIndex], effect = seq.effects[state.selectedItem.effectIndex];
- const duration = effect.endTime - effect.startTime, relativeTime = newTime - seq.startTime;
- effect.startTime = relativeTime; effect.endTime = effect.startTime + duration;
+ const durationBeats = effect.endTime - effect.startTime, relativeTimeBeats = newTimeBeats - seq.startTime;
+ effect.startTime = relativeTimeBeats; effect.endTime = effect.startTime + durationBeats;
}
renderTimeline(); updateProperties();
}
@@ -583,7 +583,7 @@
state.selectedItem = { type: 'effect', seqIndex, effectIndex, index: seqIndex };
const seq = state.sequences[seqIndex], effect = seq.effects[effectIndex];
const containerRect = dom.timelineContent.getBoundingClientRect();
- const mouseTimeBeats = (e.clientX - containerRect.left + dom.timelineContent.scrollLeft) / state.pixelsPerSecond;
+ const mouseTimeBeats = (e.clientX - containerRect.left + dom.timelineContent.scrollLeft) / state.pixelsPerBeat;
const handleTimeBeats = seq.startTime + (type === 'left' ? effect.startTime : effect.endTime);
state.handleDragOffset = handleTimeBeats - mouseTimeBeats;
document.addEventListener('mousemove', onHandleDrag); document.addEventListener('mouseup', stopHandleDrag);
@@ -592,13 +592,13 @@
function onHandleDrag(e) {
if (!state.isDraggingHandle || !state.selectedItem) return;
const containerRect = dom.timelineContent.getBoundingClientRect();
- let newTime = (e.clientX - containerRect.left + dom.timelineContent.scrollLeft) / state.pixelsPerSecond + state.handleDragOffset;
- newTime = Math.max(0, newTime);
- if (state.quantizeUnit > 0) newTime = Math.round(newTime * state.quantizeUnit) / state.quantizeUnit;
+ let newTimeBeats = (e.clientX - containerRect.left + dom.timelineContent.scrollLeft) / state.pixelsPerBeat + state.handleDragOffset;
+ newTimeBeats = Math.max(0, newTimeBeats);
+ if (state.quantizeUnit > 0) newTimeBeats = Math.round(newTimeBeats * state.quantizeUnit) / state.quantizeUnit;
const seq = state.sequences[state.selectedItem.seqIndex], effect = seq.effects[state.selectedItem.effectIndex];
- const relativeTime = newTime - seq.startTime;
- if (state.handleType === 'left') effect.startTime = Math.min(relativeTime, effect.endTime - 0.1);
- else if (state.handleType === 'right') effect.endTime = Math.max(effect.startTime + 0.1, relativeTime);
+ const relativeTimeBeats = newTimeBeats - seq.startTime;
+ if (state.handleType === 'left') effect.startTime = Math.min(relativeTimeBeats, effect.endTime - 0.1);
+ else if (state.handleType === 'right') effect.endTime = Math.max(effect.startTime + 0.1, relativeTimeBeats);
renderTimeline(); updateProperties();
}
@@ -702,9 +702,9 @@
function updateStats() {
const effectCount = state.sequences.reduce((sum, seq) => sum + seq.effects.length, 0);
- const maxTime = Math.max(0, ...state.sequences.flatMap(seq =>
+ const maxTimeBeats = Math.max(0, ...state.sequences.flatMap(seq =>
seq.effects.map(e => seq.startTime + e.endTime).concat(seq.startTime)));
- dom.stats.innerHTML = `📊 Sequences: ${state.sequences.length} | 🎬 Effects: ${effectCount} | ⏱️ Duration: ${maxTime.toFixed(2)}s`;
+ dom.stats.innerHTML = `📊 Sequences: ${state.sequences.length} | 🎬 Effects: ${effectCount} | ⏱️ Duration: ${maxTimeBeats.toFixed(2)}b (${beatsToTime(maxTimeBeats).toFixed(2)}s)`;
}
async function loadFromURLParams() {
@@ -832,7 +832,7 @@
if (!playbackController || !state.audioBuffer) return;
const containerRect = dom.timelineContent.getBoundingClientRect();
const clickX = e.clientX - containerRect.left + dom.timelineContent.scrollLeft - viewportController.TIMELINE_LEFT_PADDING;
- const clickBeats = clickX / state.pixelsPerSecond;
+ const clickBeats = clickX / state.pixelsPerBeat;
const clickTime = beatsToTime(clickBeats);
const result = playbackController.seekTo(clickBeats, clickTime);
if (result) showMessage(`Seek to ${result.clickTime.toFixed(2)}s (${result.clickBeats.toFixed(2)}b)`, 'success');