summaryrefslogtreecommitdiff
path: root/tools/timeline_editor/index.html
diff options
context:
space:
mode:
Diffstat (limited to 'tools/timeline_editor/index.html')
-rw-r--r--tools/timeline_editor/index.html675
1 files changed, 675 insertions, 0 deletions
diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html
new file mode 100644
index 0000000..0d10e4a
--- /dev/null
+++ b/tools/timeline_editor/index.html
@@ -0,0 +1,675 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Timeline Editor - demo.seq</title>
+ <style>
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 20px;
+ }
+
+ .container {
+ max-width: 1400px;
+ margin: 0 auto;
+ }
+
+ header {
+ background: #252526;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ }
+
+ h1 {
+ margin-bottom: 10px;
+ color: #4ec9b0;
+ }
+
+ .controls {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ margin-bottom: 20px;
+ }
+
+ button {
+ background: #0e639c;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ }
+
+ button:hover {
+ background: #1177bb;
+ }
+
+ button:disabled {
+ background: #3c3c3c;
+ cursor: not-allowed;
+ }
+
+ input[type="file"] {
+ display: none;
+ }
+
+ .file-label {
+ background: #0e639c;
+ color: white;
+ padding: 10px 20px;
+ border-radius: 4px;
+ cursor: pointer;
+ display: inline-block;
+ }
+
+ .file-label:hover {
+ background: #1177bb;
+ }
+
+ .timeline-container {
+ background: #252526;
+ border-radius: 8px;
+ padding: 20px;
+ overflow-x: auto;
+ }
+
+ .timeline {
+ position: relative;
+ min-height: 400px;
+ border-left: 2px solid #3c3c3c;
+ }
+
+ .time-markers {
+ position: relative;
+ height: 30px;
+ margin-bottom: 10px;
+ border-bottom: 1px solid #3c3c3c;
+ }
+
+ .time-marker {
+ position: absolute;
+ top: 0;
+ font-size: 12px;
+ color: #858585;
+ }
+
+ .time-marker::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 20px;
+ width: 1px;
+ height: 10px;
+ background: #3c3c3c;
+ }
+
+ .sequence {
+ position: absolute;
+ background: #264f78;
+ border: 2px solid #0e639c;
+ border-radius: 4px;
+ padding: 8px;
+ cursor: move;
+ min-height: 40px;
+ transition: box-shadow 0.2s;
+ }
+
+ .sequence:hover {
+ box-shadow: 0 0 10px rgba(14, 99, 156, 0.5);
+ }
+
+ .sequence.selected {
+ border-color: #4ec9b0;
+ box-shadow: 0 0 10px rgba(78, 201, 176, 0.5);
+ }
+
+ .effect {
+ position: absolute;
+ background: #3a3d41;
+ border: 1px solid #858585;
+ border-radius: 3px;
+ padding: 6px;
+ cursor: move;
+ font-size: 12px;
+ transition: box-shadow 0.2s;
+ }
+
+ .effect:hover {
+ box-shadow: 0 0 8px rgba(133, 133, 133, 0.5);
+ }
+
+ .effect.selected {
+ border-color: #ce9178;
+ box-shadow: 0 0 8px rgba(206, 145, 120, 0.5);
+ }
+
+ .properties-panel {
+ background: #252526;
+ padding: 20px;
+ border-radius: 8px;
+ margin-top: 20px;
+ }
+
+ .properties-panel h2 {
+ margin-bottom: 15px;
+ color: #4ec9b0;
+ }
+
+ .property-group {
+ margin-bottom: 15px;
+ }
+
+ .property-group label {
+ display: block;
+ margin-bottom: 5px;
+ color: #858585;
+ font-size: 14px;
+ }
+
+ .property-group input,
+ .property-group select {
+ width: 100%;
+ padding: 8px;
+ background: #3c3c3c;
+ border: 1px solid #858585;
+ border-radius: 4px;
+ color: #d4d4d4;
+ font-size: 14px;
+ }
+
+ .zoom-controls {
+ margin-bottom: 10px;
+ }
+
+ .stats {
+ background: #1e1e1e;
+ padding: 10px;
+ border-radius: 4px;
+ margin-top: 10px;
+ font-size: 12px;
+ color: #858585;
+ }
+
+ .error {
+ background: #5a1d1d;
+ color: #f48771;
+ padding: 10px;
+ border-radius: 4px;
+ margin-bottom: 10px;
+ }
+
+ .success {
+ background: #1e5231;
+ color: #89d185;
+ padding: 10px;
+ border-radius: 4px;
+ margin-bottom: 10px;
+ }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <header>
+ <h1>📊 Timeline Editor</h1>
+ <p>Interactive editor for demo.seq files</p>
+ </header>
+
+ <div class="controls">
+ <label class="file-label">
+ 📂 Load demo.seq
+ <input type="file" id="fileInput" accept=".seq">
+ </label>
+ <button id="saveBtn" disabled>💾 Save demo.seq</button>
+ <button id="addSequenceBtn" disabled>➕ Add Sequence</button>
+ <button id="deleteBtn" disabled>🗑️ Delete Selected</button>
+ </div>
+
+ <div class="zoom-controls">
+ <label>Zoom: <input type="range" id="zoomSlider" min="10" max="200" value="100" step="10"></label>
+ <span id="zoomLevel">100%</span>
+ <label style="margin-left: 20px">Pixels per second: <span id="pixelsPerSec">100</span></label>
+ </div>
+
+ <div id="messageArea"></div>
+
+ <div class="timeline-container">
+ <div class="time-markers" id="timeMarkers"></div>
+ <div class="timeline" id="timeline"></div>
+ </div>
+
+ <div class="properties-panel" id="propertiesPanel" style="display: none;">
+ <h2>Properties</h2>
+ <div id="propertiesContent"></div>
+ </div>
+
+ <div class="stats" id="stats"></div>
+ </div>
+
+ <script>
+ // Global state
+ let sequences = [];
+ let currentFile = null;
+ let selectedItem = null;
+ let pixelsPerSecond = 100;
+ let isDragging = false;
+ let dragOffset = { x: 0, y: 0 };
+
+ // DOM elements
+ const timeline = document.getElementById('timeline');
+ const fileInput = document.getElementById('fileInput');
+ const saveBtn = document.getElementById('saveBtn');
+ const addSequenceBtn = document.getElementById('addSequenceBtn');
+ const deleteBtn = document.getElementById('deleteBtn');
+ const propertiesPanel = document.getElementById('propertiesPanel');
+ const propertiesContent = document.getElementById('propertiesContent');
+ const messageArea = document.getElementById('messageArea');
+ const zoomSlider = document.getElementById('zoomSlider');
+ const zoomLevel = document.getElementById('zoomLevel');
+ const pixelsPerSecLabel = document.getElementById('pixelsPerSec');
+ const stats = document.getElementById('stats');
+
+ // Parser: demo.seq → JavaScript objects
+ function parseSeqFile(content) {
+ const sequences = [];
+ const lines = content.split('\n');
+ let currentSequence = null;
+
+ for (let line of lines) {
+ line = line.trim();
+
+ // Skip empty lines and comments
+ if (!line || line.startsWith('#')) continue;
+
+ // Parse SEQUENCE line
+ const seqMatch = line.match(/^SEQUENCE\s+([\d.]+)\s+(\d+)$/);
+ if (seqMatch) {
+ currentSequence = {
+ type: 'sequence',
+ startTime: parseFloat(seqMatch[1]),
+ priority: parseInt(seqMatch[2]),
+ effects: []
+ };
+ sequences.push(currentSequence);
+ continue;
+ }
+
+ // Parse EFFECT line
+ const effectMatch = line.match(/^EFFECT\s+(\w+)\s+([\d.]+)\s+([\d.]+)\s+(-?\d+)(?:\s+(.*))?$/);
+ if (effectMatch && currentSequence) {
+ const effect = {
+ type: 'effect',
+ className: effectMatch[1],
+ startTime: parseFloat(effectMatch[2]),
+ endTime: parseFloat(effectMatch[3]),
+ priority: parseInt(effectMatch[4]),
+ args: effectMatch[5] || ''
+ };
+ currentSequence.effects.push(effect);
+ }
+ }
+
+ return sequences;
+ }
+
+ // Serializer: JavaScript objects → demo.seq
+ function serializeSeqFile(sequences) {
+ let output = '# Demo Timeline\n';
+ output += '# Generated by Timeline Editor\n\n';
+
+ for (const seq of sequences) {
+ output += `SEQUENCE ${seq.startTime.toFixed(2)} ${seq.priority}\n`;
+
+ for (const effect of seq.effects) {
+ output += ` EFFECT ${effect.className} ${effect.startTime.toFixed(2)} ${effect.endTime.toFixed(2)} ${effect.priority}`;
+ if (effect.args) {
+ output += ` ${effect.args}`;
+ }
+ output += '\n';
+ }
+ output += '\n';
+ }
+
+ return output;
+ }
+
+ // Render timeline
+ function renderTimeline() {
+ timeline.innerHTML = '';
+ const timeMarkers = document.getElementById('timeMarkers');
+ timeMarkers.innerHTML = '';
+
+ // Calculate max time
+ let maxTime = 30; // Default 30 seconds
+ for (const seq of sequences) {
+ const seqEnd = seq.startTime + 10; // Default sequence duration
+ maxTime = Math.max(maxTime, seqEnd);
+
+ for (const effect of seq.effects) {
+ maxTime = Math.max(maxTime, seq.startTime + effect.endTime);
+ }
+ }
+
+ // Render time markers
+ const timelineWidth = maxTime * pixelsPerSecond;
+ timeline.style.width = `${timelineWidth}px`;
+
+ for (let t = 0; t <= maxTime; t += 1) {
+ const marker = document.createElement('div');
+ marker.className = 'time-marker';
+ marker.style.left = `${t * pixelsPerSecond}px`;
+ marker.textContent = `${t}s`;
+ timeMarkers.appendChild(marker);
+ }
+
+ // Render sequences
+ sequences.forEach((seq, seqIndex) => {
+ const seqDiv = document.createElement('div');
+ seqDiv.className = 'sequence';
+ seqDiv.dataset.index = seqIndex;
+
+ // Calculate sequence duration based on effects
+ let seqDuration = 10; // Default
+ if (seq.effects.length > 0) {
+ seqDuration = Math.max(...seq.effects.map(e => e.endTime));
+ }
+
+ seqDiv.style.left = `${seq.startTime * pixelsPerSecond}px`;
+ seqDiv.style.top = `${seqIndex * 80}px`;
+ seqDiv.style.width = `${seqDuration * pixelsPerSecond}px`;
+ seqDiv.style.height = '70px';
+
+ seqDiv.innerHTML = `
+ <strong>Sequence ${seqIndex + 1}</strong><br>
+ <small>Start: ${seq.startTime.toFixed(2)}s | Priority: ${seq.priority}</small>
+ `;
+
+ if (selectedItem && selectedItem.type === 'sequence' && selectedItem.index === seqIndex) {
+ seqDiv.classList.add('selected');
+ }
+
+ seqDiv.addEventListener('mousedown', (e) => startDrag(e, 'sequence', seqIndex));
+ seqDiv.addEventListener('click', (e) => {
+ e.stopPropagation();
+ selectItem('sequence', seqIndex);
+ });
+
+ timeline.appendChild(seqDiv);
+
+ // Render effects within sequence
+ seq.effects.forEach((effect, effectIndex) => {
+ const effectDiv = document.createElement('div');
+ effectDiv.className = 'effect';
+ effectDiv.dataset.seqIndex = seqIndex;
+ effectDiv.dataset.effectIndex = effectIndex;
+
+ const effectStart = (seq.startTime + effect.startTime) * pixelsPerSecond;
+ const effectWidth = (effect.endTime - effect.startTime) * pixelsPerSecond;
+
+ effectDiv.style.left = `${effectStart}px`;
+ effectDiv.style.top = `${seqIndex * 80 + 25}px`;
+ effectDiv.style.width = `${effectWidth}px`;
+ effectDiv.style.height = '35px';
+
+ effectDiv.innerHTML = `
+ ${effect.className}<br>
+ <small>${effect.startTime.toFixed(1)}-${effect.endTime.toFixed(1)}s</small>
+ `;
+
+ if (selectedItem && selectedItem.type === 'effect' &&
+ selectedItem.seqIndex === seqIndex && selectedItem.effectIndex === effectIndex) {
+ effectDiv.classList.add('selected');
+ }
+
+ effectDiv.addEventListener('mousedown', (e) => {
+ e.stopPropagation();
+ startDrag(e, 'effect', seqIndex, effectIndex);
+ });
+ effectDiv.addEventListener('click', (e) => {
+ e.stopPropagation();
+ selectItem('effect', seqIndex, effectIndex);
+ });
+
+ timeline.appendChild(effectDiv);
+ });
+ });
+
+ updateStats();
+ }
+
+ // Drag handling
+ function startDrag(e, type, seqIndex, effectIndex = null) {
+ e.preventDefault();
+ isDragging = true;
+
+ const rect = e.target.getBoundingClientRect();
+ dragOffset.x = e.clientX - rect.left;
+ dragOffset.y = e.clientY - rect.top;
+
+ selectedItem = { type, index: seqIndex, seqIndex, effectIndex };
+ renderTimeline();
+ updateProperties();
+
+ document.addEventListener('mousemove', onDrag);
+ document.addEventListener('mouseup', stopDrag);
+ }
+
+ function onDrag(e) {
+ if (!isDragging || !selectedItem) return;
+
+ const timelineRect = timeline.getBoundingClientRect();
+ const newX = e.clientX - timelineRect.left - dragOffset.x;
+ const newTime = Math.max(0, newX / pixelsPerSecond);
+
+ if (selectedItem.type === 'sequence') {
+ sequences[selectedItem.index].startTime = Math.round(newTime * 100) / 100;
+ } else if (selectedItem.type === 'effect') {
+ const effect = sequences[selectedItem.seqIndex].effects[selectedItem.effectIndex];
+ const duration = effect.endTime - effect.startTime;
+ effect.startTime = Math.round(newTime * 100) / 100;
+ effect.endTime = effect.startTime + duration;
+ }
+
+ renderTimeline();
+ updateProperties();
+ }
+
+ function stopDrag() {
+ isDragging = false;
+ document.removeEventListener('mousemove', onDrag);
+ document.removeEventListener('mouseup', stopDrag);
+ }
+
+ // Selection
+ function selectItem(type, seqIndex, effectIndex = null) {
+ selectedItem = { type, index: seqIndex, seqIndex, effectIndex };
+ renderTimeline();
+ updateProperties();
+ deleteBtn.disabled = false;
+ }
+
+ // Properties panel
+ function updateProperties() {
+ if (!selectedItem) {
+ propertiesPanel.style.display = 'none';
+ return;
+ }
+
+ propertiesPanel.style.display = 'block';
+
+ if (selectedItem.type === 'sequence') {
+ const seq = sequences[selectedItem.index];
+ propertiesContent.innerHTML = `
+ <div class="property-group">
+ <label>Start Time (seconds)</label>
+ <input type="number" id="propStartTime" value="${seq.startTime}" step="0.1" min="0">
+ </div>
+ <div class="property-group">
+ <label>Priority</label>
+ <input type="number" id="propPriority" value="${seq.priority}" step="1">
+ </div>
+ <button onclick="applyProperties()">Apply</button>
+ `;
+ } else if (selectedItem.type === 'effect') {
+ const effect = sequences[selectedItem.seqIndex].effects[selectedItem.effectIndex];
+ propertiesContent.innerHTML = `
+ <div class="property-group">
+ <label>Effect Class</label>
+ <input type="text" id="propClassName" value="${effect.className}">
+ </div>
+ <div class="property-group">
+ <label>Start Time (relative to sequence)</label>
+ <input type="number" id="propStartTime" value="${effect.startTime}" step="0.1" min="0">
+ </div>
+ <div class="property-group">
+ <label>End Time (relative to sequence)</label>
+ <input type="number" id="propEndTime" value="${effect.endTime}" step="0.1" min="0">
+ </div>
+ <div class="property-group">
+ <label>Priority</label>
+ <input type="number" id="propPriority" value="${effect.priority}" step="1">
+ </div>
+ <div class="property-group">
+ <label>Constructor Arguments</label>
+ <input type="text" id="propArgs" value="${effect.args}">
+ </div>
+ <button onclick="applyProperties()">Apply</button>
+ `;
+ }
+ }
+
+ function applyProperties() {
+ if (!selectedItem) return;
+
+ if (selectedItem.type === 'sequence') {
+ const seq = sequences[selectedItem.index];
+ seq.startTime = parseFloat(document.getElementById('propStartTime').value);
+ seq.priority = parseInt(document.getElementById('propPriority').value);
+ } else if (selectedItem.type === 'effect') {
+ const effect = sequences[selectedItem.seqIndex].effects[selectedItem.effectIndex];
+ effect.className = document.getElementById('propClassName').value;
+ effect.startTime = parseFloat(document.getElementById('propStartTime').value);
+ effect.endTime = parseFloat(document.getElementById('propEndTime').value);
+ effect.priority = parseInt(document.getElementById('propPriority').value);
+ effect.args = document.getElementById('propArgs').value;
+ }
+
+ renderTimeline();
+ showMessage('Properties updated', 'success');
+ }
+
+ // File operations
+ fileInput.addEventListener('change', (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ currentFile = file.name;
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ try {
+ sequences = parseSeqFile(e.target.result);
+ renderTimeline();
+ saveBtn.disabled = false;
+ addSequenceBtn.disabled = false;
+ showMessage(`Loaded ${currentFile} - ${sequences.length} sequences`, 'success');
+ } catch (err) {
+ showMessage(`Error parsing file: ${err.message}`, 'error');
+ }
+ };
+
+ reader.readAsText(file);
+ });
+
+ saveBtn.addEventListener('click', () => {
+ const content = serializeSeqFile(sequences);
+ const blob = new Blob([content], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = currentFile || 'demo.seq';
+ a.click();
+ URL.revokeObjectURL(url);
+ showMessage('File saved', 'success');
+ });
+
+ addSequenceBtn.addEventListener('click', () => {
+ sequences.push({
+ type: 'sequence',
+ startTime: 0,
+ priority: 0,
+ effects: []
+ });
+ renderTimeline();
+ showMessage('New sequence added', 'success');
+ });
+
+ deleteBtn.addEventListener('click', () => {
+ if (!selectedItem) return;
+
+ if (selectedItem.type === 'sequence') {
+ sequences.splice(selectedItem.index, 1);
+ } else if (selectedItem.type === 'effect') {
+ sequences[selectedItem.seqIndex].effects.splice(selectedItem.effectIndex, 1);
+ }
+
+ selectedItem = null;
+ deleteBtn.disabled = true;
+ renderTimeline();
+ updateProperties();
+ showMessage('Item deleted', 'success');
+ });
+
+ // Zoom
+ zoomSlider.addEventListener('input', (e) => {
+ const zoom = parseInt(e.target.value);
+ pixelsPerSecond = zoom;
+ zoomLevel.textContent = `${zoom}%`;
+ pixelsPerSecLabel.textContent = zoom;
+ renderTimeline();
+ });
+
+ // Click outside to deselect
+ timeline.addEventListener('click', () => {
+ selectedItem = null;
+ deleteBtn.disabled = true;
+ renderTimeline();
+ updateProperties();
+ });
+
+ // Utilities
+ function showMessage(text, type) {
+ messageArea.innerHTML = `<div class="${type}">${text}</div>`;
+ setTimeout(() => messageArea.innerHTML = '', 3000);
+ }
+
+ function updateStats() {
+ const effectCount = sequences.reduce((sum, seq) => sum + seq.effects.length, 0);
+ const maxTime = sequences.reduce((max, seq) => {
+ const seqMax = seq.effects.reduce((m, e) => Math.max(m, seq.startTime + e.endTime), seq.startTime);
+ return Math.max(max, seqMax);
+ }, 0);
+
+ stats.innerHTML = `
+ 📊 Sequences: ${sequences.length} |
+ 🎬 Effects: ${effectCount} |
+ ⏱️ Duration: ${maxTime.toFixed(2)}s
+ `;
+ }
+
+ // Initial render
+ renderTimeline();
+ </script>
+</body>
+</html>