diff options
Diffstat (limited to 'tools/timeline_editor')
| -rw-r--r-- | tools/timeline_editor/index.html | 118 |
1 files changed, 104 insertions, 14 deletions
diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html index d2bac0e..ed2f332 100644 --- a/tools/timeline_editor/index.html +++ b/tools/timeline_editor/index.html @@ -46,7 +46,7 @@ .sticky-header { position: sticky; top: 0; background: var(--bg-medium); z-index: 100; padding: 20px 20px 10px 20px; border-bottom: 2px solid var(--bg-light); flex-shrink: 0; } .waveform-container { position: relative; height: 80px; overflow: hidden; background: rgba(0, 0, 0, 0.3); border-radius: var(--radius); cursor: crosshair; } - #cpuLoadCanvas { position: absolute; left: 0; top: 0; height: 80px; display: block; z-index: 1; } + #cpuLoadCanvas { position: absolute; left: 0; bottom: 0; height: 10px; display: block; z-index: 1; } #waveformCanvas { position: absolute; left: 0; top: 0; height: 80px; display: block; z-index: 2; } .playback-indicator { position: absolute; top: 0; left: 0; width: 2px; background: var(--accent-red); box-shadow: 0 0 4px rgba(244, 135, 113, 0.8); pointer-events: none; z-index: 90; display: block; } @@ -77,6 +77,8 @@ .effect { position: absolute; background: #3a3d41; border: 1px solid var(--border-color); border-radius: 3px; padding: 4px 8px; cursor: move; font-size: 11px; transition: box-shadow 0.2s; display: flex; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .effect:hover { box-shadow: 0 0 8px rgba(133, 133, 133, 0.5); background: #45484d; } .effect.selected { border-color: var(--accent-orange); box-shadow: 0 0 8px rgba(206, 145, 120, 0.5); } + .effect.conflict { background: #4a1d1d; border-color: var(--accent-red); box-shadow: 0 0 8px rgba(244, 135, 113, 0.6); } + .effect.conflict:hover { background: #5a2424; } .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; } @@ -338,7 +340,7 @@ } function computeCPULoad() { - if (state.sequences.length === 0) return { maxTime: 60, loads: [] }; + if (state.sequences.length === 0) return { maxTime: 60, loads: [], conflicts: [] }; let maxTime = 60; for (const seq of state.sequences) { for (const effect of seq.effects) { @@ -349,6 +351,10 @@ const resolution = 0.1; const numSamples = Math.ceil(maxTime / resolution); const loads = new Array(numSamples).fill(0); + const conflicts = new Array(numSamples).fill(false); + const postProcessEffects = new Set(['FadeEffect', 'FlashEffect', 'GaussianBlurEffect', 'SolarizeEffect', 'VignetteEffect', 'ChromaAberrationEffect', 'DistortEffect', 'ThemeModulationEffect', 'CNNEffect', 'CNNv2Effect']); + + // Track load for (const seq of state.sequences) { for (const effect of seq.effects) { const startBeat = seq.startTime + effect.startTime; @@ -360,13 +366,76 @@ } } } - return { maxTime, loads, resolution }; + + // Detect within-sequence conflicts (same priority in one sequence) + for (const seq of state.sequences) { + const priorityGroups = {}; + seq.effects.forEach(effect => { + if (postProcessEffects.has(effect.className)) { + if (!priorityGroups[effect.priority]) priorityGroups[effect.priority] = []; + priorityGroups[effect.priority].push(effect); + } + }); + for (const priority in priorityGroups) { + const group = priorityGroups[priority]; + if (group.length > 1) { + for (const effect of group) { + const startBeat = seq.startTime + effect.startTime; + const endBeat = seq.startTime + effect.endTime; + const startIdx = Math.floor(startBeat / resolution); + const endIdx = Math.ceil(endBeat / resolution); + for (let i = startIdx; i < endIdx && i < numSamples; i++) { + conflicts[i] = true; + } + } + } + } + } + + // Detect cross-sequence conflicts (same priority across sequences starting at same time) + const timeGroups = {}; + state.sequences.forEach((seq, idx) => { + const key = seq.startTime.toFixed(2); + if (!timeGroups[key]) timeGroups[key] = []; + timeGroups[key].push(idx); + }); + for (const startTime in timeGroups) { + const seqIndices = timeGroups[startTime]; + if (seqIndices.length > 1) { + const crossPriorityMap = {}; + for (const idx of seqIndices) { + const seq = state.sequences[idx]; + for (const effect of seq.effects) { + if (postProcessEffects.has(effect.className)) { + if (!crossPriorityMap[effect.priority]) crossPriorityMap[effect.priority] = []; + crossPriorityMap[effect.priority].push({ effect, seq }); + } + } + } + for (const priority in crossPriorityMap) { + const group = crossPriorityMap[priority]; + if (group.length > 1) { + for (const { effect, seq } of group) { + const startBeat = seq.startTime + effect.startTime; + const endBeat = seq.startTime + effect.endTime; + const startIdx = Math.floor(startBeat / resolution); + const endIdx = Math.ceil(endBeat / resolution); + for (let i = startIdx; i < endIdx && i < numSamples; i++) { + conflicts[i] = true; + } + } + } + } + } + } + + return { maxTime, loads, conflicts, resolution }; } function renderCPULoad() { const canvas = dom.cpuLoadCanvas, ctx = canvas.getContext('2d'); - const { maxTime, loads, resolution } = computeCPULoad(); - const canvasWidth = maxTime * state.pixelsPerSecond, canvasHeight = 80; + const { maxTime, loads, conflicts, resolution } = computeCPULoad(); + const canvasWidth = maxTime * state.pixelsPerSecond, canvasHeight = 10; canvas.width = canvasWidth; canvas.height = canvasHeight; canvas.style.width = `${canvasWidth}px`; canvas.style.height = `${canvasHeight}px`; ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; ctx.fillRect(0, 0, canvasWidth, canvasHeight); @@ -374,18 +443,20 @@ const barWidth = resolution * state.pixelsPerSecond; for (let i = 0; i < loads.length; i++) { const load = loads[i]; + const hasConflict = conflicts[i]; const normalizedLoad = Math.min(load / 8, 1.0); - let r, g, b; - if (normalizedLoad < 0.5) { - r = Math.floor(normalizedLoad * 2 * 160); - g = 160; + let r, g, b, a = 0.7; + if (hasConflict) { + r = 200; g = 100; b = 90; + } else if (normalizedLoad < 0.5) { + const t = normalizedLoad * 2; + r = Math.floor(120 + t * 50); g = Math.floor(180 + t * 20); b = 140; } else { - r = 160; - g = Math.floor((1 - (normalizedLoad - 0.5) * 2) * 160); + const t = (normalizedLoad - 0.5) * 2; + r = Math.floor(170 + t * 30); g = Math.floor(200 - t * 50); b = 140; } - b = 0; const x = i * barWidth; - ctx.fillStyle = load === 0 ? 'rgba(0, 0, 0, 0.3)' : `rgb(${r}, ${g}, ${b})`; + ctx.fillStyle = load === 0 ? 'rgba(0, 0, 0, 0.3)' : `rgba(${r}, ${g}, ${b}, ${a})`; ctx.fillRect(x, 0, barWidth, canvasHeight); } } @@ -529,8 +600,26 @@ seqDiv.addEventListener('dblclick', e => { e.stopPropagation(); e.preventDefault(); seq._collapsed = !seq._collapsed; renderTimeline(); }); dom.timeline.appendChild(seqDiv); if (!seq._collapsed) { + // Known post-process effects (from seq_compiler.cc) + const postProcessEffects = new Set(['FadeEffect', 'FlashEffect', 'GaussianBlurEffect', 'SolarizeEffect', 'VignetteEffect', 'ChromaAberrationEffect', 'DistortEffect', 'ThemeModulationEffect', 'CNNEffect', 'CNNv2Effect']); + // Detect conflicts: multiple post-process effects with same priority (matches C++ logic) + const conflicts = new Set(); + const priorityGroups = {}; + seq.effects.forEach((effect, idx) => { + if (postProcessEffects.has(effect.className)) { + if (!priorityGroups[effect.priority]) priorityGroups[effect.priority] = []; + priorityGroups[effect.priority].push(idx); + } + }); + for (const priority in priorityGroups) { + const group = priorityGroups[priority]; + if (group.length > 1) { + for (const idx of group) conflicts.add(idx); + } + } seq.effects.forEach((effect, effectIndex) => { const effectDiv = document.createElement('div'); effectDiv.className = 'effect'; + if (conflicts.has(effectIndex)) effectDiv.classList.add('conflict'); effectDiv.dataset.seqIndex = seqIndex; effectDiv.dataset.effectIndex = effectIndex; const effectStart = (seq.startTime + effect.startTime) * state.pixelsPerSecond; const effectWidth = (effect.endTime - effect.startTime) * state.pixelsPerSecond; @@ -540,7 +629,8 @@ const startSec = (effect.startTime * 60.0 / state.bpm).toFixed(1), endSec = (effect.endTime * 60.0 / state.bpm).toFixed(1); const timeDisplay = state.showBeats ? `${startBeat}-${endBeat}b (${startSec}-${endSec}s)` : `${startSec}-${endSec}s (${startBeat}-${endBeat}b)`; 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)'}`; + const conflictWarning = conflicts.has(effectIndex) ? '\n⚠️ CONFLICT: Multiple post-process effects share priority ' + effect.priority : ''; + effectDiv.title = `${effect.className}\n${timeDisplay}\nPriority: ${effect.priority}${conflictWarning}\n${effect.args || '(no args)'}`; if (state.selectedItem && state.selectedItem.type === 'effect' && state.selectedItem.seqIndex === seqIndex && state.selectedItem.effectIndex === effectIndex) effectDiv.classList.add('selected'); const leftHandle = effectDiv.querySelector('.effect-handle.left'); const rightHandle = effectDiv.querySelector('.effect-handle.right'); |
