@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.
- package/CHANGELOG.md +47 -0
- package/README.md +7 -5
- package/package.json +3 -3
- package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +70 -14
- package/scripts/deep-validate.js +5 -3
- package/src/cli/make.js +6 -0
- package/src/mcp-server/server.js +166 -14
- package/src/video-editor/editor.js +852 -0
- package/src/video-renderer/renderer.js +129 -31
- package/src/video-renderer/scene-animator.js +1033 -251
- package/src/worker/server-render-free.js +5 -0
- package/src/worker/server.js +6 -0
- package/src/workflows/assets.js +25 -3
- package/src/workflows/render.js +26 -3
- package/src/workflows/script.js +2 -0
|
@@ -1,80 +1,484 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/**
|
|
3
|
-
* Scene Animator —
|
|
3
|
+
* Scene Animator — Dynamic, Context-Driven Animation Engine
|
|
4
4
|
*
|
|
5
|
-
* Generates self-contained HTML/Canvas animation scenes
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
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
|
|
25
|
-
const
|
|
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
|
|
178
|
+
* Detect the overall aesthetic from prompt/script metadata, topic, style, and tone.
|
|
29
179
|
*/
|
|
30
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
//
|
|
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: ${
|
|
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
|
|
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 *
|
|
144
|
-
const PARTICLES = Array.from({ length:
|
|
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) *
|
|
148
|
-
vy: (rng() - 0.5) *
|
|
541
|
+
vx: (rng() - 0.5) * 40,
|
|
542
|
+
vy: (rng() - 0.5) * 30 - 10,
|
|
149
543
|
r: 1.5 + rng() * 3.5,
|
|
150
|
-
alpha: 0.
|
|
544
|
+
alpha: 0.2 + rng() * 0.6
|
|
151
545
|
}));
|
|
152
546
|
|
|
153
547
|
// Spring dynamics function
|
|
154
|
-
function spring(t, delay = 0, tension =
|
|
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
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
const cx = W / 2 + Math.sin(t * 0.
|
|
186
|
-
const cy = H / 2 + Math.cos(t * 0.
|
|
187
|
-
const bgGrad = ctx.createRadialGradient(cx, cy, 60, W / 2, H / 2, W * 0.
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
ctx.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
670
|
+
// ── Full-Canvas Cinematic Visual Directors ─────────────────────────────────────
|
|
671
|
+
function drawMainScene(t) {
|
|
229
672
|
ctx.save();
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
ctx.
|
|
234
|
-
ctx.
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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.
|
|
989
|
+
ctx.moveTo(cx, cy);
|
|
990
|
+
ctx.arc(cx, cy, globeR, sweepAngle - 0.6, sweepAngle);
|
|
261
991
|
ctx.fill();
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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.
|
|
1051
|
+
'const engine = new FrameForge.AestheticEngine();',
|
|
276
1052
|
'await engine.renderScene({',
|
|
277
|
-
'
|
|
278
|
-
' fidelity: "
|
|
1053
|
+
' style: "' + AESTHETIC + '",',
|
|
1054
|
+
' fidelity: "CINEMATIC_24FPS"',
|
|
279
1055
|
'});'
|
|
280
1056
|
];
|
|
281
|
-
ctx.font = '
|
|
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 =
|
|
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.
|
|
296
|
-
|
|
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,
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
ctx.
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
328
|
-
ctx.fillText(
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
372
|
-
ctx.
|
|
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, -
|
|
1131
|
+
ctx.drawImage(cachedImage, cx - imgW / 2, cy - imgH / 2, imgW, imgH);
|
|
375
1132
|
ctx.restore();
|
|
1133
|
+
|
|
376
1134
|
} else {
|
|
377
|
-
// Kinetic
|
|
378
|
-
const bars =
|
|
379
|
-
const barW = (
|
|
380
|
-
const startX = -
|
|
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.
|
|
383
|
-
const hVal = 25 +
|
|
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 =
|
|
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
|
-
|
|
398
|
-
|
|
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(
|
|
1172
|
+
ctx.roundRect(bannerX, bannerY, bannerW, bannerH, 16);
|
|
401
1173
|
ctx.fill();
|
|
402
|
-
ctx.
|
|
1174
|
+
ctx.stroke();
|
|
1175
|
+
|
|
1176
|
+
// Accent Left Bar
|
|
403
1177
|
ctx.fillStyle = ACCENT;
|
|
404
|
-
ctx.
|
|
405
|
-
ctx.
|
|
1178
|
+
ctx.beginPath();
|
|
1179
|
+
ctx.roundRect(bannerX + 16, bannerY + 16, 6, bannerH - 32, 3);
|
|
1180
|
+
ctx.fill();
|
|
406
1181
|
|
|
407
|
-
|
|
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
|
-
//
|
|
410
|
-
ctx.
|
|
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.
|
|
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
|
|
1200
|
+
ctx.font = '400 23px ' + FONT_BODY;
|
|
429
1201
|
ctx.fillStyle = 'rgba(226, 232, 240, 0.88)';
|
|
430
|
-
ctx.shadowBlur =
|
|
1202
|
+
ctx.shadowBlur = 8;
|
|
431
1203
|
const cleanNarr = NARRATION.length > 95 ? NARRATION.slice(0, 95) + '...' : NARRATION;
|
|
432
|
-
ctx.fillText(cleanNarr,
|
|
1204
|
+
ctx.fillText(cleanNarr, bannerX + 38, bannerY + 120);
|
|
433
1205
|
}
|
|
434
1206
|
ctx.restore();
|
|
435
1207
|
|
|
436
|
-
//
|
|
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 -
|
|
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 -
|
|
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
|
-
|
|
458
|
-
|
|
1235
|
+
wrapCodeInHarnessHtml,
|
|
1236
|
+
detectAesthetic,
|
|
1237
|
+
detectComponentTheme,
|
|
1238
|
+
getScriptFontStack,
|
|
1239
|
+
AESTHETICS,
|
|
1240
|
+
REGIONAL_FONTS,
|
|
459
1241
|
};
|