@amg555/frameforge-mcp 2.3.5 → 2.3.7

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.
@@ -1,80 +1,484 @@
1
1
  'use strict';
2
2
  /**
3
- * Scene Animator — Code-Driven HTML/Canvas Scene Generator
3
+ * Scene Animator — Dynamic, Context-Driven Animation Engine
4
4
  *
5
- * Generates self-contained HTML/Canvas animation scenes conforming to the
6
- * FrameForge frame-purity contract:
7
- * - window.DURATION = seconds
8
- * - window.seek = async (t) => draws frame at time t
9
- * - Every frame is a pure function of t (no Date.now, no Math.random without seed)
10
- * - Multilingual typography support: Malayalam, Indic, CJK, Arabic, Latin
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.
11
8
  *
12
- * Supported component themes:
13
- * - 'code': IDE/terminal window with typing code and cursor
14
- * - 'chart': Animated bar chart, progress ring, and metric counter
15
- * - 'nodes': Pulsing network nodes with traveling energy packets
16
- * - 'diagram': Flowchart steps with directional energy arrows
17
- * - 'image': Floating Ken-Burns card embedding user-provided image
18
- * - 'kinetic': Audio spectrum visualizer + isometric geometry
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
19
20
  */
20
21
 
21
22
  const fs = require('fs');
22
23
  const path = require('path');
23
24
 
24
- // Multilingual font family stack prioritizing Indic/Malayalam system fonts
25
- const FONT_STACK = `'Nirmala UI', 'Noto Sans Malayalam', 'Malayalam Sangam MN', 'Kartika', 'Meera', 'Manjari', -apple-system, 'Segoe UI', system-ui, sans-serif`;
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
+ };
26
175
 
176
+ // ── Smart Aesthetic Inference ──────────────────────────────────────────────────
27
177
  /**
28
- * Detect the best visual theme from visual description, caption, and media.
178
+ * Detect the overall aesthetic from prompt/script metadata, topic, style, and tone.
29
179
  */
30
- function detectTheme({ visualDescription = '', caption = '', imagePath = null } = {}) {
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' } = {}) {
31
267
  if (imagePath) return 'image';
32
- const text = `${visualDescription} ${caption}`.toLowerCase();
33
- if (/\b(code|function|syntax|api|javascript|python|typescript|json|sql|query|endpoint|algorithm|struct|cli|terminal|bash|shell|npm|git)\b/.test(text)) {
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)) {
34
315
  return 'code';
35
316
  }
36
- if (/\b(chart|data|metric|analytics|growth|graph|statistic|stats|revenue|percent|count|number)\b/.test(text)) {
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)) {
37
321
  return 'chart';
38
322
  }
39
- if (/\b(network|node|ai|model|neural|pipeline|architecture|cloud|server|system|mesh|connect)\b/.test(text)) {
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)) {
40
327
  return 'nodes';
41
328
  }
42
- if (/\b(diagram|flow|step|process|workflow|sequence|lifecycle|cycle)\b/.test(text)) {
43
- return 'diagram';
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';
44
334
  }
45
- return 'kinetic';
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';
46
346
  }
47
347
 
348
+ // ── Harness HTML Wrapper for User / Agent Code ────────────────────────────────
48
349
  /**
49
- * Generate a complete self-contained HTML scene string.
50
- *
51
- * @param {object} opts
52
- * @param {string} opts.caption
53
- * @param {string} [opts.narration]
54
- * @param {string} [opts.visualDescription]
55
- * @param {string} [opts.imagePath]
56
- * @param {number} opts.duration
57
- * @param {number} [opts.width]
58
- * @param {number} [opts.height]
59
- * @param {number} [opts.sceneIndex]
60
- * @param {string} [opts.musicMood]
61
- * @returns {string} HTML document
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.
62
434
  */
