summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tools/timeline_editor/index.html118
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');