diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-05 22:04:05 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-05 22:04:05 +0100 |
| commit | f414d6800e7894468ff4ebcd39cc2ed0195fedaf (patch) | |
| tree | d31a1ab948db19f33471d1af0232dbb1021b6b79 /tools/timeline_editor | |
| parent | 58c21d8ea2b9dd19fdf2e5579e58ebbef7a401ff (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
Diffstat (limited to 'tools/timeline_editor')
| -rw-r--r-- | tools/timeline_editor/index.html | 116 |
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') { |
