summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-05 22:04:05 +0100
committerskal <pascal.massimino@gmail.com>2026-02-05 22:04:05 +0100
commitf414d6800e7894468ff4ebcd39cc2ed0195fedaf (patch)
treed31a1ab948db19f33471d1af0232dbb1021b6b79
parent58c21d8ea2b9dd19fdf2e5579e58ebbef7a401ff (diff)
feat(timeline-editor): Add editable names and effect resize handles
Feature #1: Editable Sequence Names - Added "Name" field to sequence property panel - User can set custom sequence names (replaces generic "Sequence N") - Name stored in seq.name property - Persists in demo.seq file when saved - Displays in large centered overlay on timeline - Empty name falls back to "Sequence N" display Property Panel: [Name ] ← NEW: editable text field [Start Time ] [Priority ] [Apply] Usage: Select sequence → Edit name in property panel → Apply Feature #2: Effect Resize Handles - Added draggable handles at left/right edges of selected effects - Left handle: Adjusts start time (end time fixed) - Right handle: Adjusts end time (start time fixed) - Handles only visible on selected effects - Smooth dragging with snap-to-beat support Visual Design: - 6px wide teal handles (rgba(78, 201, 176, 0.8)) - Cursor: ew-resize (horizontal resize) - Hover: Expands to 8px, full opacity - z-index: 10 (above effect content) - Border radius matches effect corners Handle Dragging Algorithm: 1. Click handle → startHandleDrag(type: 'left'|'right') 2. mousemove → onHandleDrag: - Calculate newTime from mouse position - Apply snap-to-beat if enabled - Convert to sequence-relative time - If left handle: effect.startTime = min(newTime, endTime - 0.1) - If right handle: effect.endTime = max(startTime + 0.1, newTime) 3. mouseup → stopHandleDrag 4. renderTimeline() → recalculates sequence bounds Technical Details: - Global state: isDraggingHandle, handleType - Separate drag handlers from effect move - Minimum effect duration: 0.1 seconds - Prevents handle overlap (start < end enforced) - Works with beat snapping when enabled - Property panel updates in real-time CSS: .effect-handle { position: absolute; width: 6px; height: 100%; cursor: ew-resize; display: none (only on .effect.selected) } Event Flow: Click handle → stopPropagation (don't trigger effect drag) ↓ startHandleDrag → set isDraggingHandle, handleType ↓ mousemove → onHandleDrag → adjust start/end time ↓ mouseup → stopHandleDrag → cleanup ↓ renderTimeline → sequence bounds recalculate ↓ updateProperties → panel shows new times User Experience: - Select effect → handles appear at edges - Drag left handle → shorten/lengthen from start - Drag right handle → shorten/lengthen from end - Visual feedback: effect resizes in real-time - Sequence box adjusts to fit resized effect - Property panel shows updated times immediately
-rw-r--r--tools/timeline_editor/index.html116
1 files changed, 113 insertions, 3 deletions
diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html
index fb813a5..f5277d8 100644
--- a/tools/timeline_editor/index.html
+++ b/tools/timeline_editor/index.html
@@ -234,6 +234,36 @@
color: #d4d4d4;
}
+ .effect-handle {
+ position: absolute;
+ top: 0;
+ width: 6px;
+ height: 100%;
+ background: rgba(78, 201, 176, 0.8);
+ cursor: ew-resize;
+ display: none;
+ z-index: 10;
+ }
+
+ .effect.selected .effect-handle {
+ display: block;
+ }
+
+ .effect-handle.left {
+ left: 0;
+ border-radius: 3px 0 0 3px;
+ }
+
+ .effect-handle.right {
+ right: 0;
+ border-radius: 0 3px 3px 0;
+ }
+
+ .effect-handle:hover {
+ background: rgba(78, 201, 176, 1);
+ width: 8px;
+ }
+
.properties-panel {
position: fixed;
top: 80px;
@@ -416,6 +446,8 @@
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
let lastActiveSeqIndex = -1;
+ let isDraggingHandle = false;
+ let handleType = null; // 'left' or 'right'
// DOM elements
const timeline = document.getElementById('timeline');
@@ -701,7 +733,11 @@
}
// Show only class name, full info on hover
- effectDiv.innerHTML = `<small>${effect.className}</small>`;
+ effectDiv.innerHTML = `
+ <div class="effect-handle left"></div>
+ <small>${effect.className}</small>
+ <div class="effect-handle right"></div>
+ `;
effectDiv.title = `${effect.className}\n${timeDisplay}\nPriority: ${effect.priority}\n${effect.args || '(no args)'}`;
if (selectedItem && selectedItem.type === 'effect' &&
@@ -709,9 +745,26 @@
effectDiv.classList.add('selected');
}
- effectDiv.addEventListener('mousedown', (e) => {
+ // Handle resizing (only for selected effects)
+ const leftHandle = effectDiv.querySelector('.effect-handle.left');
+ const rightHandle = effectDiv.querySelector('.effect-handle.right');
+
+ leftHandle.addEventListener('mousedown', (e) => {
+ e.stopPropagation();
+ startHandleDrag(e, 'left', seqIndex, effectIndex);
+ });
+
+ rightHandle.addEventListener('mousedown', (e) => {
e.stopPropagation();
- startDrag(e, 'effect', seqIndex, effectIndex);
+ startHandleDrag(e, 'right', seqIndex, effectIndex);
+ });
+
+ effectDiv.addEventListener('mousedown', (e) => {
+ // Only drag if not clicking on a handle
+ if (!e.target.classList.contains('effect-handle')) {
+ e.stopPropagation();
+ startDrag(e, 'effect', seqIndex, effectIndex);
+ }
});
effectDiv.addEventListener('click', (e) => {
e.stopPropagation();
@@ -782,6 +835,58 @@
document.removeEventListener('mouseup', stopDrag);
}
+ // Handle dragging (for resizing effects)
+ function startHandleDrag(e, type, seqIndex, effectIndex) {
+ e.preventDefault();
+ isDraggingHandle = true;
+ handleType = type;
+ selectedItem = { type: 'effect', seqIndex, effectIndex, index: seqIndex };
+ renderTimeline();
+ updateProperties();
+
+ document.addEventListener('mousemove', onHandleDrag);
+ document.addEventListener('mouseup', stopHandleDrag);
+ }
+
+ function onHandleDrag(e) {
+ if (!isDraggingHandle || !selectedItem) return;
+
+ const timelineRect = timeline.getBoundingClientRect();
+ const newX = e.clientX - timelineRect.left;
+ 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;
+ }
+
+ const seq = sequences[selectedItem.seqIndex];
+ const effect = seq.effects[selectedItem.effectIndex];
+
+ // Convert to relative time
+ const relativeTime = newTime - seq.startTime;
+
+ if (handleType === 'left') {
+ // Adjust start time, keep end time fixed
+ effect.startTime = Math.max(0, Math.min(Math.round(relativeTime * 100) / 100, effect.endTime - 0.1));
+ } else if (handleType === 'right') {
+ // Adjust end time, keep start time fixed
+ effect.endTime = Math.max(effect.startTime + 0.1, Math.round(relativeTime * 100) / 100);
+ }
+
+ renderTimeline();
+ updateProperties();
+ }
+
+ function stopHandleDrag() {
+ isDraggingHandle = false;
+ handleType = null;
+ document.removeEventListener('mousemove', onHandleDrag);
+ document.removeEventListener('mouseup', stopHandleDrag);
+ }
+
// Selection
function selectItem(type, seqIndex, effectIndex = null) {
selectedItem = { type, index: seqIndex, seqIndex, effectIndex };
@@ -803,6 +908,10 @@
const seq = sequences[selectedItem.index];
propertiesContent.innerHTML = `
<div class="property-group">
+ <label>Name</label>
+ <input type="text" id="propName" value="${seq.name || ''}" placeholder="Sequence name">
+ </div>
+ <div class="property-group">
<label>Start Time (seconds)</label>
<input type="number" id="propStartTime" value="${seq.startTime}" step="0.1" min="0">
</div>
@@ -845,6 +954,7 @@
if (selectedItem.type === 'sequence') {
const seq = sequences[selectedItem.index];
+ seq.name = document.getElementById('propName').value;
seq.startTime = parseFloat(document.getElementById('propStartTime').value);
seq.priority = parseInt(document.getElementById('propPriority').value);
} else if (selectedItem.type === 'effect') {