Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>{{ title }}</title> | |
| <!-- Prism.js for syntax highlighting (Tomorrow Night matches old_lld_template) --> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" /> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: #0d1117; | |
| color: #c9d1d9; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| .container { | |
| display: flex; | |
| height: 100vh; | |
| flex-direction: column; | |
| } | |
| .header { | |
| background: #161b22; | |
| padding: 22px 34px; | |
| border-bottom: 1px solid #30363d; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 22px; | |
| } | |
| .logo { | |
| height: 45px; | |
| width: auto; | |
| } | |
| .header-title { | |
| font-size: 28px; | |
| color: #f0f6fc; | |
| font-weight: 600; | |
| letter-spacing: 0.7px; | |
| } | |
| .header-subtitle { | |
| font-size: 19px; | |
| color: #8b949e; | |
| margin-top: 3px; | |
| } | |
| .main-content { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| .sidebar { | |
| width: 390px; | |
| background: #0d1117; | |
| border-right: 1px solid #30363d; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .sidebar-header { | |
| padding: 17px 22px; | |
| background: #161b22; | |
| font-size: 17px; | |
| text-transform: uppercase; | |
| letter-spacing: 1.4px; | |
| color: #8b949e; | |
| font-weight: 700; | |
| border-bottom: 1px solid #30363d; | |
| } | |
| .file-tree { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 11px 0; | |
| } | |
| .file-item { | |
| padding: 11px 17px 11px 34px; | |
| cursor: pointer; | |
| font-size: 18px; | |
| display: flex; | |
| align-items: center; | |
| gap: 11px; | |
| transition: background 0.15s; | |
| } | |
| .file-item:hover { | |
| background: #2a2d2e; | |
| } | |
| .file-item.active { | |
| background: #37373d; | |
| color: #58a6ff; | |
| } | |
| .file-icon { | |
| width: 22px; | |
| height: 22px; | |
| font-size: 20px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .editor-section { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .tabs { | |
| background: #161b22; | |
| border-bottom: 1px solid #30363d; | |
| display: flex; | |
| padding: 0 11px; | |
| overflow-x: auto; | |
| } | |
| .tab { | |
| padding: 14px 22px; | |
| font-size: 18px; | |
| cursor: pointer; | |
| border-right: 1px solid #30363d; | |
| background: #161b22; | |
| color: #8b949e; | |
| transition: all 0.15s ease; | |
| white-space: nowrap; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .tab:hover { | |
| color: #c9d1d9; | |
| } | |
| .tab.active { | |
| background: #0d1117; | |
| color: #58a6ff; | |
| border-bottom: 3px solid #58a6ff; | |
| } | |
| .editor-container { | |
| flex: 1; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .code-editor { | |
| flex: 1; | |
| background: #0d1117; | |
| position: relative; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .code-content { | |
| flex: 1; | |
| overflow: auto; | |
| padding: 28px; | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; | |
| font-size: 21px; | |
| line-height: 1.7; | |
| } | |
| .code-content pre { | |
| margin: 0; | |
| padding: 0; | |
| background: transparent; | |
| border: none; | |
| } | |
| .code-content code { | |
| color: #c9d1d9; | |
| text-shadow: none; | |
| } | |
| .line { | |
| display: flex; | |
| min-height: 31px; | |
| } | |
| .line-number { | |
| width: 70px; | |
| text-align: right; | |
| padding-right: 22px; | |
| color: #6e7681; | |
| user-select: none; | |
| flex-shrink: 0; | |
| } | |
| .line-content { | |
| flex: 1; | |
| white-space: pre; | |
| padding-right: 22px; | |
| color: #c9d1d9; | |
| /* Explicit light color for code */ | |
| } | |
| /* Ensure Prism.js tokens are visible */ | |
| .line-content pre { | |
| color: #c9d1d9; | |
| margin: 0; | |
| } | |
| .terminal { | |
| height: 250px; | |
| background: #0d1117; | |
| border-top: 1px solid #30363d; | |
| display: flex; | |
| flex-direction: column; | |
| flex-shrink: 0; | |
| } | |
| .terminal-header { | |
| padding: 11px 22px; | |
| background: #161b22; | |
| font-size: 17px; | |
| display: flex; | |
| align-items: center; | |
| gap: 17px; | |
| border-bottom: 1px solid #30363d; | |
| } | |
| .terminal-tab { | |
| padding: 6px 17px; | |
| cursor: pointer; | |
| border-radius: 6px; | |
| } | |
| .terminal-tab.active { | |
| background: #1e1e1e; | |
| color: #58a6ff; | |
| } | |
| .terminal-content { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 17px 28px; | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; | |
| font-size: 18px; | |
| line-height: 1.7; | |
| } | |
| .terminal-line { | |
| margin-bottom: 6px; | |
| } | |
| .terminal-prompt { | |
| color: #4ec9b0; | |
| } | |
| .terminal-success { | |
| color: #4ec9b0; | |
| } | |
| .terminal-info { | |
| color: #4fc3f7; | |
| } | |
| .terminal-error { | |
| color: #f48771; | |
| } | |
| .terminal-output { | |
| color: #dcdcaa; | |
| padding-left: 0; | |
| } | |
| /* Scrollbar styling */ | |
| .code-content::-webkit-scrollbar, | |
| .terminal-content::-webkit-scrollbar, | |
| .file-tree::-webkit-scrollbar { | |
| width: 14px; | |
| } | |
| .code-content::-webkit-scrollbar-track, | |
| .terminal-content::-webkit-scrollbar-track, | |
| .file-tree::-webkit-scrollbar-track { | |
| background: #0d1117; | |
| } | |
| .code-content::-webkit-scrollbar-thumb, | |
| .terminal-content::-webkit-scrollbar-thumb, | |
| .file-tree::-webkit-scrollbar-thumb { | |
| background: #30363d; | |
| border-radius: 7px; | |
| } | |
| .code-content::-webkit-scrollbar-thumb:hover, | |
| .terminal-content::-webkit-scrollbar-thumb:hover, | |
| .file-tree::-webkit-scrollbar-thumb:hover { | |
| background: #484f58; | |
| } | |
| .typing-cursor { | |
| display: inline-block; | |
| color: #58a6ff; | |
| font-weight: bold; | |
| animation: blink 0.7s infinite; | |
| font-size: 1.2em; | |
| line-height: 1; | |
| margin-left: -1px; | |
| vertical-align: text-bottom; | |
| z-index: 10; | |
| } | |
| @keyframes blink { | |
| 0%, | |
| 50% { | |
| opacity: 1; | |
| } | |
| 51%, | |
| 100% { | |
| opacity: 0; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <div class="header-left"> | |
| {% if logo_path %} | |
| <img src="{{ logo_path }}" alt="Logo" class="logo"> | |
| {% endif %} | |
| <div> | |
| <div class="header-title">{{ title }}</div> | |
| <div class="header-subtitle">{{ subtitle }}</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="main-content"> | |
| <div class="sidebar"> | |
| <div class="sidebar-header">π Project Files</div> | |
| <div class="file-tree" id="fileTree"></div> | |
| </div> | |
| <div class="editor-section"> | |
| <div class="tabs" id="tabsContainer"></div> | |
| <div class="editor-container"> | |
| <div class="code-editor"> | |
| <div class="code-content" id="codeContent"></div> | |
| </div> | |
| </div> | |
| <div class="terminal"> | |
| <div class="terminal-header"> | |
| <div class="terminal-tab active">π» Terminal Output</div> | |
| </div> | |
| <div class="terminal-content" id="terminalContent"> | |
| <div class="terminal-line terminal-prompt">$</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CRITICAL: Prism must load BEFORE our animation script --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-java.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-csharp.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-go.min.js"></script> | |
| <!-- Inline Timeline-Based Animation Engine --> | |
| <script> | |
| /** | |
| * Timeline-Based Animation Engine for LLD Code Walkthrough | |
| * Uses single audio file + timestamps for rock-solid sync | |
| */ | |
| class TimelineAnimator { | |
| constructor(manifest, codeFiles) { | |
| this.manifest = manifest; | |
| this.codeFiles = codeFiles; | |
| this.globalAudio = null; | |
| this.animationFrameId = null; | |
| this.isAnimating = false; | |
| this.currentFile = null; | |
| } | |
| async start() { | |
| if (!this.manifest || !this.manifest.steps || this.manifest.steps.length === 0) { | |
| console.error("No manifest steps found"); | |
| return; | |
| } | |
| console.log("Starting Timeline-Based Animation..."); | |
| this.isAnimating = true; | |
| // Load single audio file | |
| const audioFile = this.manifest.audio_file || this.manifest.steps[0]?.audio_file; | |
| if (!audioFile) { | |
| console.error("No audio file found in manifest"); | |
| return; | |
| } | |
| this.globalAudio = new Audio(audioFile); | |
| // Wait for audio to be ready | |
| await new Promise((resolve, reject) => { | |
| this.globalAudio.oncanplaythrough = resolve; | |
| this.globalAudio.onerror = reject; | |
| this.globalAudio.load(); | |
| }); | |
| // Start playback | |
| await this.globalAudio.play(); | |
| console.log(`Audio playing: ${audioFile}`); | |
| // Start timeline scheduler | |
| this.scheduleTimeline(this.manifest.steps); | |
| } | |
| scheduleTimeline(steps) { | |
| let currentStepIndex = 0; | |
| const stepsState = steps.map(() => ({ started: false, completed: false })); | |
| const tick = () => { | |
| if (!this.globalAudio || this.globalAudio.paused || this.globalAudio.ended) { | |
| console.log("Animation Complete"); | |
| this.isAnimating = false; | |
| return; | |
| } | |
| const currentTime = this.globalAudio.currentTime; | |
| const step = steps[currentStepIndex]; | |
| const state = stepsState[currentStepIndex]; | |
| if (!step) { | |
| return; | |
| } | |
| // Start step when we reach t_start | |
| if (currentTime >= step.t_start && !state.started) { | |
| state.started = true; | |
| console.log(`[${currentTime.toFixed(2)}s] Starting step ${currentStepIndex}: ${step.target_file}`); | |
| this.executeStepVisual(step, currentStepIndex); | |
| } | |
| // Move to next step when we reach t_end | |
| if (currentTime >= step.t_end && state.started && !state.completed) { | |
| state.completed = true; | |
| currentStepIndex++; | |
| } | |
| this.animationFrameId = requestAnimationFrame(tick); | |
| }; | |
| tick(); | |
| } | |
| async executeStepVisual(step, index) { | |
| // 1. Switch File if needed (with pause for context) | |
| if (step.action !== 'show_output' && this.currentFile !== step.target_file) { | |
| this.switchFile(step.target_file); | |
| this.currentFile = step.target_file; | |
| // Pause for context switch | |
| await new Promise(r => setTimeout(r, 1500)); | |
| } | |
| // HANDLE EXECUTION OUTPUT | |
| if (step.action === 'show_output') { | |
| console.log("Showing Output..."); | |
| const terminalContent = document.getElementById('terminalContent'); | |
| // Clear previous prompt if last line is just a prompt | |
| // Actually, let's just append. | |
| const outputText = executionOutput[step.target_file] || executionOutput['Main.java'] || "Execution complete."; | |
| // Add command line | |
| const cmdDiv = document.createElement('div'); | |
| cmdDiv.className = 'terminal-line'; | |
| cmdDiv.innerHTML = `<span class="terminal-prompt">$</span> run ${step.target_file || 'Main'}`; | |
| terminalContent.appendChild(cmdDiv); | |
| // Add output lines with typing effect | |
| const outputLines = outputText.split('\n'); | |
| for (let line of outputLines) { | |
| const div = document.createElement('div'); | |
| div.className = 'terminal-line terminal-output'; | |
| terminalContent.appendChild(div); | |
| // Type the line fast | |
| let buffer = ""; | |
| for (let char of line) { | |
| buffer += char; | |
| div.textContent = buffer; | |
| // small delay | |
| await new Promise(r => setTimeout(r, 10)); | |
| } | |
| // Scroll to bottom | |
| terminalContent.scrollTop = terminalContent.scrollHeight; | |
| await new Promise(r => setTimeout(r, 100)); // slight pause per line | |
| } | |
| // Add new prompt | |
| const newPrompt = document.createElement('div'); | |
| newPrompt.className = 'terminal-line terminal-prompt'; | |
| newPrompt.textContent = "$"; | |
| terminalContent.appendChild(newPrompt); | |
| terminalContent.scrollTop = terminalContent.scrollHeight; | |
| return; // End of step | |
| } | |
| // 2. Type Code based on timeline (For 'type_code' action) | |
| if (step.action === 'wait') return; | |
| const filename = step.target_file; | |
| const fullCode = this.codeFiles[filename]; | |
| if (!fullCode) return; | |
| const allLines = fullCode.split('\n'); | |
| // Check undefined to allow 0 | |
| const startLine = (step.start_line !== undefined) ? step.start_line : 1; | |
| const endLine = (step.end_line !== undefined) ? step.end_line : allLines.length; | |
| // Get target lines | |
| const linesToType = allLines.slice(startLine - 1, endLine); | |
| // Calculate available time window | |
| const availableTime = (step.t_end - step.t_start) * 1000; | |
| // Calculate speed | |
| let totalChars = linesToType.reduce((acc, line) => acc + line.length, 0); | |
| let msPerChar = (availableTime * 0.9) / (totalChars || 1); | |
| // Clamp speed to human-like limits | |
| msPerChar = Math.max(25, Math.min(msPerChar, 90)); | |
| // Typing Loop with Drift Correction | |
| const container = document.getElementById('codeContent'); | |
| const language = this.getLanguage(filename); | |
| // Cursor | |
| let cursor = document.getElementById('typing-cursor'); | |
| if (!cursor) { | |
| cursor = document.createElement('span'); | |
| cursor.id = 'typing-cursor'; | |
| cursor.className = 'typing-cursor'; | |
| cursor.innerHTML = 'β'; | |
| } | |
| const startTime = Date.now(); | |
| let charsTypedTotal = 0; | |
| for (let i = 0; i < linesToType.length; i++) { | |
| const lineIdx = startLine - 1 + i; | |
| const lineText = linesToType[i]; | |
| let lineDiv = container.children[lineIdx]; | |
| if (!lineDiv) continue; | |
| let lineContent = lineDiv.querySelector('.line-content'); | |
| let currentBuffer = ""; | |
| if (lineText.length === 0) { | |
| lineContent.innerHTML = ''; | |
| lineContent.appendChild(cursor); | |
| } | |
| for (let char of lineText) { | |
| currentBuffer += char; | |
| charsTypedTotal++; | |
| let highlighted = ""; | |
| if (Prism && Prism.highlight) { | |
| highlighted = Prism.highlight( | |
| currentBuffer, | |
| Prism.languages[language] || Prism.languages.javascript, | |
| language | |
| ); | |
| } else { | |
| highlighted = currentBuffer | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">"); | |
| } | |
| lineContent.innerHTML = `<pre style="margin:0; display:inline;">${highlighted}</pre>`; | |
| lineContent.appendChild(cursor); | |
| cursor.scrollIntoView({ block: "nearest", inline: "nearest" }); | |
| // DRIFT CORRECTION | |
| const elapsed = Date.now() - startTime; | |
| const targetTime = charsTypedTotal * msPerChar; | |
| const delay = targetTime - elapsed; | |
| if (delay > 0) { | |
| await new Promise(r => setTimeout(r, delay)); | |
| } else { | |
| await new Promise(r => setTimeout(r, 0)); | |
| } | |
| } | |
| } | |
| } | |
| switchFile(filename) { | |
| document.querySelectorAll('.file-item').forEach(item => { | |
| item.classList.toggle('active', item.dataset.file === filename); | |
| }); | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.classList.toggle('active', tab.dataset.file === filename); | |
| }); | |
| // CRITICAL: Clear and re-render code editor for new file | |
| const codeContent = document.getElementById('codeContent'); | |
| codeContent.innerHTML = ''; | |
| const fullCode = this.codeFiles[filename] || ''; | |
| const lines = fullCode.split('\n'); | |
| lines.forEach((lineText, idx) => { | |
| const lineDiv = document.createElement('div'); | |
| lineDiv.className = 'line'; | |
| const lineNum = document.createElement('div'); | |
| lineNum.className = 'line-number'; | |
| lineNum.textContent = idx + 1; | |
| const lineContent = document.createElement('div'); | |
| lineContent.className = 'line-content'; | |
| lineContent.innerHTML = ''; // Start empty for typing animation | |
| lineDiv.appendChild(lineNum); | |
| lineDiv.appendChild(lineContent); | |
| codeContent.appendChild(lineDiv); | |
| }); | |
| } | |
| getLanguage(filename) { | |
| const ext = filename.split('.').pop().toLowerCase(); | |
| return { | |
| 'js': 'javascript', 'py': 'python', 'java': 'java', 'cpp': 'cpp', | |
| 'c': 'c', 'cs': 'csharp', 'go': 'go' | |
| }[ext] || 'javascript'; | |
| } | |
| } | |
| </script> | |
| <!-- Template Variables and Initialization --> | |
| <script> | |
| const codeFiles = {{ code_files | tojson }}; | |
| const manifest = {{ manifest | default (none) | tojson }}; | |
| const autoRun = {{ auto_run | tojson }}; | |
| const executionOutput = {{ execution_output | tojson }}; | |
| // State | |
| let currentFile = null; | |
| let isAnimating = false; | |
| let currentAudio = null; | |
| // Progress tracking: { filename: last_line_fully_typed } | |
| let fileLineProgress = {}; | |
| function initializeUI() { | |
| const fileTree = document.getElementById('fileTree'); | |
| const tabsContainer = document.getElementById('tabsContainer'); | |
| fileTree.innerHTML = ''; | |
| tabsContainer.innerHTML = ''; | |
| const files = Object.keys(codeFiles); | |
| if (files.length === 0) return; | |
| // Initialize progress trackers | |
| files.forEach(f => fileLineProgress[f] = 0); | |
| // Default view | |
| currentFile = files[0]; | |
| files.forEach((filename) => { | |
| const ext = filename.split('.').pop().toLowerCase(); | |
| const iconMap = { | |
| 'py': 'π', 'java': 'β', 'cpp': 'βοΈ', 'c': 'π₯οΈ', | |
| 'js': 'π', 'ts': 'π', 'cs': '#οΈβ£', 'go': 'π·' | |
| }; | |
| const icon = iconMap[ext] || 'π'; | |
| const isActive = filename === currentFile; | |
| const fileItem = document.createElement('div'); | |
| fileItem.className = `file-item ${isActive ? 'active' : ''}`; | |
| fileItem.dataset.file = filename; | |
| fileItem.innerHTML = `<span class="file-icon">${icon}</span><span>${filename}</span>`; | |
| fileItem.addEventListener('click', () => switchFile(filename)); | |
| fileTree.appendChild(fileItem); | |
| const tab = document.createElement('div'); | |
| tab.className = `tab ${isActive ? 'active' : ''}`; | |
| tab.dataset.file = filename; | |
| tab.innerHTML = `<span>${icon}</span><span>${filename}</span>`; | |
| tab.addEventListener('click', () => switchFile(filename)); | |
| tabsContainer.appendChild(tab); | |
| }); | |
| } | |
| function switchFile(filename) { | |
| // Only allow switch if logic permits | |
| currentFile = filename; | |
| // Update Active UI | |
| document.querySelectorAll('.file-item').forEach(item => { | |
| item.classList.toggle('active', item.dataset.file === filename); | |
| }); | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.classList.toggle('active', tab.dataset.file === filename); | |
| }); | |
| // Render content (initially empty or preserved?) | |
| // We need to preserve content if we are switching back. | |
| // But for animation, we usually re-render the whole buffer. | |
| // For now, let's keep it simple: render what we have typed so far. | |
| // (In this simple runner, we might assume sequential, but sidebar implies random access. | |
| // The old template re-renders everything. Let's do that.) | |
| // Actually, fileLineProgress tells us how many lines are *done*. | |
| // But we might be mid-line. For simplicity in this fix, we just clear and re-render if needed. | |
| // But executeStep handles rendering. Manual switching handles static view. | |
| renderStaticView(filename); | |
| } | |
| // Helper to render whatever is currently "done" or initial setup | |
| function renderStaticView(filename) { | |
| const codeContent = document.getElementById('codeContent'); | |
| // If we are animating, we don't want to destroy the DOM unless necessary. | |
| // But if user clicks sidebar, step logic might override. | |
| // We'll prepare the lines as empty divs if not present. | |
| codeContent.innerHTML = ''; | |
| const fullCode = codeFiles[filename] || ''; | |
| const lines = fullCode.split('\n'); | |
| lines.forEach((lineText, idx) => { | |
| const lineDiv = document.createElement('div'); | |
| lineDiv.className = 'line'; | |
| const lineNum = document.createElement('div'); | |
| lineNum.className = 'line-number'; | |
| lineNum.textContent = idx + 1; | |
| const lineContent = document.createElement('div'); | |
| lineContent.className = 'line-content'; | |
| // Initially empty or full? | |
| // If we assume manual pipeline runs from 0 to N, we start empty. | |
| lineContent.innerHTML = ''; | |
| lineDiv.appendChild(lineNum); | |
| lineDiv.appendChild(lineContent); | |
| codeContent.appendChild(lineDiv); | |
| }); | |
| } | |
| function getLanguage(filename) { | |
| const ext = filename.split('.').pop().toLowerCase(); | |
| return { | |
| 'js': 'javascript', 'py': 'python', 'java': 'java', 'cpp': 'cpp', | |
| 'c': 'c', 'cs': 'csharp', 'go': 'go' | |
| }[ext] || 'javascript'; | |
| } | |
| // --- TIMELINE-BASED ANIMATION (See lld_timeline_animation.js) --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Init with first file | |
| initializeUI(); | |
| if (codeFiles) { | |
| // Pre-populate initial empty lines structure | |
| const first = Object.keys(codeFiles)[0]; | |
| if (first) renderStaticView(first); | |
| } | |
| if (autoRun && manifest) { | |
| // Use timeline-based animator | |
| const animator = new TimelineAnimator(manifest, codeFiles); | |
| setTimeout(() => animator.start(), 1000); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |