@amg555/frameforge-mcp 2.3.4 → 2.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +2 -2
  3. package/out/frames/f000000.jpg +0 -0
  4. package/out/frames/f000001.jpg +0 -0
  5. package/out/frames/f000002.jpg +0 -0
  6. package/out/frames/f000003.jpg +0 -0
  7. package/out/frames/f000004.jpg +0 -0
  8. package/out/frames/f000005.jpg +0 -0
  9. package/out/frames/f000006.jpg +0 -0
  10. package/out/frames/f000007.jpg +0 -0
  11. package/out/frames/f000008.jpg +0 -0
  12. package/out/frames/f000009.jpg +0 -0
  13. package/out/frames/f000010.jpg +0 -0
  14. package/out/frames/f000011.jpg +0 -0
  15. package/out/frames/f000012.jpg +0 -0
  16. package/out/frames/f000013.jpg +0 -0
  17. package/out/frames/f000014.jpg +0 -0
  18. package/out/frames/f000015.jpg +0 -0
  19. package/out/frames/f000016.jpg +0 -0
  20. package/out/frames/f000017.jpg +0 -0
  21. package/out/frames/f000018.jpg +0 -0
  22. package/out/frames/f000019.jpg +0 -0
  23. package/out/frames/f000020.jpg +0 -0
  24. package/out/frames/f000021.jpg +0 -0
  25. package/out/frames/f000022.jpg +0 -0
  26. package/out/frames/f000023.jpg +0 -0
  27. package/out/frames/f000024.jpg +0 -0
  28. package/out/frames/f000025.jpg +0 -0
  29. package/out/frames/f000026.jpg +0 -0
  30. package/out/frames/f000027.jpg +0 -0
  31. package/out/frames/f000028.jpg +0 -0
  32. package/out/frames/f000029.jpg +0 -0
  33. package/out/frames/f000030.jpg +0 -0
  34. package/out/frames/f000031.jpg +0 -0
  35. package/out/frames/f000032.jpg +0 -0
  36. package/out/frames/f000033.jpg +0 -0
  37. package/out/frames/f000034.jpg +0 -0
  38. package/out/frames/f000035.jpg +0 -0
  39. package/out/frames/f000036.jpg +0 -0
  40. package/out/frames/f000037.jpg +0 -0
  41. package/out/frames/f000038.jpg +0 -0
  42. package/out/frames/f000039.jpg +0 -0
  43. package/out/frames/f000040.jpg +0 -0
  44. package/out/frames/f000041.jpg +0 -0
  45. package/out/frames/f000042.jpg +0 -0
  46. package/out/frames/f000043.jpg +0 -0
  47. package/out/frames/f000044.jpg +0 -0
  48. package/out/frames/f000045.jpg +0 -0
  49. package/out/frames/f000046.jpg +0 -0
  50. package/out/frames/f000047.jpg +0 -0
  51. package/package.json +2 -2
  52. package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +25 -12
  53. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.css +2 -2
  54. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +1 -1
  55. package/scripts/deep-validate.js +2 -2
  56. package/scripts/html-renderer/new-project.mjs +2 -2
  57. package/src/cli/make.js +6 -0
  58. package/src/harness/bin/harness.mjs +20 -7
  59. package/src/mcp-server/server.js +57 -7
  60. package/src/video-renderer/renderer.js +268 -60
  61. package/src/video-renderer/scene-animator.js +1241 -0
  62. package/src/workflows/assets.js +23 -2
  63. package/src/workflows/render.js +32 -9
  64. package/src/workflows/script.js +2 -0
@@ -0,0 +1,1241 @@
1
+ 'use strict';
2
+ /**
3
+ * Scene Animator — Dynamic, Context-Driven Animation Engine
4
+ *
5
+ * Generates self-contained HTML/Canvas animation scenes tailored to the video's
6
+ * topic, style, language, and script narrative. Every scene is a pure function of t
7
+ * under the FrameForge harness contract.
8
+ *
9
+ * Supported Aesthetics:
10
+ * - cyber_tech: Neon cyan/magenta, perspective grids, circuit lines, neural mesh
11
+ * - cinematic_documentary: Warm sepia/gold, dust motes, archival maps, monuments, royal seals
12
+ * - finance_executive: Deep sapphire, emerald bull green, candlestick charts, tickers
13
+ * - retro_synthwave: Indigo sunset, hot pink, horizon sun, 80s scanlines, laser grids
14
+ * - nature_organic: Forest emerald, sage, 3D DNA double-helix, floating pollen spores
15
+ * - hype_sports: Asphalt black, crimson/neon lime, angled speed streaks, acoustic shockwaves
16
+ * - minimal_luxury: Matte black, champagne/rose gold, 3D wireframe polyhedra, architectural dots
17
+ * - breaking_news: Newsroom slate, alert red, rotating 3D wireframe globe, radar sweeps
18
+ * - cosmic_space: Deep nebula, 3D Earth, orbiting satellites, microwave wavefronts
19
+ * - warm_creative: Terracotta, sunshine amber, organic morphing geometry, playful springs
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ // ── Multilingual Font Resolution by Script ─────────────────────────────────────
26
+ const REGIONAL_FONTS = {
27
+ malayalam: `'Nirmala UI', 'Noto Sans Malayalam', 'Malayalam Sangam MN', 'Kartika', 'Meera', 'Manjari'`,
28
+ devanagari: `'Nirmala UI', 'Noto Sans Devanagari', 'Mangal', 'Aparajita'`,
29
+ tamil: `'Nirmala UI', 'Noto Sans Tamil', 'Latha', 'Vijaya'`,
30
+ telugu: `'Nirmala UI', 'Noto Sans Telugu', 'Gautami', 'Vani'`,
31
+ arabic: `'Segoe UI', 'Amiri', 'Noto Sans Arabic', 'Traditional Arabic'`,
32
+ cjk: `'Yu Gothic', 'Meiryo', 'Hiragino Sans', 'Microsoft YaHei', 'Malgun Gothic', 'Noto Sans CJK'`,
33
+ };
34
+
35
+ function getScriptFontStack(text = '', language = '') {
36
+ const lang = (language || '').toLowerCase().slice(0, 2);
37
+ if (lang === 'ml' || /[\u0D00-\u0D7F]/.test(text)) return REGIONAL_FONTS.malayalam;
38
+ if (lang === 'hi' || lang === 'mr' || lang === 'ne' || /[\u0900-\u097F]/.test(text)) return REGIONAL_FONTS.devanagari;
39
+ if (lang === 'ta' || /[\u0B80-\u0BFF]/.test(text)) return REGIONAL_FONTS.tamil;
40
+ if (lang === 'te' || /[\u0C00-\u0C7F]/.test(text)) return REGIONAL_FONTS.telugu;
41
+ if (lang === 'ar' || lang === 'ur' || /[\u0600-\u06FF]/.test(text)) return REGIONAL_FONTS.arabic;
42
+ if (lang === 'ja' || lang === 'zh' || lang === 'ko' || /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff]/.test(text)) return REGIONAL_FONTS.cjk;
43
+ return null;
44
+ }
45
+
46
+ // ── Aesthetic Definitions ──────────────────────────────────────────────────────
47
+ const AESTHETICS = {
48
+ cyber_tech: {
49
+ name: 'Cyber Tech',
50
+ palettes: [
51
+ { bg0: '#142036', bg1: '#0b111c', bg2: '#05080f', accent: '#00f0ff', accent2: '#d946ef', cardBg: 'rgba(9, 16, 29, 0.85)', cardBorder: 'rgba(0, 240, 255, 0.3)', tag: 'CYBER TECH' },
52
+ { bg0: '#1a1033', bg1: '#0e081c', bg2: '#05030b', accent: '#818cf8', accent2: '#38bdf8', cardBg: 'rgba(16, 11, 31, 0.85)', cardBorder: 'rgba(129, 140, 248, 0.3)', tag: 'SYSTEM MATRIX' },
53
+ { bg0: '#082522', bg1: '#041614', bg2: '#020908', accent: '#10b981', accent2: '#06b6d4', cardBg: 'rgba(6, 26, 23, 0.85)', cardBorder: 'rgba(16, 185, 129, 0.3)', tag: 'NEURAL LINK' },
54
+ ],
55
+ fontFamily: (script) => script ? `${script}, 'Rajdhani', -apple-system, sans-serif` : `'Rajdhani', 'Segoe UI', -apple-system, sans-serif`,
56
+ fontDisplayWeight: '800',
57
+ backgroundType: 'grid_perspective',
58
+ springTension: 200,
59
+ springFriction: 13,
60
+ },
61
+ cinematic_documentary: {
62
+ name: 'Cinematic Documentary',
63
+ palettes: [
64
+ { bg0: '#261b11', bg1: '#150e09', bg2: '#0a0604', accent: '#d4af37', accent2: '#f59e0b', cardBg: 'rgba(28, 19, 13, 0.88)', cardBorder: 'rgba(212, 175, 55, 0.35)', tag: 'HISTORICAL ARCHIVE' },
65
+ { bg0: '#1e1c18', bg1: '#11100e', bg2: '#080807', accent: '#e2d9c8', accent2: '#d4af37', cardBg: 'rgba(22, 20, 17, 0.88)', cardBorder: 'rgba(226, 217, 200, 0.3)', tag: 'CHRONICLE' },
66
+ { bg0: '#1a1f1d', bg1: '#0e1211', bg2: '#060807', accent: '#5eead4', accent2: '#e2d9c8', cardBg: 'rgba(18, 23, 21, 0.88)', cardBorder: 'rgba(94, 234, 212, 0.3)', tag: 'HERITAGE' },
67
+ ],
68
+ fontFamily: (script) => script ? `${script}, 'Cinzel', 'Georgia', serif` : `'Cinzel', 'Georgia', 'Times New Roman', serif`,
69
+ fontDisplayWeight: '700',
70
+ backgroundType: 'particles_dust',
71
+ springTension: 120,
72
+ springFriction: 18,
73
+ },
74
+ finance_executive: {
75
+ name: 'Finance Executive',
76
+ palettes: [
77
+ { bg0: '#091e2b', bg1: '#041017', bg2: '#02070a', accent: '#10b981', accent2: '#38bdf8', cardBg: 'rgba(7, 22, 33, 0.9)', cardBorder: 'rgba(16, 185, 129, 0.35)', tag: 'MARKET INTELLIGENCE' },
78
+ { bg0: '#161426', bg1: '#0b0a14', bg2: '#040308', accent: '#6366f1', accent2: '#10b981', cardBg: 'rgba(15, 13, 28, 0.9)', cardBorder: 'rgba(99, 102, 241, 0.35)', tag: 'QUANT ANALYSIS' },
79
+ { bg0: '#211516', bg1: '#120a0b', bg2: '#060303', accent: '#ef4444', accent2: '#f59e0b', cardBg: 'rgba(24, 13, 14, 0.9)', cardBorder: 'rgba(239, 68, 68, 0.35)', tag: 'VOLATILITY DESK' },
80
+ ],
81
+ fontFamily: (script) => script ? `${script}, 'Inter', 'Helvetica Neue', sans-serif` : `'Inter', 'Helvetica Neue', Arial, sans-serif`,
82
+ fontDisplayWeight: '800',
83
+ backgroundType: 'candlestick_charts',
84
+ springTension: 170,
85
+ springFriction: 14,
86
+ },
87
+ retro_synthwave: {
88
+ name: 'Retro Synthwave',
89
+ palettes: [
90
+ { bg0: '#2d0938', bg1: '#16041c', bg2: '#08010a', accent: '#ff2a85', accent2: '#00f0ff', cardBg: 'rgba(35, 7, 45, 0.85)', cardBorder: 'rgba(255, 42, 133, 0.45)', tag: 'SYNTH RUNNER' },
91
+ { bg0: '#140c38', bg1: '#08041c', bg2: '#02010a', accent: '#a855f7', accent2: '#f43f5e', cardBg: 'rgba(18, 10, 48, 0.85)', cardBorder: 'rgba(168, 85, 247, 0.45)', tag: 'NEON HORIZON' },
92
+ ],
93
+ fontFamily: (script) => script ? `${script}, 'Orbitron', 'Courier New', monospace` : `'Orbitron', 'Courier New', monospace`,
94
+ fontDisplayWeight: '800',
95
+ backgroundType: 'retro_horizon',
96
+ springTension: 190,
97
+ springFriction: 11,
98
+ },
99
+ nature_organic: {
100
+ name: 'Nature Organic',
101
+ palettes: [
102
+ { bg0: '#0c2617', bg1: '#05140b', bg2: '#020804', accent: '#22c55e', accent2: '#86efac', cardBg: 'rgba(9, 28, 17, 0.85)', cardBorder: 'rgba(34, 197, 94, 0.35)', tag: 'BIOSPHERE' },
103
+ { bg0: '#0c2326', bg1: '#051314', bg2: '#020708', accent: '#14b8a6', accent2: '#a7f3d0', cardBg: 'rgba(9, 26, 28, 0.85)', cardBorder: 'rgba(20, 184, 166, 0.35)', tag: 'HYDRO WAVE' },
104
+ ],
105
+ fontFamily: (script) => script ? `${script}, 'DM Sans', -apple-system, sans-serif` : `'DM Sans', -apple-system, sans-serif`,
106
+ fontDisplayWeight: '700',
107
+ backgroundType: 'organic_ripples',
108
+ springTension: 140,
109
+ springFriction: 15,
110
+ },
111
+ hype_sports: {
112
+ name: 'Hype Sports',
113
+ palettes: [
114
+ { bg0: '#2b0c10', bg1: '#170507', bg2: '#0a0102', accent: '#ef4444', accent2: '#f59e0b', cardBg: 'rgba(32, 8, 12, 0.9)', cardBorder: 'rgba(239, 68, 68, 0.45)', tag: 'HIGH VOLTAGE' },
115
+ { bg0: '#262208', bg1: '#141203', bg2: '#080701', accent: '#84cc16', accent2: '#ef4444', cardBg: 'rgba(28, 25, 6, 0.9)', cardBorder: 'rgba(132, 204, 22, 0.45)', tag: 'MAX INTENSITY' },
116
+ { bg0: '#141416', bg1: '#0c0c0d', bg2: '#040405', accent: '#ffffff', accent2: '#ef4444', cardBg: 'rgba(18, 18, 20, 0.9)', cardBorder: 'rgba(255, 255, 255, 0.35)', tag: 'OVERDRIVE' },
117
+ ],
118
+ fontFamily: (script) => script ? `${script}, 'Anton', 'Teko', 'Arial Black', sans-serif` : `'Anton', 'Teko', 'Arial Black', sans-serif`,
119
+ fontDisplayWeight: '900',
120
+ backgroundType: 'speed_streaks',
121
+ springTension: 240,
122
+ springFriction: 12,
123
+ },
124
+ minimal_luxury: {
125
+ name: 'Minimal Luxury',
126
+ palettes: [
127
+ { bg0: '#1a1a1c', bg1: '#101012', bg2: '#080809', accent: '#ffffff', accent2: '#a87954', cardBg: 'rgba(18, 18, 20, 0.8)', cardBorder: 'rgba(255, 255, 255, 0.15)', tag: 'ATELIER' },
128
+ { bg0: '#1c1618', bg1: '#110d0f', bg2: '#080506', accent: '#f43f5e', accent2: '#e2e8f0', cardBg: 'rgba(20, 15, 17, 0.8)', cardBorder: 'rgba(244, 63, 94, 0.25)', tag: 'COUTURE' },
129
+ { bg0: '#13181a', bg1: '#0b0f10', bg2: '#050708', accent: '#38bdf8', accent2: '#cbd5e1', cardBg: 'rgba(14, 19, 21, 0.8)', cardBorder: 'rgba(56, 189, 248, 0.25)', tag: 'ARCHITECT' },
130
+ ],
131
+ fontFamily: (script) => script ? `${script}, 'Plus Jakarta Sans', 'Poppins', -apple-system, sans-serif` : `'Plus Jakarta Sans', 'Poppins', -apple-system, sans-serif`,
132
+ fontDisplayWeight: '600',
133
+ backgroundType: 'geometric_silhouettes',
134
+ springTension: 130,
135
+ springFriction: 17,
136
+ },
137
+ breaking_news: {
138
+ name: 'Breaking News',
139
+ palettes: [
140
+ { bg0: '#240f13', bg1: '#14070a', bg2: '#080203', accent: '#dc2626', accent2: '#ffffff', cardBg: 'rgba(26, 10, 14, 0.92)', cardBorder: 'rgba(220, 38, 38, 0.45)', tag: 'BREAKING NEWS' },
141
+ { bg0: '#101c2e', bg1: '#08101c', bg2: '#03060d', accent: '#38bdf8', accent2: '#dc2626', cardBg: 'rgba(11, 22, 38, 0.92)', cardBorder: 'rgba(56, 189, 248, 0.4)', tag: 'SPECIAL REPORT' },
142
+ ],
143
+ fontFamily: (script) => script ? `${script}, 'Oswald', 'Franklin Gothic Medium', sans-serif` : `'Oswald', 'Franklin Gothic Medium', sans-serif`,
144
+ fontDisplayWeight: '700',
145
+ backgroundType: 'radar_globe',
146
+ springTension: 220,
147
+ springFriction: 14,
148
+ },
149
+ cosmic_space: {
150
+ name: 'Cosmic Space',
151
+ palettes: [
152
+ { bg0: '#131138', bg1: '#09081f', bg2: '#03020a', accent: '#38bdf8', accent2: '#c084fc', cardBg: 'rgba(12, 10, 38, 0.88)', cardBorder: 'rgba(56, 189, 248, 0.4)', tag: 'ORBITAL MECHANICS' },
153
+ { bg0: '#0d1f36', bg1: '#06101c', bg2: '#02050a', accent: '#00f0ff', accent2: '#38bdf8', cardBg: 'rgba(8, 20, 38, 0.88)', cardBorder: 'rgba(0, 240, 255, 0.4)', tag: 'DEEP SPACE' },
154
+ { bg0: '#1f0d2b', bg1: '#100617', bg2: '#060209', accent: '#f43f5e', accent2: '#e879f9', cardBg: 'rgba(20, 8, 28, 0.88)', cardBorder: 'rgba(244, 63, 94, 0.4)', tag: 'SUPERNOVA' },
155
+ ],
156
+ fontFamily: (script) => script ? `${script}, 'Space Grotesk', -apple-system, sans-serif` : `'Space Grotesk', 'Segoe UI', -apple-system, sans-serif`,
157
+ fontDisplayWeight: '700',
158
+ backgroundType: 'cosmic_stars',
159
+ springTension: 150,
160
+ springFriction: 14,
161
+ },
162
+ warm_creative: {
163
+ name: 'Warm Creative',
164
+ palettes: [
165
+ { bg0: '#2b1910', bg1: '#190d07', bg2: '#0a0502', accent: '#f97316', accent2: '#fbbf24', cardBg: 'rgba(32, 18, 11, 0.85)', cardBorder: 'rgba(249, 115, 22, 0.35)', tag: 'CREATIVE LAB' },
166
+ { bg0: '#2b1020', bg1: '#180711', bg2: '#090206', accent: '#f43f5e', accent2: '#fbbf24', cardBg: 'rgba(31, 10, 22, 0.85)', cardBorder: 'rgba(244, 63, 94, 0.35)', tag: 'VIBRANT LIFE' },
167
+ ],
168
+ fontFamily: (script) => script ? `${script}, 'Outfit', 'Comfortaa', -apple-system, sans-serif` : `'Outfit', 'Comfortaa', -apple-system, sans-serif`,
169
+ fontDisplayWeight: '700',
170
+ backgroundType: 'particles_dust',
171
+ springTension: 160,
172
+ springFriction: 12,
173
+ },
174
+ };
175
+
176
+ // ── Smart Aesthetic Inference ──────────────────────────────────────────────────
177
+ /**
178
+ * Detect the overall aesthetic from prompt/script metadata, topic, style, and tone.
179
+ */
180
+ function detectAesthetic({
181
+ topic = '',
182
+ style = '',
183
+ theme = '',
184
+ title = '',
185
+ caption = '',
186
+ narration = '',
187
+ visualDescription = '',
188
+ language = '',
189
+ musicMood = '',
190
+ } = {}) {
191
+ // Explicit override wins
192
+ const explicit = (theme || style || '').toLowerCase().trim();
193
+ if (AESTHETICS[explicit]) return explicit;
194
+ if (/cyber|tech|code|ai|crypto|hacker|futur/i.test(explicit)) return 'cyber_tech';
195
+ if (/doc|history|ancient|story|cinema|kerala|temple|culture/i.test(explicit)) return 'cinematic_documentary';
196
+ if (/finan|market|stock|money|invest|crypto|business/i.test(explicit)) return 'finance_executive';
197
+ if (/synth|retro|80s|vapor|arcade|game|anime/i.test(explicit)) return 'retro_synthwave';
198
+ if (/nature|green|earth|eco|wild|health|meditat|biology/i.test(explicit)) return 'nature_organic';
199
+ if (/sport|hype|fitness|gym|action|extreme|battle|speed/i.test(explicit)) return 'hype_sports';
200
+ if (/luxury|minimal|design|architect|clean|fashion|apple/i.test(explicit)) return 'minimal_luxury';
201
+ if (/news|alert|urgent|crisis|report|scandal/i.test(explicit)) return 'breaking_news';
202
+ if (/space|cosmos|universe|planet|astro|quantum/i.test(explicit)) return 'cosmic_space';
203
+
204
+ // Keyword scan across all textual signals (English + Multilingual)
205
+ const combined = `${topic} ${title} ${visualDescription} ${narration} ${caption} ${musicMood}`.toLowerCase();
206
+
207
+ // Cosmic Space / Quantum / Orbital
208
+ if (/(?:^|[^\w])(quantum|astronomy|galaxy|universe|space|mars|satellite|telescope|cosmos|interstellar|orbit|gps|satellites)(?:[^\w]|$)/.test(combined)
209
+ || /പ്രപഞ്ച|ബഹിരാകാശ|നക്ഷത്ര|ഗ്രഹം|ഉപഗ്രഹ|ജ്യോതിശാസ്ത്ര|अंतरिक्ष|ब्रह्मांड|तारा|விண்வெளி|பிரபஞ்சம்/.test(combined)) {
210
+ return 'cosmic_space';
211
+ }
212
+ // Cinematic Documentary / History / Heritage / Culture
213
+ if (/(?:^|[^\w])(history|ancient|king|temple|civilization|century|heritage|monument|museum|kerala|parashurama|samuthiri|thiruvithamkoor|tradition|folklore|epic|archive|biography|documentary|empire)(?:[^\w]|$)/.test(combined)
214
+ || /ചരിത്ര|പുരാതന|സാമ്രാജ്യ|രാജവംശ|ക്ഷേത്ര|പാരമ്പര്യ|സംസ്കാര|കേരള|നാട്ടുരാജ്യ|ഇതിഹാസ|സ്മാരക|വാസ്തുവിദ്യ|इतिहास|प्राचीन|संस्कृति|साम्राज्य|मंदिर|धरोहर|வரலாறு|பழங்கால|பேரரசு|கோவில்|பாரம்பரியம்/.test(combined)) {
215
+ return 'cinematic_documentary';
216
+ }
217
+ // Finance / Markets / Wealth
218
+ if (/(?:^|[^\w])(stock|crypto|bitcoin|market|invest|trading|inflation|economy|gdp|finance|fund|valuation|revenue|profit|banking|shares|trillion|billion|wealth)(?:[^\w]|$)/.test(combined)
219
+ || /നിക്ഷേപ|ഓഹരി|വിപണി|പണം|സാമ്പത്തിക|ലാഭം|ബാങ്ക്|സമ്പത്ത്|वित्त|बाज़ार|शेयर|निवेश|व्यापार|धन|मुதலீடு|பங்குச்சந்தை|பொருளாதாரம்/.test(combined)) {
220
+ return 'finance_executive';
221
+ }
222
+ // Cyber / Tech / Software
223
+ if (/(?:^|[^\w])(code|algorithm|software|developer|api|python|javascript|rust|neural|llm|compiler|linux|server|cloud|github|framework|database|terminal|cyber|robot)(?:[^\w]|$)/.test(combined)
224
+ || /സാങ്കേതിക|കോഡിംഗ്|സോഫ്റ്റ്‌വെയർ|കമ്പ്യൂട്ടർ|പ്രോഗ്രാമിംഗ്|तकनीक|सॉफ्टवेयर|कंप्यूटर|தொழில்நுட்பம்|மென்பொருள்/.test(combined)) {
225
+ return 'cyber_tech';
226
+ }
227
+ // Retro Synthwave
228
+ if (/(?:^|[^\w])(synthwave|80s|neon|retrowave|arcade|pixel|game|vaporwave|outrun|retro)(?:[^\w]|$)/.test(combined)) {
229
+ return 'retro_synthwave';
230
+ }
231
+ // Nature / Organic
232
+ if (/(?:^|[^\w])(nature|forest|ocean|plant|wildlife|animal|tree|climate|carbon|organic|meditation|ayurveda|ecology|water|river|green)(?:[^\w]|$)/.test(combined)
233
+ || /പ്രകൃതി|വനം|കാട്|പരിസ്ഥിതി|ആയുർവേദ|ജീവൻ|പച്ചപ്പ്|നദി|പ്രകൃതിദത്ത|प्रकृति|जंगल|पर्यावरण|आयुर्वेद|இயற்கை|சுற்றுச்சூழல்/.test(combined)) {
234
+ return 'nature_organic';
235
+ }
236
+ // Hype Sports
237
+ if (/(?:^|[^\w])(sports|workout|gym|athlete|championship|extreme|speed|race|muscle|football|cricket|danger|vs|versus|hype|adrenaline)(?:[^\w]|$)/.test(combined)
238
+ || /കായിക|മത്സര|വേഗത|വ്യായാമം|ചാമ്പ്യൻ|खेल|कसरत|प्रतियोगिता|விளையாட்டு/.test(combined)) {
239
+ return 'hype_sports';
240
+ }
241
+ // Breaking News
242
+ if (/(?:^|[^\w])(breaking|news|headline|reported|urgent|scandal|investigation|exclusive|update|alert|police|crisis)(?:[^\w]|$)/.test(combined)
243
+ || /വാർത്ത|അടിയന്തര|ബ്രേക്കിംഗ്|അറിയിപ്പ്|समाचार|ताज़ा खबर|आपातकाल|செய்தி|முக்கிய செய்தி/.test(combined)) {
244
+ return 'breaking_news';
245
+ }
246
+ // Minimal Luxury
247
+ if (/(?:^|[^\w])(luxury|minimal|architect|premium|bespoke|couture|gallery|elegance|aesthetic|curated|pure)(?:[^\w]|$)/.test(combined)) {
248
+ return 'minimal_luxury';
249
+ }
250
+
251
+ // Fallback by music mood if available
252
+ if (musicMood === 'energetic') return 'hype_sports';
253
+ if (musicMood === 'tense') return 'breaking_news';
254
+ if (musicMood === 'dramatic') return 'cinematic_documentary';
255
+ if (musicMood === 'calm') return 'nature_organic';
256
+ if (musicMood === 'mysterious') return 'cosmic_space';
257
+
258
+ // Default to versatile cyber_tech for general modern explainers
259
+ return 'cyber_tech';
260
+ }
261
+
262
+ // ── Component Theme Detection ──────────────────────────────────────────────────
263
+ /**
264
+ * Detect the graphic entity to render on the full canvas for a given scene.
265
+ */
266
+ function detectComponentTheme({ visualDescription = '', caption = '', narration = '', topic = '', imagePath = null, aesthetic = 'cyber_tech' } = {}) {
267
+ if (imagePath) return 'image';
268
+ const text = `${visualDescription} ${caption} ${narration} ${topic}`.toLowerCase();
269
+
270
+ // Celestial / Orbiting satellites (e.g. GPS, orbital mechanics, planetary science)
271
+ if (/(?:^|[^\w])(orbit|satellite|satellites|gps|planet|earth|mars|moon|galaxy|universe|solar|astronomy|gravity|space|telescope|interstellar)(?:[^\w]|$)/.test(text)
272
+ || /ബഹിരാകാശ|ഗ്രഹം|ഉപഗ്രഹ|ഭൂമി|നക്ഷത്ര|പ്രപഞ്ച|अंतरिक्ष|ग्रह|तारा|விண்வெளி/.test(text)) {
273
+ return 'celestial_orbit';
274
+ }
275
+
276
+ // DNA / Organic cellular helix
277
+ if (/(?:^|[^\w])(dna|helix|gene|cell|molecule|biology|organism|protein|virus|bacteria|medical|genetic|chromosome)(?:[^\w]|$)/.test(text)
278
+ || /ഡിഎൻഎ|കോശം|തന്മാത്ര|ജീവശാസ്ത്ര|डीएनए|कोशिका/.test(text)) {
279
+ return 'dna_helix';
280
+ }
281
+
282
+ // Radar Globe / World Map
283
+ if (/(?:^|[^\w])(globe|map|world|geography|radar|international|continent|territory|country|border|location)(?:[^\w]|$)/.test(text)
284
+ || /ഭൂപടം|ലോകം|ഭൂഖണ്ഡം|ഭൂപട|नक्शा|ग्लोब/.test(text)) {
285
+ return 'radar_globe';
286
+ }
287
+
288
+ // Quote / Speech
289
+ if (/(?:^|[^\w])(quote|speech|said|proverb|statement|narrator|words|motto|philosophy|wisdom)(?:[^\w]|$)/.test(text)
290
+ || /വാചകം|ഉദ്ധരണി|വാക്കുകൾ|कथन/.test(text)) {
291
+ return 'quote';
292
+ }
293
+
294
+ // High Velocity Speed Action
295
+ if (/(?:^|[^\w])(sports|workout|gym|athlete|speed|streaks|race|championship|extreme|versus|vs|hype|adrenaline|power|velocity|fast)(?:[^\w]|$)/.test(text)
296
+ || /കായിക|മത്സര|വേഗത|വ്യായാമം|खेल|दौड़/.test(text)) {
297
+ return 'speed_action';
298
+ }
299
+
300
+ // Metric / Numerical KPI
301
+ if (/(?:^|[^\w])(metric|counter|number|percentage|score|stat|gain|roi|accuracy|target|kpi|growth)(?:[^\w]|$)/.test(text)
302
+ || /ശതമാനം|അളവ്|പ്രകടനം|प्रतिशत/.test(text)) {
303
+ return 'metric';
304
+ }
305
+
306
+ // Timeline / Milestones / Chronology
307
+ if (/(?:^|[^\w])(timeline|milestone|chronology|history|evolution|roadmap|phase|journey|centuries|years|era|ancient|century|dynasty|temple|monument)(?:[^\w]|$)/.test(text)
308
+ || /ചരിത്ര|പുരാതന|സാമ്രാജ്യ|രാജവംശ|പാരമ്പര്യ|ഇതിഹാസ|इतिहास|प्राचीन|धरोहर/.test(text)) {
309
+ return 'timeline';
310
+ }
311
+
312
+ // Code / Programming IDE
313
+ if (/(?:^|[^\w])(code|function|syntax|api|javascript|python|typescript|json|sql|query|endpoint|algorithm|struct|cli|terminal|bash|shell|npm|git|compiler|linux|rust)(?:[^\w]|$)/.test(text)
314
+ || /കോഡിംഗ്|സോഫ്റ്റ്‌വെയർ|പ്രോഗ്രാമിംഗ്|कोड|सॉफ्टवेयर/.test(text)) {
315
+ return 'code';
316
+ }
317
+
318
+ // Chart / Finance / Economics
319
+ if (/(?:^|[^\w])(chart|data|analytics|growth|graph|statistic|bars|volume|distribution|stock|crypto|bitcoin|market|invest|trading|inflation|economy|gdp|finance|gain|loss|bull|bear|candlestick|price)(?:[^\w]|$)/.test(text)
320
+ || /നിക്ഷേപ|ഓഹരി|വിപണി|പണം|ചാർട്ട്|शेयर|बाज़ार/.test(text)) {
321
+ return 'chart';
322
+ }
323
+
324
+ // Neural Network Nodes
325
+ if (/(?:^|[^\w])(network|node|nodes|ai|model|neural|pipeline|architecture|server|system|mesh|connect|synapse|deep learning)(?:[^\w]|$)/.test(text)
326
+ || /നെറ്റ്‌വർക്ക്|സിനാപ്സ്|ശൃംഖല|नेटवर्क/.test(text)) {
327
+ return 'nodes';
328
+ }
329
+
330
+ // 3D Isometric Geometry / Polyhedron
331
+ if (/(?:^|[^\w])(cube|geometry|3d|wireframe|isometric|matrix|crystal|structure|polyhedron|dimension|architect|blueprint|math)(?:[^\w]|$)/.test(text)
332
+ || /ജ്യാമിതി|രൂപം|ത്രിമാന|संरचना/.test(text)) {
333
+ return 'isometric_geometry';
334
+ }
335
+
336
+ // Fallbacks matched to aesthetic
337
+ if (aesthetic === 'cosmic_space') return 'celestial_orbit';
338
+ if (aesthetic === 'cinematic_documentary') return 'timeline';
339
+ if (aesthetic === 'finance_executive') return 'chart';
340
+ if (aesthetic === 'nature_organic') return 'dna_helix';
341
+ if (aesthetic === 'breaking_news') return 'radar_globe';
342
+ if (aesthetic === 'cyber_tech') return 'nodes';
343
+ if (aesthetic === 'minimal_luxury') return 'isometric_geometry';
344
+ if (aesthetic === 'hype_sports') return 'speed_action';
345
+ return 'waveform';
346
+ }
347
+
348
+ // ── Harness HTML Wrapper for User / Agent Code ────────────────────────────────
349
+ /**
350
+ * Wrap agent-provided Canvas drawing code into a pure-function-of-t harness page.
351
+ */
352
+ function wrapCodeInHarnessHtml({
353
+ code = '',
354
+ duration = 3,
355
+ width = 1920,
356
+ height = 1080,
357
+ language = '',
358
+ style = '',
359
+ topic = '',
360
+ } = {}) {
361
+ const scriptFont = getScriptFontStack('', language);
362
+ const aestheticKey = detectAesthetic({ topic, style, language });
363
+ const aesthetic = AESTHETICS[aestheticKey] || AESTHETICS.cyber_tech;
364
+ const fontDisplay = aesthetic.fontFamily(scriptFont);
365
+ const fontBody = scriptFont ? `${scriptFont}, -apple-system, sans-serif` : `'Segoe UI', -apple-system, sans-serif`;
366
+
367
+ return `<!doctype html>
368
+ <html lang="en">
369
+ <head>
370
+ <meta charset="utf-8">
371
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
372
+ <style>
373
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
374
+ html, body {
375
+ width: 100%; height: 100%; overflow: hidden;
376
+ background: #06090e;
377
+ font-family: ${fontBody};
378
+ -webkit-font-smoothing: antialiased;
379
+ }
380
+ canvas { width: 100%; height: 100%; display: block; }
381
+ </style>
382
+ </head>
383
+ <body>
384
+ <canvas id="stage" width="${width}" height="${height}"></canvas>
385
+ <script>
386
+ window.DURATION = ${Math.max(1, Number(duration) || 3)};
387
+ const canvas = document.getElementById('stage');
388
+ const ctx = canvas.getContext('2d');
389
+ const W = ${width};
390
+ const H = ${height};
391
+ const FONT_DISPLAY = ${JSON.stringify(fontDisplay)};
392
+ const FONT_BODY = ${JSON.stringify(fontBody)};
393
+
394
+ // Seeded PRNG for deterministic pure-t rendering
395
+ function sfc32(a, b, c, d) {
396
+ return function() {
397
+ a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
398
+ let t = (a + b | 0) + d | 0;
399
+ d = d + 1 | 0;
400
+ a = b ^ b >>> 9;
401
+ b = c + (c << 3) | 0;
402
+ c = c << 21 | c >>> 11;
403
+ c = c + t | 0;
404
+ return (t >>> 0) / 4294967296;
405
+ };
406
+ }
407
+ const hash = (i) => sfc32(i * 19 + 42, i * 29 + 1337, i * 13 + 7, 99)();
408
+
409
+ // Spring dynamics
410
+ function spring(t, delay = 0, tension = 180, friction = 14) {
411
+ const dt = t - delay;
412
+ if (dt <= 0) return 0;
413
+ const omega = Math.sqrt(tension);
414
+ const zeta = friction / (2 * omega);
415
+ if (zeta < 1) {
416
+ const omegaD = omega * Math.sqrt(1 - zeta * zeta);
417
+ return 1 - Math.exp(-zeta * omega * dt) * (Math.cos(omegaD * dt) + (zeta * omega / omegaD) * Math.sin(omegaD * dt));
418
+ }
419
+ return 1 - Math.exp(-omega * dt);
420
+ }
421
+
422
+ window.seek = async (t) => {
423
+ ${code}
424
+ };
425
+ </script>
426
+ </body>
427
+ </html>`;
428
+ }
429
+
430
+ // ── HTML Scene Code Generator ──────────────────────────────────────────────────
431
+ /**
432
+ * Generate a complete, self-contained HTML scene conforming to window.seek(t).
433
+ * Paints full-canvas cinematic procedural animations — zero PPT card slideshow patterns.
434
+ */
435
+ function generateSceneHtml({
436
+ caption = '',
437
+ narration = '',
438
+ visualDescription = '',
439
+ imagePath = null,
440
+ topic = '',
441
+ style = '',
442
+ theme = '',
443
+ title = '',
444
+ language = '',
445
+ musicMood = 'neutral',
446
+ duration = 3,
447
+ width = 1920,
448
+ height = 1080,
449
+ sceneIndex = 0,
450
+ } = {}) {
451
+ const aestheticKey = detectAesthetic({
452
+ topic,
453
+ style,
454
+ theme,
455
+ title,
456
+ narration,
457
+ caption,
458
+ visualDescription,
459
+ language,
460
+ musicMood,
461
+ });
462
+ const aesthetic = AESTHETICS[aestheticKey] || AESTHETICS.cyber_tech;
463
+ const compTheme = detectComponentTheme({
464
+ visualDescription,
465
+ caption,
466
+ narration,
467
+ topic,
468
+ imagePath,
469
+ aesthetic: aestheticKey,
470
+ });
471
+ const clampedDur = Math.max(1, Number(duration) || 3);
472
+
473
+ // Select palette variation based on sceneIndex
474
+ const pal = aesthetic.palettes[sceneIndex % aesthetic.palettes.length];
475
+
476
+ // Resolve script-specific fonts
477
+ const scriptFont = getScriptFontStack(`${caption} ${narration}`, language);
478
+ const fontDisplay = aesthetic.fontFamily(scriptFont);
479
+ const fontBody = scriptFont ? `${scriptFont}, -apple-system, sans-serif` : `'Segoe UI', -apple-system, sans-serif`;
480
+
481
+ // Convert imagePath to data URI if available
482
+ let imageDataUri = '';
483
+ if (imagePath && fs.existsSync(imagePath)) {
484
+ try {
485
+ const ext = path.extname(imagePath).toLowerCase().slice(1) || 'jpeg';
486
+ const mime = ext === 'png' ? 'image/png' : ext === 'webp' ? 'image/webp' : 'image/jpeg';
487
+ const b64 = fs.readFileSync(imagePath).toString('base64');
488
+ imageDataUri = `data:${mime};base64,${b64}`;
489
+ } catch (_) {}
490
+ }
491
+
492
+ return `<!doctype html>
493
+ <html lang="en">
494
+ <head>
495
+ <meta charset="utf-8">
496
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
497
+ <style>
498
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
499
+ html, body {
500
+ width: 100%;
501
+ height: 100%;
502
+ overflow: hidden;
503
+ background: ${pal.bg2};
504
+ font-family: ${fontBody};
505
+ -webkit-font-smoothing: antialiased;
506
+ }
507
+ canvas {
508
+ width: 100%;
509
+ height: 100%;
510
+ display: block;
511
+ }
512
+ </style>
513
+ </head>
514
+ <body>
515
+ <canvas id="stage" width="${width}" height="${height}"></canvas>
516
+ <script>
517
+ window.DURATION = ${clampedDur};
518
+ const canvas = document.getElementById('stage');
519
+ const ctx = canvas.getContext('2d');
520
+ const W = ${width};
521
+ const H = ${height};
522
+
523
+ // Seeded PRNG for frame purity
524
+ function sfc32(a, b, c, d) {
525
+ return function() {
526
+ a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
527
+ let t = (a + b | 0) + d | 0;
528
+ d = d + 1 | 0;
529
+ a = b ^ b >>> 9;
530
+ b = c + (c << 3) | 0;
531
+ c = c << 21 | c >>> 11;
532
+ c = c + t | 0;
533
+ return (t >>> 0) / 4294967296;
534
+ };
535
+ }
536
+
537
+ const rng = sfc32(${42 + sceneIndex * 19}, ${1337 + sceneIndex * 29}, ${7 + sceneIndex * 13}, 99);
538
+ const PARTICLES = Array.from({ length: 75 }, () => ({
539
+ x: rng() * W,
540
+ y: rng() * H,
541
+ vx: (rng() - 0.5) * 40,
542
+ vy: (rng() - 0.5) * 30 - 10,
543
+ r: 1.5 + rng() * 3.5,
544
+ alpha: 0.2 + rng() * 0.6
545
+ }));
546
+
547
+ // Spring dynamics function
548
+ function spring(t, delay = 0, tension = ${aesthetic.springTension}, friction = ${aesthetic.springFriction}) {
549
+ const dt = t - delay;
550
+ if (dt <= 0) return 0;
551
+ const omega = Math.sqrt(tension);
552
+ const zeta = friction / (2 * omega);
553
+ if (zeta < 1) {
554
+ const omegaD = omega * Math.sqrt(1 - zeta * zeta);
555
+ return 1 - Math.exp(-zeta * omega * dt) * (Math.cos(omegaD * dt) + (zeta * omega / omegaD) * Math.sin(omegaD * dt));
556
+ }
557
+ return 1 - Math.exp(-omega * dt);
558
+ }
559
+
560
+ const AESTHETIC = ${JSON.stringify(aestheticKey)};
561
+ const BG_TYPE = ${JSON.stringify(aesthetic.backgroundType)};
562
+ const COMP_THEME = ${JSON.stringify(compTheme)};
563
+ const CAPTION = ${JSON.stringify(caption)};
564
+ const NARRATION = ${JSON.stringify(narration)};
565
+ const TAG = ${JSON.stringify(pal.tag)};
566
+ const ACCENT = ${JSON.stringify(pal.accent)};
567
+ const ACCENT2 = ${JSON.stringify(pal.accent2)};
568
+ const BG0 = ${JSON.stringify(pal.bg0)};
569
+ const BG1 = ${JSON.stringify(pal.bg1)};
570
+ const BG2 = ${JSON.stringify(pal.bg2)};
571
+ const CARD_BG = ${JSON.stringify(pal.cardBg)};
572
+ const CARD_BORDER = ${JSON.stringify(pal.cardBorder)};
573
+ const FONT_DISPLAY = ${JSON.stringify(fontDisplay)};
574
+ const FONT_BODY = ${JSON.stringify(fontBody)};
575
+ const FONT_WEIGHT = ${JSON.stringify(aesthetic.fontDisplayWeight)};
576
+ const IMAGE_SRC = ${JSON.stringify(imageDataUri)};
577
+
578
+ let cachedImage = null;
579
+ if (IMAGE_SRC) {
580
+ cachedImage = new Image();
581
+ cachedImage.src = IMAGE_SRC;
582
+ }
583
+
584
+ // ── Background Ambient Layers ──────────────────────────────────────────────────
585
+ function drawBackground(t) {
586
+ const cx = W / 2 + Math.sin(t * 0.5) * 120;
587
+ const cy = H / 2 + Math.cos(t * 0.4) * 70;
588
+ const bgGrad = ctx.createRadialGradient(cx, cy, 60, W / 2, H / 2, W * 0.95);
589
+ bgGrad.addColorStop(0, BG0);
590
+ bgGrad.addColorStop(0.55, BG1);
591
+ bgGrad.addColorStop(1, BG2);
592
+ ctx.fillStyle = bgGrad;
593
+ ctx.fillRect(0, 0, W, H);
594
+
595
+ ctx.save();
596
+ if (BG_TYPE === 'grid_perspective') {
597
+ // 3D Perspective Grid
598
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
599
+ ctx.lineWidth = 1;
600
+ const horizon = H * 0.52;
601
+ const shift = (t * 40) % 50;
602
+ for (let x = -W * 0.5; x <= W * 1.5; x += 90) {
603
+ ctx.beginPath(); ctx.moveTo(W / 2, horizon); ctx.lineTo(x, H); ctx.stroke();
604
+ }
605
+ for (let y = horizon + shift; y <= H; y += 40) {
606
+ const p = (y - horizon) / (H - horizon);
607
+ ctx.strokeStyle = 'rgba(255, 255, 255, ' + (0.02 + p * 0.08) + ')';
608
+ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
609
+ }
610
+ } else if (BG_TYPE === 'candlestick_charts') {
611
+ // Ambient market backdrop grid
612
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';
613
+ ctx.lineWidth = 1;
614
+ for (let y = 80; y < H; y += 80) {
615
+ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
616
+ }
617
+ for (let x = 120; x < W; x += 120) {
618
+ ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
619
+ }
620
+ } else if (BG_TYPE === 'retro_horizon') {
621
+ // Retrowave Sun & Floor Grid
622
+ const sunY = H * 0.44;
623
+ const sunR = 150;
624
+ const sunGrad = ctx.createLinearGradient(0, sunY - sunR, 0, sunY + sunR);
625
+ sunGrad.addColorStop(0, '#ffd000');
626
+ sunGrad.addColorStop(0.5, '#ff7300');
627
+ sunGrad.addColorStop(1, '#ff2a85');
628
+ ctx.fillStyle = sunGrad;
629
+ ctx.beginPath(); ctx.arc(W / 2, sunY, sunR, Math.PI, 0); ctx.fill();
630
+
631
+ ctx.strokeStyle = ACCENT;
632
+ const fShift = (t * 50) % 40;
633
+ for (let y = sunY + fShift; y <= H; y += 35) {
634
+ const a = (y - sunY) / (H - sunY);
635
+ ctx.strokeStyle = 'rgba(255, 42, 133, ' + (a * 0.3) + ')';
636
+ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
637
+ }
638
+ } else if (BG_TYPE === 'organic_ripples') {
639
+ ctx.lineWidth = 2;
640
+ for (let w = 0; w < 4; w++) {
641
+ ctx.strokeStyle = 'rgba(34, 197, 94, ' + (0.05 + w * 0.03) + ')';
642
+ ctx.beginPath();
643
+ for (let x = 0; x <= W; x += 30) {
644
+ const y = H * (0.35 + w * 0.15) + Math.sin(x * 0.005 + t * 1.5 + w) * 35;
645
+ if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
646
+ }
647
+ ctx.stroke();
648
+ }
649
+ } else if (BG_TYPE === 'speed_streaks') {
650
+ ctx.strokeStyle = 'rgba(239, 68, 68, 0.12)';
651
+ ctx.lineWidth = 2;
652
+ for (let i = 0; i < 20; i++) {
653
+ const sx = ((i * 110 + t * 600) % (W * 1.4)) - W * 0.2;
654
+ ctx.beginPath(); ctx.moveTo(sx, 0); ctx.lineTo(sx - 200, H); ctx.stroke();
655
+ }
656
+ } else if (BG_TYPE === 'particles_dust' || BG_TYPE === 'cosmic_stars') {
657
+ // Parallax particle atmosphere
658
+ for (let i = 0; i < PARTICLES.length; i++) {
659
+ const p = PARTICLES[i];
660
+ const px = (p.x + p.vx * t + W * 2) % W;
661
+ const py = (p.y + p.vy * t + H * 2) % H;
662
+ ctx.fillStyle = ACCENT;
663
+ ctx.globalAlpha = p.alpha * (0.4 + 0.6 * Math.sin(t * 2 + i));
664
+ ctx.beginPath(); ctx.arc(px, py, p.r, 0, Math.PI * 2); ctx.fill();
665
+ }
666
+ }
667
+ ctx.restore();
668
+ }
669
+
670
+ // ── Full-Canvas Cinematic Visual Directors ─────────────────────────────────────
671
+ function drawMainScene(t) {
672
+ ctx.save();
673
+ const enterSpring = spring(t, 0.05);
674
+ const sceneScale = Math.min(1.02, Math.max(0.9, enterSpring));
675
+ ctx.translate(W / 2, H * 0.40);
676
+ ctx.scale(sceneScale, sceneScale);
677
+ ctx.translate(-W / 2, -H * 0.40);
678
+
679
+ const cx = W / 2;
680
+ const cy = H * 0.40;
681
+
682
+ if (COMP_THEME === 'celestial_orbit') {
683
+ // 3D Celestial Body (Earth) with orbiting satellites & signal pulses
684
+ const earthR = Math.min(W, H) * 0.15;
685
+
686
+ // Atmospheric halo
687
+ const haloGrad = ctx.createRadialGradient(cx, cy, earthR * 0.9, cx, cy, earthR * 1.45);
688
+ haloGrad.addColorStop(0, 'rgba(56, 189, 248, 0.4)');
689
+ haloGrad.addColorStop(1, 'rgba(56, 189, 248, 0)');
690
+ ctx.fillStyle = haloGrad;
691
+ ctx.beginPath(); ctx.arc(cx, cy, earthR * 1.45, 0, Math.PI * 2); ctx.fill();
692
+
693
+ // Planet body
694
+ const planetGrad = ctx.createRadialGradient(cx - earthR * 0.35, cy - earthR * 0.35, earthR * 0.1, cx, cy, earthR);
695
+ planetGrad.addColorStop(0, '#38bdf8');
696
+ planetGrad.addColorStop(0.7, '#0284c7');
697
+ planetGrad.addColorStop(1, '#082f49');
698
+ ctx.fillStyle = planetGrad;
699
+ ctx.beginPath(); ctx.arc(cx, cy, earthR, 0, Math.PI * 2); ctx.fill();
700
+
701
+ // Rotating longitude meridians
702
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.22)';
703
+ ctx.lineWidth = 1.5;
704
+ for (let m = 0; m < 4; m++) {
705
+ const mAngle = t * 0.5 + (m * Math.PI) / 4;
706
+ const mWidth = earthR * Math.cos(mAngle);
707
+ ctx.beginPath();
708
+ ctx.ellipse(cx, cy, Math.abs(mWidth), earthR, 0, 0, Math.PI * 2);
709
+ ctx.stroke();
710
+ }
711
+ // Latitude parallel rings
712
+ for (let lat = -1; lat <= 1; lat += 2) {
713
+ const latY = cy + lat * earthR * 0.5;
714
+ const latW = earthR * Math.cos(Math.asin(0.5));
715
+ ctx.beginPath();
716
+ ctx.ellipse(cx, latY, latW, earthR * 0.15, 0, 0, Math.PI * 2);
717
+ ctx.stroke();
718
+ }
719
+
720
+ // 3 3D-inclined Elliptical Orbits with Satellites
721
+ const orbitAngles = [-0.55, 0.45, 1.2];
722
+ for (let k = 0; k < 3; k++) {
723
+ const a = earthR * (1.85 + k * 0.35);
724
+ const b = a * 0.45;
725
+ const tilt = orbitAngles[k];
726
+ const satSpeed = 0.85 + k * 0.25;
727
+ const satAngle = t * satSpeed + k * 2.1;
728
+
729
+ // Draw orbit path
730
+ ctx.save();
731
+ ctx.translate(cx, cy);
732
+ ctx.rotate(tilt);
733
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.14)';
734
+ ctx.lineWidth = 1.5;
735
+ ctx.beginPath(); ctx.ellipse(0, 0, a, b, 0, 0, Math.PI * 2); ctx.stroke();
736
+
737
+ // Satellite coordinates in tilted frame
738
+ const sxLoc = a * Math.cos(satAngle);
739
+ const syLoc = b * Math.sin(satAngle);
740
+ ctx.restore();
741
+
742
+ // Convert to canvas coordinates
743
+ const sx = cx + sxLoc * Math.cos(tilt) - syLoc * Math.sin(tilt);
744
+ const sy = cy + sxLoc * Math.sin(tilt) + syLoc * Math.cos(tilt);
745
+
746
+ // Spherical microwave signal pulses from satellite
747
+ for (let wave = 0; wave < 3; wave++) {
748
+ const waveProgress = ((t * 1.6 + wave * 0.33 + k * 0.2) % 1);
749
+ const waveR = waveProgress * earthR * 1.6;
750
+ const waveAlpha = (1 - waveProgress) * 0.5;
751
+ ctx.strokeStyle = k === 0 ? ACCENT : ACCENT2;
752
+ ctx.globalAlpha = waveAlpha;
753
+ ctx.lineWidth = 2;
754
+ ctx.beginPath(); ctx.arc(sx, sy, waveR, 0, Math.PI * 2); ctx.stroke();
755
+ }
756
+ ctx.globalAlpha = 1;
757
+
758
+ // Draw satellite body
759
+ ctx.fillStyle = '#ffffff';
760
+ ctx.beginPath(); ctx.arc(sx, sy, 5, 0, Math.PI * 2); ctx.fill();
761
+ // Solar panel wings
762
+ ctx.fillStyle = ACCENT;
763
+ ctx.fillRect(sx - 12, sy - 3, 24, 6);
764
+ }
765
+
766
+ // Target beacon on Earth surface
767
+ const beaconX = cx + earthR * 0.28;
768
+ const beaconY = cy + earthR * 0.32;
769
+ const beaconPulse = 1 + 0.35 * Math.sin(t * 5);
770
+ ctx.strokeStyle = ACCENT;
771
+ ctx.lineWidth = 2;
772
+ ctx.beginPath(); ctx.arc(beaconX, beaconY, 14 * beaconPulse, 0, Math.PI * 2); ctx.stroke();
773
+ ctx.fillStyle = '#ffffff';
774
+ ctx.beginPath(); ctx.arc(beaconX, beaconY, 4, 0, Math.PI * 2); ctx.fill();
775
+
776
+ } else if (COMP_THEME === 'dna_helix') {
777
+ // 3D Rotating DNA Double Helix
778
+ const rungs = 24;
779
+ const spanW = W * 0.72;
780
+ const startX = cx - spanW / 2;
781
+ const stepX = spanW / (rungs - 1);
782
+ const amp = 105;
783
+
784
+ for (let i = 0; i < rungs; i++) {
785
+ const rx = startX + i * stepX;
786
+ const phase = rx * 0.007 + t * 2.2;
787
+ const y1 = cy + Math.sin(phase) * amp;
788
+ const y2 = cy - Math.sin(phase) * amp;
789
+ const depth = Math.cos(phase); // z-depth
790
+
791
+ // Connecting nucleotide rung
792
+ ctx.strokeStyle = depth > 0 ? ACCENT : 'rgba(255, 255, 255, 0.2)';
793
+ ctx.lineWidth = depth > 0 ? 3 : 1.5;
794
+ ctx.beginPath(); ctx.moveTo(rx, y1); ctx.lineTo(rx, y2); ctx.stroke();
795
+
796
+ // Nodes on strands with depth perspective
797
+ const r1 = 5 + depth * 3;
798
+ const r2 = 5 - depth * 3;
799
+ ctx.fillStyle = ACCENT;
800
+ ctx.beginPath(); ctx.arc(rx, y1, Math.max(2, r1), 0, Math.PI * 2); ctx.fill();
801
+ ctx.fillStyle = ACCENT2;
802
+ ctx.beginPath(); ctx.arc(rx, y2, Math.max(2, r2), 0, Math.PI * 2); ctx.fill();
803
+ }
804
+
805
+ } else if (COMP_THEME === 'chart') {
806
+ // Full-Canvas High-Resolution Japanese Candlestick Chart
807
+ const chartW = W * 0.75;
808
+ const chartH = H * 0.44;
809
+ const chartX = cx - chartW / 2;
810
+ const chartY = cy - chartH / 2;
811
+ const baseLine = chartY + chartH;
812
+
813
+ // Grid levels
814
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
815
+ ctx.lineWidth = 1;
816
+ for (let l = 0; l <= 4; l++) {
817
+ const gy = chartY + (chartH / 4) * l;
818
+ ctx.beginPath(); ctx.moveTo(chartX, gy); ctx.lineTo(chartX + chartW, gy); ctx.stroke();
819
+ }
820
+
821
+ // 20 Japanese Candlesticks with wicks
822
+ const candles = 20;
823
+ const cStep = chartW / candles;
824
+ const revealProg = Math.min(1, t / (window.DURATION * 0.8));
825
+ const activeCandles = Math.floor(candles * revealProg);
826
+
827
+ const pricePoints = [];
828
+ for (let c = 0; c < activeCandles; c++) {
829
+ const cSeed = c * 37 + 13;
830
+ const isBull = Math.sin(cSeed) > -0.2;
831
+ const bodyH = 25 + Math.abs(Math.sin(cSeed * 1.5)) * 65;
832
+ const wickTopH = 15 + Math.abs(Math.cos(cSeed)) * 30;
833
+ const wickBotH = 12 + Math.abs(Math.sin(cSeed * 2)) * 25;
834
+ const candleMidY = chartY + chartH * 0.5 + Math.sin(c * 0.35 + t * 0.8) * (chartH * 0.25);
835
+ const candleX = chartX + c * cStep + cStep / 2;
836
+
837
+ ctx.strokeStyle = isBull ? '#10b981' : '#ef4444';
838
+ ctx.fillStyle = isBull ? '#10b981' : '#ef4444';
839
+ ctx.lineWidth = 2;
840
+
841
+ // Wicks
842
+ ctx.beginPath();
843
+ ctx.moveTo(candleX, candleMidY - bodyH / 2 - wickTopH);
844
+ ctx.lineTo(candleX, candleMidY + bodyH / 2 + wickBotH);
845
+ ctx.stroke();
846
+
847
+ // Body
848
+ ctx.fillRect(candleX - cStep * 0.35, candleMidY - bodyH / 2, cStep * 0.7, bodyH);
849
+ pricePoints.push({ x: candleX, y: candleMidY });
850
+ }
851
+
852
+ // Glowing EMA Moving Average Ribbon
853
+ if (pricePoints.length > 1) {
854
+ ctx.strokeStyle = ACCENT;
855
+ ctx.lineWidth = 3;
856
+ ctx.beginPath();
857
+ ctx.moveTo(pricePoints[0].x, pricePoints[0].y);
858
+ for (let p = 1; p < pricePoints.length; p++) {
859
+ const xc = (pricePoints[p - 1].x + pricePoints[p].x) / 2;
860
+ const yc = (pricePoints[p - 1].y + pricePoints[p].y) / 2;
861
+ ctx.quadraticCurveTo(pricePoints[p - 1].x, pricePoints[p - 1].y, xc, yc);
862
+ }
863
+ ctx.stroke();
864
+
865
+ // Ticker badge on last point
866
+ const lastPt = pricePoints[pricePoints.length - 1];
867
+ ctx.fillStyle = ACCENT;
868
+ ctx.fillRect(lastPt.x + 8, lastPt.y - 12, 70, 24);
869
+ ctx.font = 'bold 13px monospace';
870
+ ctx.fillStyle = '#000000';
871
+ ctx.textAlign = 'center';
872
+ ctx.fillText('+34.8%', lastPt.x + 43, lastPt.y + 4);
873
+ }
874
+
875
+ } else if (COMP_THEME === 'timeline') {
876
+ // 3D Milestone Roadmap with Royal Golden Seals
877
+ const stations = 5;
878
+ const spanW = W * 0.76;
879
+ const startX = cx - spanW / 2;
880
+ const stepX = spanW / (stations - 1);
881
+ const lineY = cy + 15;
882
+
883
+ // Glowing road baseline
884
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.18)';
885
+ ctx.lineWidth = 3;
886
+ ctx.beginPath(); ctx.moveTo(startX, lineY); ctx.lineTo(startX + spanW, lineY); ctx.stroke();
887
+
888
+ const activeDist = Math.min(1, t / (window.DURATION * 0.85));
889
+ ctx.strokeStyle = ACCENT;
890
+ ctx.lineWidth = 4;
891
+ ctx.beginPath(); ctx.moveTo(startX, lineY); ctx.lineTo(startX + spanW * activeDist, lineY); ctx.stroke();
892
+
893
+ for (let s = 0; s < stations; s++) {
894
+ const sx = startX + s * stepX;
895
+ const sActive = (s / (stations - 1)) <= activeDist;
896
+ const sScale = spring(t, 0.1 + s * 0.14);
897
+
898
+ // Milestone seal node
899
+ ctx.fillStyle = sActive ? ACCENT : 'rgba(255, 255, 255, 0.15)';
900
+ ctx.beginPath(); ctx.arc(sx, lineY, 18 * Math.min(1, sScale), 0, Math.PI * 2); ctx.fill();
901
+
902
+ ctx.strokeStyle = '#ffffff';
903
+ ctx.lineWidth = 2;
904
+ ctx.beginPath(); ctx.arc(sx, lineY, 8 * Math.min(1, sScale), 0, Math.PI * 2); ctx.stroke();
905
+
906
+ // Milestone Year / Chapter Label
907
+ ctx.font = 'bold 15px ' + FONT_DISPLAY;
908
+ ctx.fillStyle = sActive ? '#ffffff' : 'rgba(255, 255, 255, 0.4)';
909
+ ctx.textAlign = 'center';
910
+ ctx.fillText('ERA 0' + (s + 1), sx, lineY - 35);
911
+ }
912
+
913
+ } else if (COMP_THEME === 'nodes') {
914
+ // Full-Canvas Neural Network / Distributed Topology
915
+ const tiers = [
916
+ [[-W * 0.32, -80], [-W * 0.32, 0], [-W * 0.32, 80]],
917
+ [[-W * 0.12, -110], [-W * 0.12, -40], [-W * 0.12, 40], [-W * 0.12, 110]],
918
+ [[W * 0.12, -110], [W * 0.12, -40], [W * 0.12, 40], [W * 0.12, 110]],
919
+ [[W * 0.32, -60], [W * 0.32, 60]]
920
+ ];
921
+
922
+ ctx.lineWidth = 1.5;
923
+ // Synapse connections
924
+ for (let tier = 0; tier < tiers.length - 1; tier++) {
925
+ for (const [x1, y1] of tiers[tier]) {
926
+ for (const [x2, y2] of tiers[tier + 1]) {
927
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
928
+ ctx.beginPath(); ctx.moveTo(cx + x1, cy + y1); ctx.lineTo(cx + x2, cy + y2); ctx.stroke();
929
+ }
930
+ }
931
+ }
932
+
933
+ // Traveling glowing energy pulses
934
+ for (let p = 0; p < 8; p++) {
935
+ const pProg = (t * 0.7 + p * 0.14) % 1;
936
+ const tierIdx = Math.floor(pProg * 3);
937
+ const subProg = (pProg * 3) % 1;
938
+ const startTier = tiers[tierIdx];
939
+ const endTier = tiers[tierIdx + 1];
940
+ const nodeA = startTier[p % startTier.length];
941
+ const nodeB = endTier[(p + 1) % endTier.length];
942
+ const ex = cx + nodeA[0] + (nodeB[0] - nodeA[0]) * subProg;
943
+ const ey = cy + nodeA[1] + (nodeB[1] - nodeA[1]) * subProg;
944
+
945
+ ctx.fillStyle = ACCENT;
946
+ ctx.beginPath(); ctx.arc(ex, ey, 5, 0, Math.PI * 2); ctx.fill();
947
+ }
948
+
949
+ // Node glyphs
950
+ for (const tier of tiers) {
951
+ for (const [nx, ny] of tier) {
952
+ ctx.fillStyle = ACCENT2;
953
+ ctx.beginPath(); ctx.arc(cx + nx, cy + ny, 9, 0, Math.PI * 2); ctx.fill();
954
+ ctx.fillStyle = '#ffffff';
955
+ ctx.beginPath(); ctx.arc(cx + nx, cy + ny, 4, 0, Math.PI * 2); ctx.fill();
956
+ }
957
+ }
958
+
959
+ } else if (COMP_THEME === 'radar_globe') {
960
+ // 3D Tactical Wireframe Rotating Radar Globe
961
+ const globeR = 150;
962
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
963
+ ctx.lineWidth = 1.5;
964
+
965
+ // Outer degree ring
966
+ ctx.beginPath(); ctx.arc(cx, cy, globeR, 0, Math.PI * 2); ctx.stroke();
967
+ for (let deg = 0; deg < 360; deg += 30) {
968
+ const rad = (deg * Math.PI) / 180;
969
+ ctx.beginPath();
970
+ ctx.moveTo(cx + Math.cos(rad) * (globeR - 6), cy + Math.sin(rad) * (globeR - 6));
971
+ ctx.lineTo(cx + Math.cos(rad) * (globeR + 6), cy + Math.sin(rad) * (globeR + 6));
972
+ ctx.stroke();
973
+ }
974
+
975
+ // Rotating longitude circles
976
+ for (let m = 0; m < 4; m++) {
977
+ const mAngle = t * 0.6 + (m * Math.PI) / 4;
978
+ const mW = globeR * Math.cos(mAngle);
979
+ ctx.beginPath(); ctx.ellipse(cx, cy, Math.abs(mW), globeR, 0, 0, Math.PI * 2); ctx.stroke();
980
+ }
981
+
982
+ // Sweeping radar beam
983
+ const sweepAngle = t * 2.2;
984
+ const sweepGrad = ctx.createRadialGradient(cx, cy, 10, cx, cy, globeR);
985
+ sweepGrad.addColorStop(0, 'rgba(56, 189, 248, 0.45)');
986
+ sweepGrad.addColorStop(1, 'rgba(56, 189, 248, 0)');
987
+ ctx.fillStyle = sweepGrad;
988
+ ctx.beginPath();
989
+ ctx.moveTo(cx, cy);
990
+ ctx.arc(cx, cy, globeR, sweepAngle - 0.6, sweepAngle);
991
+ ctx.fill();
992
+
993
+ } else if (COMP_THEME === 'isometric_geometry') {
994
+ // True 3D Wireframe Polyhedron (Rotating Octahedron)
995
+ const rotX = t * 0.7;
996
+ const rotY = t * 1.1;
997
+ const verts3D = [
998
+ [0, -110, 0], [110, 0, 0], [0, 0, 110], [-110, 0, 0], [0, 0, -110], [0, 110, 0]
999
+ ];
1000
+ const edges = [
1001
+ [0,1], [0,2], [0,3], [0,4],
1002
+ [5,1], [5,2], [5,3], [5,4],
1003
+ [1,2], [2,3], [3,4], [4,1]
1004
+ ];
1005
+
1006
+ // Project vertices
1007
+ const projected = verts3D.map(([x, y, z]) => {
1008
+ // Rotate Y
1009
+ const x1 = x * Math.cos(rotY) + z * Math.sin(rotY);
1010
+ const z1 = -x * Math.sin(rotY) + z * Math.cos(rotY);
1011
+ // Rotate X
1012
+ const y2 = y * Math.cos(rotX) - z1 * Math.sin(rotX);
1013
+ const z2 = y * Math.sin(rotX) + z1 * Math.cos(rotX);
1014
+ const fov = 350;
1015
+ const scale = fov / (fov + z2);
1016
+ return [cx + x1 * scale, cy + y2 * scale];
1017
+ });
1018
+
1019
+ ctx.strokeStyle = ACCENT;
1020
+ ctx.lineWidth = 2.5;
1021
+ for (const [v1, v2] of edges) {
1022
+ ctx.beginPath();
1023
+ ctx.moveTo(projected[v1][0], projected[v1][1]);
1024
+ ctx.lineTo(projected[v2][0], projected[v2][1]);
1025
+ ctx.stroke();
1026
+ }
1027
+ for (const [px, py] of projected) {
1028
+ ctx.fillStyle = '#ffffff';
1029
+ ctx.beginPath(); ctx.arc(px, py, 4, 0, Math.PI * 2); ctx.fill();
1030
+ }
1031
+
1032
+ } else if (COMP_THEME === 'code') {
1033
+ // Sleek Developer Terminal Window
1034
+ const winW = W * 0.65;
1035
+ const winH = H * 0.42;
1036
+ const winX = cx - winW / 2;
1037
+ const winY = cy - winH / 2;
1038
+
1039
+ ctx.fillStyle = 'rgba(10, 15, 26, 0.9)';
1040
+ ctx.strokeStyle = CARD_BORDER;
1041
+ ctx.lineWidth = 2;
1042
+ ctx.beginPath(); ctx.roundRect(winX, winY, winW, winH, 14); ctx.fill(); ctx.stroke();
1043
+
1044
+ // Window controls
1045
+ ctx.fillStyle = '#ff5f56'; ctx.beginPath(); ctx.arc(winX + 24, winY + 22, 6, 0, Math.PI * 2); ctx.fill();
1046
+ ctx.fillStyle = '#ffbd2e'; ctx.beginPath(); ctx.arc(winX + 44, winY + 22, 6, 0, Math.PI * 2); ctx.fill();
1047
+ ctx.fillStyle = '#27c93f'; ctx.beginPath(); ctx.arc(winX + 64, winY + 22, 6, 0, Math.PI * 2); ctx.fill();
1048
+
1049
+ const codeLines = [
1050
+ 'import { FrameForge } from "@amg555/frameforge";',
1051
+ 'const engine = new FrameForge.AestheticEngine();',
1052
+ 'await engine.renderScene({',
1053
+ ' style: "' + AESTHETIC + '",',
1054
+ ' fidelity: "CINEMATIC_24FPS"',
1055
+ '});'
1056
+ ];
1057
+ ctx.font = '19px "Fira Code", monospace';
1058
+ const typeProg = Math.min(1, t / (window.DURATION * 0.75));
1059
+ const totalChars = codeLines.join('\\n').length;
1060
+ let charsToShow = Math.floor(totalChars * typeProg);
1061
+
1062
+ let lineY = winY + 70;
1063
+ for (let l = 0; l < codeLines.length; l++) {
1064
+ const line = codeLines[l];
1065
+ const sliceLen = Math.min(line.length, charsToShow);
1066
+ const textToDraw = line.slice(0, sliceLen);
1067
+ charsToShow = Math.max(0, charsToShow - line.length - 1);
1068
+
1069
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
1070
+ ctx.textAlign = 'left';
1071
+ ctx.fillText(String(l + 1).padStart(2, '0'), winX + 26, lineY);
1072
+ ctx.fillStyle = l === 0 ? ACCENT : l === 1 ? ACCENT2 : '#e2e8f0';
1073
+ ctx.fillText(textToDraw, winX + 70, lineY);
1074
+ lineY += 32;
1075
+ }
1076
+
1077
+ } else if (COMP_THEME === 'speed_action') {
1078
+ // High-Energy Speed Streaks & Shockwaves
1079
+ const waveR = ((t * 1.8) % 1) * 220;
1080
+ ctx.strokeStyle = ACCENT;
1081
+ ctx.lineWidth = 3;
1082
+ ctx.beginPath(); ctx.arc(cx, cy, waveR, 0, Math.PI * 2); ctx.stroke();
1083
+
1084
+ for (let s = 0; s < 12; s++) {
1085
+ const sx = ((s * 90 + t * 900) % (W * 1.2)) - W * 0.1;
1086
+ ctx.strokeStyle = ACCENT2;
1087
+ ctx.lineWidth = 2.5;
1088
+ ctx.beginPath(); ctx.moveTo(sx, cy - 80); ctx.lineTo(sx - 150, cy + 80); ctx.stroke();
1089
+ }
1090
+
1091
+ } else if (COMP_THEME === 'quote') {
1092
+ // Editorial Quote
1093
+ ctx.font = 'bold 110px serif';
1094
+ ctx.fillStyle = ACCENT;
1095
+ ctx.textAlign = 'center';
1096
+ ctx.fillText('“', cx, cy - 40);
1097
+
1098
+ ctx.font = 'italic 30px ' + FONT_DISPLAY;
1099
+ ctx.fillStyle = '#ffffff';
1100
+ const quoteText = NARRATION.length > 95 ? NARRATION.slice(0, 95) + '...' : NARRATION;
1101
+ ctx.fillText(quoteText, cx, cy + 25);
1102
+
1103
+ } else if (COMP_THEME === 'metric') {
1104
+ // Massive Animated KPI Gauge
1105
+ const centerVal = Math.round(spring(t, 0.15) * 98.4);
1106
+ const radius = 95;
1107
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
1108
+ ctx.lineWidth = 16;
1109
+ ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.stroke();
1110
+
1111
+ ctx.strokeStyle = ACCENT;
1112
+ ctx.lineWidth = 16;
1113
+ ctx.beginPath();
1114
+ const arcEnd = -Math.PI / 2 + (Math.PI * 2) * (spring(t, 0.2) * 0.984);
1115
+ ctx.arc(cx, cy, radius, -Math.PI / 2, arcEnd);
1116
+ ctx.stroke();
1117
+
1118
+ ctx.font = 'bold 54px monospace';
1119
+ ctx.fillStyle = '#ffffff';
1120
+ ctx.textAlign = 'center';
1121
+ ctx.fillText(centerVal + '%', cx, cy + 18);
1122
+
1123
+ } else if (COMP_THEME === 'image' && cachedImage && cachedImage.complete && cachedImage.naturalWidth > 0) {
1124
+ // Floating Ken-Burns Image Card
1125
+ const imgW = W * 0.65;
1126
+ const imgH = H * 0.44;
1127
+ const zoom = 1.0 + (t / window.DURATION) * 0.08;
1128
+ ctx.save();
1129
+ ctx.beginPath(); ctx.roundRect(cx - imgW / 2, cy - imgH / 2, imgW, imgH, 16); ctx.clip();
1130
+ ctx.scale(zoom, zoom);
1131
+ ctx.drawImage(cachedImage, cx - imgW / 2, cy - imgH / 2, imgW, imgH);
1132
+ ctx.restore();
1133
+
1134
+ } else {
1135
+ // Kinetic Frequency Equalizer Waveform
1136
+ const bars = 36;
1137
+ const barW = (W * 0.7) / bars;
1138
+ const startX = cx - (W * 0.7) / 2;
1139
+ for (let b = 0; b < bars; b++) {
1140
+ const phase = t * 4.5 + b * 0.28;
1141
+ const hVal = 25 + 110 * Math.abs(Math.sin(phase) * Math.cos(phase * 0.8));
1142
+ const bx = startX + b * barW;
1143
+ const by = cy - hVal / 2;
1144
+
1145
+ const barGrad = ctx.createLinearGradient(0, by, 0, by + hVal);
1146
+ barGrad.addColorStop(0, ACCENT);
1147
+ barGrad.addColorStop(1, ACCENT2);
1148
+ ctx.fillStyle = barGrad;
1149
+ ctx.beginPath(); ctx.roundRect(bx, by, barW - 6, hVal, 4); ctx.fill();
1150
+ }
1151
+ }
1152
+
1153
+ ctx.restore();
1154
+ }
1155
+
1156
+ // ── Cinematic Broadcast Lower-Third Overlay ────────────────────────────────────
1157
+ function drawCinematicOverlay(t) {
1158
+ ctx.save();
1159
+ const textSpring = spring(t, 0.18);
1160
+ const bannerY = H * 0.74 + (1 - textSpring) * 45;
1161
+ const bannerH = H * 0.18;
1162
+ const bannerW = W * 0.88;
1163
+ const bannerX = (W - bannerW) / 2;
1164
+
1165
+ ctx.globalAlpha = Math.min(1, Math.max(0, textSpring));
1166
+
1167
+ // Glassmorphic Lower-Third Banner
1168
+ ctx.fillStyle = 'rgba(8, 12, 22, 0.82)';
1169
+ ctx.strokeStyle = CARD_BORDER;
1170
+ ctx.lineWidth = 1.5;
1171
+ ctx.beginPath();
1172
+ ctx.roundRect(bannerX, bannerY, bannerW, bannerH, 16);
1173
+ ctx.fill();
1174
+ ctx.stroke();
1175
+
1176
+ // Accent Left Bar
1177
+ ctx.fillStyle = ACCENT;
1178
+ ctx.beginPath();
1179
+ ctx.roundRect(bannerX + 16, bannerY + 16, 6, bannerH - 32, 3);
1180
+ ctx.fill();
1181
+
1182
+ // Tag Pill
1183
+ ctx.font = 'bold 13px monospace';
1184
+ ctx.fillStyle = ACCENT;
1185
+ ctx.textAlign = 'left';
1186
+ ctx.fillText(TAG, bannerX + 38, bannerY + 36);
1187
+
1188
+ // Primary Caption Headline
1189
+ ctx.font = FONT_WEIGHT + ' 44px ' + FONT_DISPLAY;
1190
+ ctx.fillStyle = '#ffffff';
1191
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.9)';
1192
+ ctx.shadowBlur = 16;
1193
+ ctx.shadowOffsetY = 4;
1194
+ ctx.textAlign = 'left';
1195
+ const cleanCap = CAPTION.length > 55 ? CAPTION.slice(0, 55) + '...' : CAPTION;
1196
+ ctx.fillText(cleanCap, bannerX + 38, bannerY + 84);
1197
+
1198
+ // Subtitle / Narration preview
1199
+ if (NARRATION) {
1200
+ ctx.font = '400 23px ' + FONT_BODY;
1201
+ ctx.fillStyle = 'rgba(226, 232, 240, 0.88)';
1202
+ ctx.shadowBlur = 8;
1203
+ const cleanNarr = NARRATION.length > 95 ? NARRATION.slice(0, 95) + '...' : NARRATION;
1204
+ ctx.fillText(cleanNarr, bannerX + 38, bannerY + 120);
1205
+ }
1206
+ ctx.restore();
1207
+
1208
+ // Bottom Progress Scrubber
1209
+ ctx.save();
1210
+ const progress = Math.min(1, Math.max(0, t / window.DURATION));
1211
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
1212
+ ctx.fillRect(0, H - 6, W, 6);
1213
+
1214
+ const progGrad = ctx.createLinearGradient(0, 0, W, 0);
1215
+ progGrad.addColorStop(0, ACCENT);
1216
+ progGrad.addColorStop(1, ACCENT2);
1217
+ ctx.fillStyle = progGrad;
1218
+ ctx.fillRect(0, H - 6, W * progress, 6);
1219
+ ctx.restore();
1220
+ }
1221
+
1222
+ // ── Master Seek Hook ───────────────────────────────────────────────────────────
1223
+ window.seek = async (t) => {
1224
+ drawBackground(t);
1225
+ drawMainScene(t);
1226
+ drawCinematicOverlay(t);
1227
+ };
1228
+ </script>
1229
+ </body>
1230
+ </html>`;
1231
+ }
1232
+
1233
+ module.exports = {
1234
+ generateSceneHtml,
1235
+ wrapCodeInHarnessHtml,
1236
+ detectAesthetic,
1237
+ detectComponentTheme,
1238
+ getScriptFontStack,
1239
+ AESTHETICS,
1240
+ REGIONAL_FONTS,
1241
+ };