63
435
  function generateSceneHtml({
64
436
  caption = '',
65
437
  narration = '',
66
438
  visualDescription = '',
67
439
  imagePath = null,
440
+ topic = '',
441
+ style = '',
442
+ theme = '',
443
+ title = '',
444
+ language = '',
445
+ musicMood = 'neutral',
68
446
  duration = 3,
69
447
  width = 1920,
70
448
  height = 1080,
71
449
  sceneIndex = 0,
72
- musicMood = 'neutral',
73
450
  } = {}) {
74
- const theme = detectTheme({ visualDescription, caption, imagePath });
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
+ });
75
471
  const clampedDur = Math.max(1, Number(duration) || 3);
76
472
 
77
- // Convert imagePath to data URL if local file exists
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
78
482
  let imageDataUri = '';
79
483
  if (imagePath && fs.existsSync(imagePath)) {
80
484
  try {
@@ -85,16 +489,6 @@ function generateSceneHtml({
85
489
  } catch (_) {}
86
490
  }
87
491
 
88
- // Color palettes based on scene index and music mood
89
- const PALETTES = [
90
- { bg0: '#142036', bg1: '#0b111c', bg2: '#05080f', accent: '#38bdf8', accent2: '#818cf8', tag: 'CYBER TECH' },
91
- { bg0: '#231436', bg1: '#110a1c', bg2: '#07040d', accent: '#c084fc', accent2: '#f43f5e', tag: 'DEEP DYNAMICS' },
92
- { bg0: '#0e261f', bg1: '#071511', bg2: '#030a08', accent: '#34d399', accent2: '#38bdf8', tag: 'SYSTEM ACTIVE' },
93
- { bg0: '#291d0e', bg1: '#150f07', bg2: '#0a0703', accent: '#fbbf24', accent2: '#f97316', tag: 'HIGH IMPACT' },
94
- { bg0: '#161d2b', bg1: '#0d131d', bg2: '#06090e', accent: '#60a5fa', accent2: '#a78bfa', tag: 'INTELLIGENCE' },
95
- ];
96
- const pal = PALETTES[sceneIndex % PALETTES.length];
97
-
98
492
  return `<!doctype html>
99
493
  <html lang="en">
100
494
  <head>
@@ -107,7 +501,7 @@ function generateSceneHtml({
107
501
  height: 100%;
108
502
  overflow: hidden;
109
503
  background: ${pal.bg2};
110
- font-family: ${FONT_STACK};
504
+ font-family: ${fontBody};
111
505
  -webkit-font-smoothing: antialiased;
112
506
  }
113
507
  canvas {
@@ -126,7 +520,7 @@ const ctx = canvas.getContext('2d');
126
520
  const W = ${width};
127
521
  const H = ${height};
128
522
 
129
- // Seeded PRNG for frame purity (same t -> exact same frame)
523
+ // Seeded PRNG for frame purity
130
524
  function sfc32(a, b, c, d) {
131
525
  return function() {
132
526
  a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
@@ -140,18 +534,18 @@ function sfc32(a, b, c, d) {
140
534
  };
141
535
  }
142
536
 
143
- const rng = sfc32(${42 + sceneIndex * 17}, ${1337 + sceneIndex * 31}, ${7 + sceneIndex * 11}, 99);
144
- const PARTICLES = Array.from({ length: 54 }, () => ({
537
+ const rng = sfc32(${42 + sceneIndex * 19}, ${1337 + sceneIndex * 29}, ${7 + sceneIndex * 13}, 99);
538
+ const PARTICLES = Array.from({ length: 75 }, () => ({
145
539
  x: rng() * W,
146
540
  y: rng() * H,
147
- vx: (rng() - 0.5) * 50,
148
- vy: (rng() - 0.5) * 35 - 15,
541
+ vx: (rng() - 0.5) * 40,
542
+ vy: (rng() - 0.5) * 30 - 10,
149
543
  r: 1.5 + rng() * 3.5,
150
- alpha: 0.3 + rng() * 0.5
544
+ alpha: 0.2 + rng() * 0.6
151
545
  }));
152
546
 
153
547
  // Spring dynamics function
154
- function spring(t, delay = 0, tension = 160, friction = 14) {
548
+ function spring(t, delay = 0, tension = ${aesthetic.springTension}, friction = ${aesthetic.springFriction}) {
155
549
  const dt = t - delay;
156
550
  if (dt <= 0) return 0;
157
551
  const omega = Math.sqrt(tension);
@@ -163,7 +557,9 @@ function spring(t, delay = 0, tension = 160, friction = 14) {
163
557
  return 1 - Math.exp(-omega * dt);
164
558
  }
165
559
 
166
- const THEME = ${JSON.stringify(theme)};
560
+ const AESTHETIC = ${JSON.stringify(aestheticKey)};
561
+ const BG_TYPE = ${JSON.stringify(aesthetic.backgroundType)};
562
+ const COMP_THEME = ${JSON.stringify(compTheme)};
167
563
  const CAPTION = ${JSON.stringify(caption)};
168
564
  const NARRATION = ${JSON.stringify(narration)};
169
565
  const TAG = ${JSON.stringify(pal.tag)};
@@ -172,6 +568,11 @@ const ACCENT2 = ${JSON.stringify(pal.accent2)};
172
568
  const BG0 = ${JSON.stringify(pal.bg0)};
173
569
  const BG1 = ${JSON.stringify(pal.bg1)};
174
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)};
175
576
  const IMAGE_SRC = ${JSON.stringify(imageDataUri)};
176
577
 
177
578
  let cachedImage = null;
@@ -180,273 +581,650 @@ if (IMAGE_SRC) {
180
581
  cachedImage.src = IMAGE_SRC;
181
582
  }
182
583
 
183
- window.seek = async (t) => {
184
- // ── 1. Dynamic Moving Gradient Background ────────────────────────────────
185
- const cx = W / 2 + Math.sin(t * 0.7) * 160;
186
- const cy = H / 2 + Math.cos(t * 0.5) * 90;
187
- const bgGrad = ctx.createRadialGradient(cx, cy, 60, W / 2, H / 2, W * 0.85);
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);
188
589
  bgGrad.addColorStop(0, BG0);
189
590
  bgGrad.addColorStop(0.55, BG1);
190
591
  bgGrad.addColorStop(1, BG2);
191
592
  ctx.fillStyle = bgGrad;
192
593
  ctx.fillRect(0, 0, W, H);
193
594
 
194
- // ── 2. Subtle Moving Perspective Grid ───────────────────────────────────
195
595
  ctx.save();
196
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.04)';
197
- ctx.lineWidth = 1;
198
- const gridShift = (t * 30) % 60;
199
- for (let x = 0; x <= W; x += 60) {
200
- ctx.beginPath();
201
- ctx.moveTo(x, 0);
202
- ctx.lineTo(x, H);
203
- ctx.stroke();
204
- }
205
- for (let y = gridShift; y <= H; y += 60) {
206
- ctx.beginPath();
207
- ctx.moveTo(0, y);
208
- ctx.lineTo(W, y);
209
- ctx.stroke();
210
- }
211
- ctx.restore();
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();
212
630
 
213
- // ── 3. Floating Dust & Energy Particles ──────────────────────────────────
214
- ctx.save();
215
- for (let i = 0; i < PARTICLES.length; i++) {
216
- const p = PARTICLES[i];
217
- const px = (p.x + p.vx * t + W * 10) % W;
218
- const py = (p.y + p.vy * t + H * 10) % H;
219
- const pulse = 0.4 + 0.6 * Math.sin(t * 3.2 + i * 0.5);
220
- ctx.fillStyle = ACCENT;
221
- ctx.globalAlpha = p.alpha * pulse * 0.7;
222
- ctx.beginPath();
223
- ctx.arc(px, py, p.r, 0, Math.PI * 2);
224
- ctx.fill();
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
+ }
225
666
  }
226
667
  ctx.restore();
668
+ }
227
669
 
228
- // ── 4. Central Graphic Card matching Theme ──────────────────────────────
670
+ // ── Full-Canvas Cinematic Visual Directors ─────────────────────────────────────
671
+ function drawMainScene(t) {
229
672
  ctx.save();
230
- const cardSpring = spring(t, 0.08, 140, 13);
231
- const cardScale = Math.min(1.04, Math.max(0, cardSpring));
232
- const cardY = H * 0.40 + (1 - cardSpring) * 50;
233
- ctx.translate(W / 2, cardY);
234
- ctx.scale(cardScale, cardScale);
235
-
236
- const cardW = Math.round(W * 0.55);
237
- const cardH = Math.round(H * 0.38);
238
-
239
- // Card backdrop with glassmorphic border
240
- ctx.fillStyle = 'rgba(11, 17, 28, 0.82)';
241
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
242
- ctx.lineWidth = 2;
243
- ctx.beginPath();
244
- ctx.roundRect(-cardW / 2, -cardH / 2, cardW, cardH, 20);
245
- ctx.fill();
246
- ctx.stroke();
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);
247
678
 
248
- // Accent edge highlight
249
- ctx.strokeStyle = ACCENT;
250
- ctx.lineWidth = 2;
251
- ctx.beginPath();
252
- ctx.moveTo(-cardW / 2 + 30, -cardH / 2);
253
- ctx.lineTo(cardW / 2 - 30, -cardH / 2);
254
- ctx.stroke();
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;
255
964
 
256
- if (THEME === 'code') {
257
- // IDE Titlebar
258
- ctx.fillStyle = 'rgba(255, 255, 255, 0.05)';
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;
259
988
  ctx.beginPath();
260
- ctx.roundRect(-cardW / 2, -cardH / 2, cardW, 40, [20, 20, 0, 0]);
989
+ ctx.moveTo(cx, cy);
990
+ ctx.arc(cx, cy, globeR, sweepAngle - 0.6, sweepAngle);
261
991
  ctx.fill();
262
- // Window dots
263
- ctx.fillStyle = '#ff5f56'; ctx.beginPath(); ctx.arc(-cardW / 2 + 25, -cardH / 2 + 20, 6, 0, Math.PI * 2); ctx.fill();
264
- ctx.fillStyle = '#ffbd2e'; ctx.beginPath(); ctx.arc(-cardW / 2 + 45, -cardH / 2 + 20, 6, 0, Math.PI * 2); ctx.fill();
265
- ctx.fillStyle = '#27c93f'; ctx.beginPath(); ctx.arc(-cardW / 2 + 65, -cardH / 2 + 20, 6, 0, Math.PI * 2); ctx.fill();
266
- // Tab title
267
- ctx.font = '14px monospace';
268
- ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
269
- ctx.textAlign = 'left';
270
- ctx.fillText('scene-pipeline.js', -cardW / 2 + 90, -cardH / 2 + 25);
271
-
272
- // Code lines with typing effect
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
+
273
1049
  const codeLines = [
274
1050
  'import { FrameForge } from "@amg555/frameforge";',
275
- 'const engine = new FrameForge.AnimationEngine({ t });',
1051
+ 'const engine = new FrameForge.AestheticEngine();',
276
1052
  'await engine.renderScene({',
277
- ' status: "ACTIVE_24FPS",',
278
- ' fidelity: "BROADCAST_BT709"',
1053
+ ' style: "' + AESTHETIC + '",',
1054
+ ' fidelity: "CINEMATIC_24FPS"',
279
1055
  '});'
280
1056
  ];
281
- ctx.font = '18px "Fira Code", monospace';
1057
+ ctx.font = '19px "Fira Code", monospace';
282
1058
  const typeProg = Math.min(1, t / (window.DURATION * 0.75));
283
1059
  const totalChars = codeLines.join('\\n').length;
284
1060
  let charsToShow = Math.floor(totalChars * typeProg);
285
1061
 
286
- let lineY = -cardH / 2 + 80;
1062
+ let lineY = winY + 70;
287
1063
  for (let l = 0; l < codeLines.length; l++) {
288
1064
  const line = codeLines[l];
289
1065
  const sliceLen = Math.min(line.length, charsToShow);
290
1066
  const textToDraw = line.slice(0, sliceLen);
291
1067
  charsToShow = Math.max(0, charsToShow - line.length - 1);
292
1068
 
293
- // Line number
294
1069
  ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
295
- ctx.fillText(String(l + 1).padStart(2, '0'), -cardW / 2 + 30, lineY);
296
- // Syntax text
1070
+ ctx.textAlign = 'left';
1071
+ ctx.fillText(String(l + 1).padStart(2, '0'), winX + 26, lineY);
297
1072
  ctx.fillStyle = l === 0 ? ACCENT : l === 1 ? ACCENT2 : '#e2e8f0';
298
- ctx.fillText(textToDraw, -cardW / 2 + 75, lineY);
1073
+ ctx.fillText(textToDraw, winX + 70, lineY);
299
1074
  lineY += 32;
300
1075
  }
301
- } else if (THEME === 'chart') {
302
- // Animated bar chart + metrics
303
- const bars = 8;
304
- const barW = (cardW - 140) / bars;
305
- const startX = -cardW / 2 + 70;
306
- const baseLine = cardH / 2 - 50;
307
1076
 
308
- for (let b = 0; b < bars; b++) {
309
- const barSpring = spring(t, 0.15 + b * 0.05, 120, 12);
310
- const targetH = 50 + ((b * 37) % 140);
311
- const curH = targetH * Math.min(1.05, Math.max(0, barSpring));
312
- const bx = startX + b * barW;
313
- const by = baseLine - curH;
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();
314
1083
 
315
- const barGrad = ctx.createLinearGradient(0, by, 0, baseLine);
316
- barGrad.addColorStop(0, ACCENT);
317
- barGrad.addColorStop(1, ACCENT2);
318
- ctx.fillStyle = barGrad;
319
- ctx.beginPath();
320
- ctx.roundRect(bx, by, barW - 12, curH, 6);
321
- ctx.fill();
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();
322
1089
  }
323
- // Animated Counter
324
- const counterVal = Math.round(spring(t, 0.2, 100, 10) * 98.4);
325
- ctx.font = 'bold 36px monospace';
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;
326
1099
  ctx.fillStyle = '#ffffff';
327
- ctx.textAlign = 'right';
328
- ctx.fillText(counterVal + '% METRIC GAIN', cardW / 2 - 40, -cardH / 2 + 50);
329
- } else if (THEME === 'nodes') {
330
- // Interconnected pulsing network nodes
331
- const nodeCoords = [
332
- [-cardW * 0.35, -30], [-cardW * 0.15, 40], [0, -50],
333
- [cardW * 0.15, 30], [cardW * 0.35, -20]
334
- ];
335
- // Connections
336
- ctx.strokeStyle = 'rgba(56, 189, 248, 0.35)';
337
- ctx.lineWidth = 2;
338
- for (let i = 0; i < nodeCoords.length - 1; i++) {
339
- ctx.beginPath();
340
- ctx.moveTo(nodeCoords[i][0], nodeCoords[i][1]);
341
- ctx.lineTo(nodeCoords[i + 1][0], nodeCoords[i + 1][1]);
342
- ctx.stroke();
1100
+ const quoteText = NARRATION.length > 95 ? NARRATION.slice(0, 95) + '...' : NARRATION;
1101
+ ctx.fillText(quoteText, cx, cy + 25);
343
1102
 
344
- // Energy packet along connection
345
- const packetPos = (t * 0.8 + i * 0.25) % 1;
346
- const ex = nodeCoords[i][0] + (nodeCoords[i + 1][0] - nodeCoords[i][0]) * packetPos;
347
- const ey = nodeCoords[i][1] + (nodeCoords[i + 1][1] - nodeCoords[i][1]) * packetPos;
348
- ctx.fillStyle = ACCENT;
349
- ctx.beginPath();
350
- ctx.arc(ex, ey, 5, 0, Math.PI * 2);
351
- ctx.fill();
352
- }
353
- // Nodes
354
- for (let i = 0; i < nodeCoords.length; i++) {
355
- const [nx, ny] = nodeCoords[i];
356
- const pulse = 1 + 0.25 * Math.sin(t * 4 + i);
357
- ctx.fillStyle = ACCENT2;
358
- ctx.beginPath();
359
- ctx.arc(nx, ny, 14 * pulse, 0, Math.PI * 2);
360
- ctx.fill();
361
- ctx.fillStyle = '#ffffff';
362
- ctx.beginPath();
363
- ctx.arc(nx, ny, 6, 0, Math.PI * 2);
364
- ctx.fill();
365
- }
366
- } else if (THEME === 'image' && cachedImage && cachedImage.complete && cachedImage.naturalWidth > 0) {
367
- // Ken-Burns floating image
368
- const zoom = 1.0 + (t / window.DURATION) * 0.07;
369
- ctx.save();
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;
370
1113
  ctx.beginPath();
371
- ctx.roundRect(-cardW / 2 + 12, -cardH / 2 + 12, cardW - 24, cardH - 24, 14);
372
- ctx.clip();
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();
373
1130
  ctx.scale(zoom, zoom);
374
- ctx.drawImage(cachedImage, -cardW / 2 + 12, -cardH / 2 + 12, cardW - 24, cardH - 24);
1131
+ ctx.drawImage(cachedImage, cx - imgW / 2, cy - imgH / 2, imgW, imgH);
375
1132
  ctx.restore();
1133
+
376
1134
  } else {
377
- // Kinetic Audio Spectrum Bars
378
- const bars = 28;
379
- const barW = (cardW - 80) / bars;
380
- const startX = -cardW / 2 + 40;
1135
+ // Kinetic Frequency Equalizer Waveform
1136
+ const bars = 36;
1137
+ const barW = (W * 0.7) / bars;
1138
+ const startX = cx - (W * 0.7) / 2;
381
1139
  for (let b = 0; b < bars; b++) {
382
- const phase = t * 4.5 + b * 0.3;
383
- const hVal = 25 + 95 * Math.abs(Math.sin(phase) * Math.cos(phase * 0.8));
1140
+ const phase = t * 4.5 + b * 0.28;
1141
+ const hVal = 25 + 110 * Math.abs(Math.sin(phase) * Math.cos(phase * 0.8));
384
1142
  const bx = startX + b * barW;
385
- const by = 20 - hVal / 2;
1143
+ const by = cy - hVal / 2;
386
1144
 
387
1145
  const barGrad = ctx.createLinearGradient(0, by, 0, by + hVal);
388
1146
  barGrad.addColorStop(0, ACCENT);
389
1147
  barGrad.addColorStop(1, ACCENT2);
390
1148
  ctx.fillStyle = barGrad;
391
- ctx.beginPath();
392
- ctx.roundRect(bx, by, barW - 6, hVal, 4);
393
- ctx.fill();
1149
+ ctx.beginPath(); ctx.roundRect(bx, by, barW - 6, hVal, 4); ctx.fill();
394
1150
  }
395
1151
  }
396
1152
 
397
- // Tag Badge
398
- ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
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;
399
1171
  ctx.beginPath();
400
- ctx.roundRect(-cardW / 2 + 25, -cardH / 2 + 20, 140, 30, 15);
1172
+ ctx.roundRect(bannerX, bannerY, bannerW, bannerH, 16);
401
1173
  ctx.fill();
402
- ctx.font = 'bold 13px system-ui, sans-serif';
1174
+ ctx.stroke();
1175
+
1176
+ // Accent Left Bar
403
1177
  ctx.fillStyle = ACCENT;
404
- ctx.textAlign = 'center';
405
- ctx.fillText(TAG, -cardW / 2 + 95, -cardH / 2 + 40);
1178
+ ctx.beginPath();
1179
+ ctx.roundRect(bannerX + 16, bannerY + 16, 6, bannerH - 32, 3);
1180
+ ctx.fill();
406
1181
 
407
- ctx.restore();
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);
408
1187
 
409
- // ── 5. Kinetic Typography with Multilingual / Malayalam Support ─────────
410
- ctx.save();
411
- const textSpring = spring(t, 0.22, 150, 13);
412
- const textY = H * 0.77 + (1 - textSpring) * 45;
413
- const textAlpha = Math.min(1, Math.max(0, textSpring));
414
-
415
- ctx.globalAlpha = textAlpha;
416
- ctx.textAlign = 'center';
417
-
418
- // Primary Caption / Headline
419
- ctx.font = 'bold 54px ' + ${JSON.stringify(FONT_STACK)};
420
- ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
421
- ctx.shadowBlur = 20;
422
- ctx.shadowOffsetY = 6;
1188
+ // Primary Caption Headline
1189
+ ctx.font = FONT_WEIGHT + ' 44px ' + FONT_DISPLAY;
423
1190
  ctx.fillStyle = '#ffffff';
424
- ctx.fillText(CAPTION, W / 2, textY);
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);
425
1197
 
426
1198
  // Subtitle / Narration preview
427
1199
  if (NARRATION) {
428
- ctx.font = '400 28px ' + ${JSON.stringify(FONT_STACK)};
1200
+ ctx.font = '400 23px ' + FONT_BODY;
429
1201
  ctx.fillStyle = 'rgba(226, 232, 240, 0.88)';
430
- ctx.shadowBlur = 12;
1202
+ ctx.shadowBlur = 8;
431
1203
  const cleanNarr = NARRATION.length > 95 ? NARRATION.slice(0, 95) + '...' : NARRATION;
432
- ctx.fillText(cleanNarr, W / 2, textY + 62);
1204
+ ctx.fillText(cleanNarr, bannerX + 38, bannerY + 120);
433
1205
  }
434
1206
  ctx.restore();
435
1207
 
436
- // ── 6. Progress Scrubber ────────────────────────────────────────────────
1208
+ // Bottom Progress Scrubber
437
1209
  ctx.save();
438
1210
  const progress = Math.min(1, Math.max(0, t / window.DURATION));
439
1211
  ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
440
- ctx.fillRect(0, H - 8, W, 8);
1212
+ ctx.fillRect(0, H - 6, W, 6);
441
1213
 
442
1214
  const progGrad = ctx.createLinearGradient(0, 0, W, 0);
443
1215
  progGrad.addColorStop(0, ACCENT);
444
1216
  progGrad.addColorStop(1, ACCENT2);
445
1217
  ctx.fillStyle = progGrad;
446
- ctx.fillRect(0, H - 8, W * progress, 8);
1218
+ ctx.fillRect(0, H - 6, W * progress, 6);
447
1219
  ctx.restore();
1220
+ }
1221
+
1222
+ // ── Master Seek Hook ───────────────────────────────────────────────────────────
1223
+ window.seek = async (t) => {
1224
+ drawBackground(t);
1225
+ drawMainScene(t);
1226
+ drawCinematicOverlay(t);
448
1227
  };
449
- window.ready = true;
450
1228
  </script>
451
1229
  </body>
452
1230
  </html>`;
@@ -454,6 +1232,10 @@ window.ready = true;
454
1232
 
455
1233
  module.exports = {
456
1234
  generateSceneHtml,
457
- detectTheme,
458
- FONT_STACK,
1235
+ wrapCodeInHarnessHtml,
1236
+ detectAesthetic,
1237
+ detectComponentTheme,
1238
+ getScriptFontStack,
1239
+ AESTHETICS,
1240
+ REGIONAL_FONTS,
459
1241
  };