@amg555/frameforge-mcp 2.4.2 → 2.4.3
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 +28 -0
- package/out/frames/f000048.jpg +0 -0
- package/out/frames/f000049.jpg +0 -0
- package/out/frames/f000050.jpg +0 -0
- package/out/frames/f000051.jpg +0 -0
- package/out/frames/f000052.jpg +0 -0
- package/out/frames/f000053.jpg +0 -0
- package/out/frames/f000054.jpg +0 -0
- package/out/frames/f000055.jpg +0 -0
- package/out/frames/f000056.jpg +0 -0
- package/out/frames/f000057.jpg +0 -0
- package/out/frames/f000058.jpg +0 -0
- package/out/frames/f000059.jpg +0 -0
- package/package.json +1 -1
- package/scripts/deep-validate.js +2 -2
- package/src/mcp-server/server.js +1 -1
- package/src/video-renderer/scene-animator.js +411 -286
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ All notable changes to FrameForge are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [2.4.3] — 2026-11-09
|
|
8
|
+
|
|
9
|
+
### Enhanced
|
|
10
|
+
- **Sentence-Synchronized Subtitles (`src/video-renderer/scene-animator.js`)**:
|
|
11
|
+
- Replaced static slide titles, topic banners, and category tags (`// CHRONICLE`, `// HERITAGE`, top headlines) with sentence-synchronized broadcast subtitles centered at bottom with subtle frosted glass backing pills.
|
|
12
|
+
- Automatically slices narration scripts into sentence beats keyed to duration (`DURATION / sentenceCount`) with smooth spring entrance transitions (`spring(localT, 0.04, 210, 16)`).
|
|
13
|
+
- Word wrapping for long sentences to prevent clipping.
|
|
14
|
+
|
|
15
|
+
- **3-Beat Progressive Choreography & Dynamic Camera (`src/video-renderer/scene-animator.js`)**:
|
|
16
|
+
- Added multi-beat progression across all 9 doodle themes:
|
|
17
|
+
- `doodle_dressing`: Beat 1 browsing wardrobe $\to$ Beat 2 trying on shirt $\to$ Beat 3 smiling & waving in glowing amber mirror.
|
|
18
|
+
- `doodle_shiver_cold`: Beat 1 shivering alone $\to$ Beat 2 "NOT FOR COLD" stamp $\to$ Beat 3 "NOT FOR PROTECTION" stamp with character contemplating.
|
|
19
|
+
- `doodle_hunter_savannah`: Character walking with spear across savannah grass swatting mosquitoes under sun.
|
|
20
|
+
- `doodle_sewing_needle`: Campfire $\to$ loupe zoom into bone needle stitching animal hide $\to$ archaeological timeline badge.
|
|
21
|
+
- `doodle_shame_reflection`: Looking down $\to$ water reflection ripples clear $\to$ blush & Theory of Mind halo.
|
|
22
|
+
- `doodle_tribe_meeting`: Two clans approach $\to$ peace greeting $\to$ social language connection arc.
|
|
23
|
+
- `doodle_thinking_future`: Campfire thinker $\to$ thought cloud $\to$ planning for tomorrow tools & sunrise.
|
|
24
|
+
- `doodle_microscope_lice`: Character scratching itching pelt $\to$ microscope slide $\to$ head vs body lice divergence badge.
|
|
25
|
+
- Added subtle cinematic camera drift and breathing (`camZoom`, `camPanX`, `camPanY`) via progress $p = t / DURATION$.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **Regex Parsing Bug in Template Literal (`src/video-renderer/scene-animator.js`)**:
|
|
29
|
+
- Fixed syntax error caused by unescaped newline in regex literal inside template literal string.
|
|
30
|
+
- **Theme Detection Priority (`src/video-renderer/scene-animator.js`)**:
|
|
31
|
+
- Prioritized specific narrative themes (`doodle_microscope_lice`, `doodle_shiver_cold`, `doodle_hunter_savannah`, `doodle_tribe_meeting`, `doodle_thinking_future`, `doodle_sewing_needle`, `doodle_shame_reflection`) ahead of general wardrobe dressing to guarantee every scene gets its exact script-matched visual doodle.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
7
35
|
## [2.4.2] — 2026-11-09
|
|
8
36
|
|
|
9
37
|
### Added
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amg555/frameforge-mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.3",
|
|
4
4
|
"description": "FrameForge — AI video production engine for agentic IDEs. npx @amg555/frameforge-mcp. FFmpeg + TTS run on your machine. 21 MCP tools.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
package/scripts/deep-validate.js
CHANGED
|
@@ -20,7 +20,7 @@ check('batch handling', code.includes('Array.isArray(body)'));
|
|
|
20
20
|
check('ping handler', code.includes("method === 'ping'"));
|
|
21
21
|
check('notifications/init', code.includes("notifications/initialized"));
|
|
22
22
|
check('body size limit', code.includes('MAX_BODY_BYTES'));
|
|
23
|
-
check('SERVER_INFO version 2.4.
|
|
23
|
+
check('SERVER_INFO version 2.4.3', code.includes("version: '2.4.3'"));
|
|
24
24
|
check('18 tools defined (incl get_job_status)', code.includes('get_job_status'));
|
|
25
25
|
check('export_to_editor tool defined', code.includes('export_to_editor'));
|
|
26
26
|
check('edit_video tool defined', code.includes('edit_video'));
|
|
@@ -299,7 +299,7 @@ check('cf-worker has checkRateLimit', cfWorker.includes('checkRateLimit'));
|
|
|
299
299
|
check('cf-worker hashes token (SHA-256)', cfWorker.includes('SHA-256'));
|
|
300
300
|
check('cf-worker uses RATE_LIMITS KV', cfWorker.includes('env.RATE_LIMITS'));
|
|
301
301
|
check('cf-worker returns 429 on limit', cfWorker.includes('429'));
|
|
302
|
-
check('cf-worker version 2.4.
|
|
302
|
+
check('cf-worker version 2.4.3', cfWorker.includes("'2.4.3'"));
|
|
303
303
|
check('wrangler.toml has kv_namespaces',fs.readFileSync('./cloudflare-mcp/wrangler.toml','utf8').includes('kv_namespaces'));
|
|
304
304
|
|
|
305
305
|
// ── plugins/skills present ────────────────────────────────────────────────
|
package/src/mcp-server/server.js
CHANGED
|
@@ -268,54 +268,54 @@ function detectComponentTheme({ visualDescription = '', caption = '', narration
|
|
|
268
268
|
const text = `${visualDescription} ${caption} ${narration} ${topic}`.toLowerCase();
|
|
269
269
|
|
|
270
270
|
// ── Doodled Character Actions (Human Stories, History, Evolution, Daily Life) ──
|
|
271
|
-
// 1. Microscope / Lice Theory / Parasite / Genetics (
|
|
271
|
+
// 1. Microscope / Lice Theory / Parasite / Genetics (Scene 7)
|
|
272
272
|
if (/(?:^|[^\w])(lice|louse|microscope|fiber|weave|parasite|lice evolution|genetic biology proof)(?:[^\w]|$)/i.test(text)
|
|
273
273
|
|| /പേൻ|പേൻ തിയറി|തലയിലെ പേൻ|ശരീരത്തിലെ പേൻ|ലൈസ് തിയറി|മൈക്രോസ്കോപ്പ്/.test(text)) {
|
|
274
274
|
return 'doodle_microscope_lice';
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
// 2.
|
|
278
|
-
if (/(?:^|[^\w])(dress|dressing|wearing clothes|shirt|wardrobe|closet|hanger|outfit|choose clothes|wear|garment|wedding dress|interview|daily habit|why we wear clothes)(?:[^\w]|$)/i.test(text)
|
|
279
|
-
|| /വസ്ത്രധാരണം|ധരിച്ചിട്ടുണ്ടാ|ഷർട്ട്|തിരഞ്ഞെടുത്തത്|തുണി എടുത്തിടുകയല്ല|കല്യാണത്തിന്|ഇന്റർവ്യൂ|ആമുഖം: വസ്ത്രധാരണം|വസ്ത്രം തിരഞ്ഞെടുക്കുമ്പോൾ|വസ്ത്രം തിരഞ്ഞെടുക്കും/.test(text)) {
|
|
280
|
-
return 'doodle_dressing';
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// 3. Shivering / Cold / Protection Flaw / Winter / Freezing (e.g. myth busting cold defense)
|
|
277
|
+
// 2. Shivering / Cold / Protection Flaw / Winter / Freezing (Scene 2, 6)
|
|
284
278
|
if (/(?:^|[^\w])(shiver|shivering|cold|freeze|freezing|ice|winter|teeth|chatter|protection flaw)(?:[^\w]|$)/i.test(text)
|
|
285
|
-
||
|
|
279
|
+
|| /സാധാരണ ധാരണകൾ|തണുപ്പും സംരക്ഷണവും|തള്ളിക്കളയുന്നു|ശരീരത്തെ സംരക്ഷിക്കാൻ|ശരീര സംരക്ഷണം|വിറയ്ക്കുക|തണുപ്പിൽ നിന്ന് രക്ഷ നേടാൻ|തണുപ്പ് എന്ന കാരണം|തണുപ്പുള്ള സ്ഥലങ്ങളിലേക്ക്/.test(text)) {
|
|
286
280
|
return 'doodle_shiver_cold';
|
|
287
281
|
}
|
|
288
282
|
|
|
289
|
-
//
|
|
283
|
+
// 3. Hunter in Savannah / Wild / Blazing Sun / Spear / Mosquitoes (Scene 5)
|
|
284
|
+
if (/(?:^|[^\w])(savannah|savanna|spear|hunter|hunting|africa savannah|foraging|wild jungle|mosquito|mosquitoes|thorns)(?:[^\w]|$)/i.test(text)
|
|
285
|
+
|| /കാട്ടിൽ നഗ്നരായി|നഗ്നരായിട്ടാണ്|കൊടുംചൂട്|കൊതുകുകടി|മുള്ളുകൾ|കുന്തം|കാട്ടിലെ ലക്ഷക്കണക്കിന്/.test(text)) {
|
|
286
|
+
return 'doodle_hunter_savannah';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 4. Meeting Tribes / Social Language / Status / Community / Strangers (Scene 10)
|
|
290
290
|
if (/(?:^|[^\w])(tribe|tribal|clan|stranger|meeting strangers|social language|status in society|hierarchy|friend or enemy)(?:[^\w]|$)/i.test(text)
|
|
291
291
|
|| /കൂട്ടമായി|സമൂഹം|അപരിചിത|ഗോത്രം|ശത്രു|സുഹൃത്ത്|സാമൂഹിക ഭാഷ|സാമൂഹിക|സ്ഥാനം തിരിച്ചറിഞ്ഞിരുന്നത്/.test(text)) {
|
|
292
292
|
return 'doodle_tribe_meeting';
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
// 5. Future Planning / Cognitive Revolution / Tomorrow / Foresight / Campfire thinking
|
|
295
|
+
// 5. Future Planning / Cognitive Revolution / Tomorrow / Foresight / Campfire thinking (Scene 8)
|
|
296
296
|
if (/(?:^|[^\w])(planning for tomorrow|future planning|cognitive revolution|thinking future|contemplate|foresight|ian gilligan)(?:[^\w]|$)/i.test(text)
|
|
297
297
|
|| /നാളത്തേക്ക്|പ്ലാൻ|ഭാവി|ഭാവിയെ കുറിച്ച്|ബുദ്ധി ഉപയോഗിച്ച്|ചിന്തയിലെ മാറ്റം|ഇയാൻ ഗില്ലിഗൻ|ചിന്തിക്കാൻ തുടങ്ങി/.test(text)) {
|
|
298
298
|
return 'doodle_thinking_future';
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
// 6.
|
|
302
|
-
if (/(?:^|[^\w])(savannah|savanna|spear|hunter|hunting|africa savannah|foraging|wild jungle|naked|clothless|mosquito|mosquitoes|thorns)(?:[^\w]|$)/i.test(text)
|
|
303
|
-
|| /വേട്ട|വേട്ടയാടി|ആഫ്രിക്കൻ കാടുകളിൽ|കാട്ടിൽ നഗ്നരായി|നഗ്നരായിട്ടാണ്|കൊടുംചൂട്|കൊതുകുകടി|മുള്ളുകൾ|കുന്തം|കാട്ടിലെ ലക്ഷക്കണക്കിന്/.test(text)) {
|
|
304
|
-
return 'doodle_hunter_savannah';
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// 7. Bone Needle / Sewing / Stitching / Animal Pelt / Prehistoric Tools
|
|
301
|
+
// 6. Bone Needle / Sewing / Stitching / Animal Pelt / Prehistoric Tools (Scene 4)
|
|
308
302
|
if (/(?:^|[^\w])(needle|bone needle|needles|sew|sewing|stitch|stitching|thread|animal hide|leather hide|hide sewing|fur pelt|tailor|sewing tools)(?:[^\w]|$)/i.test(text)
|
|
309
303
|
|| /സൂചി|സൂചിയും|തുന്ന|തുന്നാൻ|തോൽ തുന്നാൻ|തുന്നൽ|തോൽ തുന്നാനുള്ള/.test(text)) {
|
|
310
304
|
return 'doodle_sewing_needle';
|
|
311
305
|
}
|
|
312
306
|
|
|
313
|
-
//
|
|
307
|
+
// 7. Shame / Mirror / Reflection / Water / Self Awareness / Theory of Mind (Scene 3, 9)
|
|
314
308
|
if (/(?:^|[^\w])(shame|blush|embarrass|mirror|reflection|water pool|theory of mind|how others see|self-aware|identity|looking at me|psychology|mind-shift)(?:[^\w]|$)/i.test(text)
|
|
315
|
-
||
|
|
309
|
+
|| /കണ്ണാടി|പ്രതിഫലനം|മറ്റുള്ളവർ എന്നെ|തിയറി ഓഫ് മൈൻഡ്|സ്വന്തം ശരീരം മറച്ചു|വ്യക്തിത്വം|ശ്രദ്ധിക്കുന്നുണ്ട്|യഥാർത്ഥ രഹസ്യം|നാണം മറയ്ക്കാൻ/.test(text)) {
|
|
316
310
|
return 'doodle_shame_reflection';
|
|
317
311
|
}
|
|
318
312
|
|
|
313
|
+
// 8. Dressing / Wardrobe / Wearing Clothes / Outfit / Morning Routine (Scene 1, 11)
|
|
314
|
+
if (/(?:^|[^\w])(dress|dressing|wearing clothes|shirt|wardrobe|closet|hanger|outfit|choose clothes|wear|garment|wedding dress|interview|daily habit|why we wear clothes)(?:[^\w]|$)/i.test(text)
|
|
315
|
+
|| /വസ്ത്രധാരണം|ധരിച്ചിട്ടുണ്ടാ|ഷർട്ട്|തിരഞ്ഞെടുത്തത്|തുണി എടുത്തിടുകയല്ല|കല്യാണത്തിന്|ഇന്റർവ്യൂ|ആമുഖം: വസ്ത്രധാരണം|വസ്ത്രം തിരഞ്ഞെടുക്കുമ്പോൾ|വസ്ത്രം തിരഞ്ഞെടുക്കും/.test(text)) {
|
|
316
|
+
return 'doodle_dressing';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
319
|
// 9. General Human Character
|
|
320
320
|
if (/(?:^|[^\w])(human|person|man|woman|people|child|guy|doodle|character|actor)(?:[^\w]|$)/i.test(text)
|
|
321
321
|
|| /മനുഷ്യൻ|ആളുകൾ|വ്യക്തി/.test(text)) {
|
|
@@ -740,10 +740,13 @@ function drawBackground(t) {
|
|
|
740
740
|
// ── Full-Canvas Cinematic Visual Directors ─────────────────────────────────────
|
|
741
741
|
function drawMainScene(t) {
|
|
742
742
|
ctx.save();
|
|
743
|
+
const p = Math.min(1, Math.max(0, t / window.DURATION));
|
|
743
744
|
const enterSpring = spring(t, 0.05, 170, 13);
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
745
|
+
const camZoom = 1.0 + Math.sin(t * 0.45) * 0.02 + p * 0.04;
|
|
746
|
+
const camPanX = Math.sin(t * 0.35) * 12;
|
|
747
|
+
const camPanY = Math.cos(t * 0.28) * 6;
|
|
748
|
+
ctx.translate(W / 2 + camPanX, H * 0.50 + camPanY);
|
|
749
|
+
ctx.scale(Math.min(1.04, Math.max(0.88, enterSpring)) * camZoom, Math.min(1.04, Math.max(0.88, enterSpring)) * camZoom);
|
|
747
750
|
ctx.translate(-W / 2, -H * 0.50);
|
|
748
751
|
|
|
749
752
|
const cx = W / 2;
|
|
@@ -1027,12 +1030,15 @@ function drawMainScene(t) {
|
|
|
1027
1030
|
ctx.restore();
|
|
1028
1031
|
}
|
|
1029
1032
|
|
|
1030
|
-
// ── Doodled Character Narrative Themes
|
|
1033
|
+
// ── Doodled Character Narrative Themes (3-Beat Progressive Choreography) ─
|
|
1031
1034
|
if (COMP_THEME === 'doodle_dressing') {
|
|
1032
|
-
//
|
|
1035
|
+
// Beat 1: Browsing wardrobe -> Beat 2: Trying shirt -> Beat 3: Looking in mirror reflection
|
|
1036
|
+
const beat2 = spring(t, window.DURATION * 0.35, 180, 16);
|
|
1037
|
+
const beat3 = spring(t, window.DURATION * 0.70, 190, 16);
|
|
1038
|
+
|
|
1033
1039
|
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.25)', 2.5);
|
|
1034
1040
|
|
|
1035
|
-
// Clothes rack on left
|
|
1041
|
+
// Clothes rack on left (sways in beat 1)
|
|
1036
1042
|
sLine(cx - 360, cy + 220, cx - 360, cy - 140, 11, '#94a3b8', 5);
|
|
1037
1043
|
sLine(cx - 410, cy + 220, cx - 310, cy + 220, 12, '#94a3b8', 5);
|
|
1038
1044
|
sLine(cx - 420, cy - 140, cx - 180, cy - 140, 13, '#cbd5e1', 5);
|
|
@@ -1053,96 +1059,132 @@ function drawMainScene(t) {
|
|
|
1053
1059
|
ctx.fill();
|
|
1054
1060
|
}
|
|
1055
1061
|
|
|
1056
|
-
// Mirror on right
|
|
1062
|
+
// Mirror on right - emerges and glows in Beat 2 & 3
|
|
1057
1063
|
const mirrorX = cx + 320;
|
|
1058
1064
|
const mirrorY = cy + 20;
|
|
1059
|
-
ctx.fillStyle = 'rgba(56, 189, 248, 0.08)';
|
|
1065
|
+
ctx.fillStyle = 'rgba(56, 189, 248, ' + (0.05 + beat2 * 0.08) + ')';
|
|
1060
1066
|
ctx.beginPath();
|
|
1061
1067
|
ctx.roundRect(mirrorX - 85, mirrorY - 210, 170, 390, 24);
|
|
1062
1068
|
ctx.fill();
|
|
1063
|
-
ctx.strokeStyle = '#
|
|
1069
|
+
ctx.strokeStyle = beat3 > 0.5 ? '#f59e0b' : '#94a3b8';
|
|
1064
1070
|
ctx.lineWidth = 4;
|
|
1065
1071
|
ctx.stroke();
|
|
1066
1072
|
|
|
1067
|
-
// Reflected character in mirror (
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1073
|
+
// Reflected character in mirror (smiling and thumbs up in beat 3)
|
|
1074
|
+
if (beat2 > 0.1) {
|
|
1075
|
+
ctx.save();
|
|
1076
|
+
ctx.globalAlpha = Math.min(0.65, beat2 * 0.65);
|
|
1077
|
+
drawDoodleCharacter({
|
|
1078
|
+
x: mirrorX,
|
|
1079
|
+
y: cy + 120,
|
|
1080
|
+
scale: 1.05,
|
|
1081
|
+
hairStyle: 'modern',
|
|
1082
|
+
expression: beat3 > 0.5 ? 'smiling' : 'curious',
|
|
1083
|
+
clothing: 'shirt',
|
|
1084
|
+
armPose: beat3 > 0.5 ? 'peace_gesture' : 'dress_shirt',
|
|
1085
|
+
seed: 88,
|
|
1086
|
+
});
|
|
1087
|
+
ctx.restore();
|
|
1088
|
+
}
|
|
1081
1089
|
|
|
1082
|
-
// Main character
|
|
1090
|
+
// Main character moves progressively from rack -> center -> mirror
|
|
1091
|
+
const charX = cx - 220 + beat2 * 170 + beat3 * 60;
|
|
1083
1092
|
drawDoodleCharacter({
|
|
1084
|
-
x:
|
|
1093
|
+
x: charX,
|
|
1085
1094
|
y: cy + 140,
|
|
1086
1095
|
scale: 1.35,
|
|
1087
1096
|
hairStyle: 'modern',
|
|
1088
|
-
expression: 'talking',
|
|
1097
|
+
expression: beat3 > 0.5 ? 'smiling' : 'talking',
|
|
1089
1098
|
clothing: 'shirt',
|
|
1090
|
-
armPose: 'dress_shirt',
|
|
1099
|
+
armPose: beat2 > 0.5 ? 'dress_shirt' : 'natural',
|
|
1091
1100
|
seed: 101,
|
|
1092
1101
|
});
|
|
1093
1102
|
|
|
1094
1103
|
} else if (COMP_THEME === 'doodle_shiver_cold') {
|
|
1095
|
-
//
|
|
1104
|
+
// Beat 1: Shivering in cold -> Beat 2: NOT FOR COLD stamp -> Beat 3: NOT FOR PROTECTION stamp
|
|
1105
|
+
const stamp1 = spring(t, window.DURATION * 0.38, 220, 15);
|
|
1106
|
+
const stamp2 = spring(t, window.DURATION * 0.72, 220, 15);
|
|
1107
|
+
|
|
1096
1108
|
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1097
1109
|
|
|
1098
1110
|
// Freezing wind streaks rushing across
|
|
1099
|
-
for (let w = 0; w <
|
|
1100
|
-
const windX = ((t *
|
|
1101
|
-
const windY = cy - 250 + (w *
|
|
1102
|
-
sLine(windX, windY, windX +
|
|
1111
|
+
for (let w = 0; w < 14; w++) {
|
|
1112
|
+
const windX = ((t * 750 + w * 210) % (W + 600)) - 300;
|
|
1113
|
+
const windY = cy - 250 + (w * 42) + Math.sin(t * 3.5 + w) * 15;
|
|
1114
|
+
sLine(windX, windY, windX + 170, windY + 5, 200 + w, 'rgba(255,255,255,0.3)', 2);
|
|
1103
1115
|
}
|
|
1104
1116
|
|
|
1105
1117
|
// Drifting snow particles
|
|
1106
|
-
for (let
|
|
1107
|
-
const px = ((
|
|
1108
|
-
const py = (
|
|
1109
|
-
sCircle(px, py, 2.5, 300 +
|
|
1118
|
+
for (let sp = 0; sp < 25; sp++) {
|
|
1119
|
+
const px = ((sp * 85 + t * 450) % W);
|
|
1120
|
+
const py = (sp * 45 + t * 90) % H;
|
|
1121
|
+
sCircle(px, py, 2.5, 300 + sp, '#e0f2fe', 1, '#ffffff');
|
|
1110
1122
|
}
|
|
1111
1123
|
|
|
1112
|
-
// Shivering breath vapor cloud
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1124
|
+
// Shivering breath vapor cloud (calms in Beat 3)
|
|
1125
|
+
if (stamp2 < 0.8) {
|
|
1126
|
+
const breathPhase = (t * 2.2) % 1;
|
|
1127
|
+
const bCloudX = cx - 50 + breathPhase * 60;
|
|
1128
|
+
const bCloudY = cy - 90 - breathPhase * 25;
|
|
1129
|
+
ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.5) + ')';
|
|
1130
|
+
ctx.beginPath();
|
|
1131
|
+
ctx.arc(bCloudX, bCloudY, 15 + breathPhase * 25, 0, Math.PI * 2);
|
|
1132
|
+
ctx.fill();
|
|
1133
|
+
}
|
|
1120
1134
|
|
|
1121
|
-
// Main
|
|
1122
|
-
const
|
|
1123
|
-
const
|
|
1135
|
+
// Main character: shivers in Beat 1 & 2, then realizes / curious in Beat 3
|
|
1136
|
+
const isShivering = stamp2 < 0.7;
|
|
1137
|
+
const vibX = isShivering ? Math.sin(t * 38) * 4.5 : 0;
|
|
1138
|
+
const vibY = isShivering ? Math.cos(t * 32) * 2 : 0;
|
|
1124
1139
|
drawDoodleCharacter({
|
|
1125
|
-
x: cx + vibX,
|
|
1140
|
+
x: cx - 120 + vibX,
|
|
1126
1141
|
y: cy + 140 + vibY,
|
|
1127
1142
|
scale: 1.35,
|
|
1128
1143
|
hairStyle: 'wild',
|
|
1129
|
-
expression: 'shivering',
|
|
1144
|
+
expression: isShivering ? 'shivering' : 'curious',
|
|
1130
1145
|
clothing: 'none',
|
|
1131
|
-
armPose: 'shiver',
|
|
1146
|
+
armPose: isShivering ? 'shiver' : 'thinker',
|
|
1132
1147
|
seed: 102,
|
|
1133
1148
|
});
|
|
1134
1149
|
|
|
1135
|
-
//
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1150
|
+
// Stamp 1: NOT FOR COLD (slams down in Beat 2)
|
|
1151
|
+
if (stamp1 > 0.05) {
|
|
1152
|
+
ctx.save();
|
|
1153
|
+
const s1X = cx + 240;
|
|
1154
|
+
const s1Y = cy - 100;
|
|
1155
|
+
ctx.translate(s1X, s1Y);
|
|
1156
|
+
ctx.scale(stamp1, stamp1);
|
|
1157
|
+
sCircle(0, 0, 50, 400, 'rgba(239,68,68,0.9)', 4);
|
|
1158
|
+
sLine(-36, -36, 36, 36, 401, 'rgba(239,68,68,0.95)', 4.5);
|
|
1159
|
+
ctx.font = 'bold 20px monospace';
|
|
1160
|
+
ctx.fillStyle = '#ef4444';
|
|
1161
|
+
ctx.textAlign = 'center';
|
|
1162
|
+
ctx.fillText('NOT FOR COLD', 0, 75);
|
|
1163
|
+
ctx.restore();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Stamp 2: NOT FOR PROTECTION (slams down in Beat 3)
|
|
1167
|
+
if (stamp2 > 0.05) {
|
|
1168
|
+
ctx.save();
|
|
1169
|
+
const s2X = cx + 240;
|
|
1170
|
+
const s2Y = cy + 90;
|
|
1171
|
+
ctx.translate(s2X, s2Y);
|
|
1172
|
+
ctx.scale(stamp2, stamp2);
|
|
1173
|
+
sCircle(0, 0, 50, 410, 'rgba(239,68,68,0.9)', 4);
|
|
1174
|
+
sLine(-36, -36, 36, 36, 411, 'rgba(239,68,68,0.95)', 4.5);
|
|
1175
|
+
ctx.font = 'bold 20px monospace';
|
|
1176
|
+
ctx.fillStyle = '#ef4444';
|
|
1177
|
+
ctx.textAlign = 'center';
|
|
1178
|
+
ctx.fillText('NOT FOR PROTECTION', 0, 75);
|
|
1179
|
+
ctx.restore();
|
|
1180
|
+
}
|
|
1142
1181
|
|
|
1143
1182
|
} else if (COMP_THEME === 'doodle_hunter_savannah') {
|
|
1144
|
-
//
|
|
1145
|
-
|
|
1183
|
+
// Beat 1: Walking with spear -> Beat 2: Swatting mosquitoes -> Beat 3: Pausing alert
|
|
1184
|
+
const walkProg = Math.min(1, Math.max(0, t / window.DURATION));
|
|
1185
|
+
const swatting = (t % 2.4 > 1.2);
|
|
1186
|
+
|
|
1187
|
+
// Blazing midday sun with undulating rays
|
|
1146
1188
|
const sunR = 75 + Math.sin(t * 2) * 6;
|
|
1147
1189
|
ctx.fillStyle = '#f59e0b';
|
|
1148
1190
|
ctx.beginPath();
|
|
@@ -1165,27 +1207,31 @@ function drawMainScene(t) {
|
|
|
1165
1207
|
sLine(gx, cy + 220, gx + sway, cy + 140, 500 + g, '#d97706', 2.5);
|
|
1166
1208
|
}
|
|
1167
1209
|
|
|
1168
|
-
// Buzzing mosquitoes orbiting
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1210
|
+
// Buzzing mosquitoes orbiting hunter
|
|
1211
|
+
const charX = cx - 180 + walkProg * 240;
|
|
1212
|
+
for (let m = 0; m < 4; m++) {
|
|
1213
|
+
const mx = charX + 50 + Math.sin(t * 8 + m * 2) * 65;
|
|
1214
|
+
const my = cy - 60 + Math.cos(t * 7 + m * 2) * 45;
|
|
1215
|
+
sCircle(mx, my, 2.5, 550 + m, '#0f172a', 1.5, '#ef4444');
|
|
1173
1216
|
}
|
|
1174
1217
|
|
|
1175
|
-
// Walking early hunter
|
|
1218
|
+
// Walking early hunter with spear
|
|
1176
1219
|
drawDoodleCharacter({
|
|
1177
|
-
x:
|
|
1220
|
+
x: charX,
|
|
1178
1221
|
y: cy + 140,
|
|
1179
1222
|
scale: 1.35,
|
|
1180
1223
|
hairStyle: 'wild',
|
|
1181
|
-
expression: 'talking',
|
|
1224
|
+
expression: swatting ? 'shivering' : 'talking',
|
|
1182
1225
|
clothing: 'none',
|
|
1183
1226
|
armPose: 'hold_spear',
|
|
1184
1227
|
seed: 103,
|
|
1185
1228
|
});
|
|
1186
1229
|
|
|
1187
1230
|
} else if (COMP_THEME === 'doodle_sewing_needle') {
|
|
1188
|
-
//
|
|
1231
|
+
// Beat 1: Seated by campfire -> Beat 2: Zoom loupe with bone needle -> Beat 3: Timeline badges
|
|
1232
|
+
const loupeSpring = spring(t, window.DURATION * 0.32, 180, 15);
|
|
1233
|
+
const badgeSpring = spring(t, window.DURATION * 0.68, 200, 15);
|
|
1234
|
+
|
|
1189
1235
|
// Campfire with animated flames and sparks
|
|
1190
1236
|
const fireX = cx - 380;
|
|
1191
1237
|
const fireY = cy + 180;
|
|
@@ -1197,16 +1243,15 @@ function drawMainScene(t) {
|
|
|
1197
1243
|
ctx.quadraticCurveTo(fireX + (f - 2) * 8, fireY - flH, fireX - 10 + f * 12, fireY);
|
|
1198
1244
|
ctx.fill();
|
|
1199
1245
|
}
|
|
1200
|
-
// Floating fire sparks
|
|
1201
1246
|
for (let sp = 0; sp < 8; sp++) {
|
|
1202
1247
|
const sx = fireX + Math.sin(t * 4 + sp) * 35;
|
|
1203
1248
|
const sy = fireY - 30 - ((t * 80 + sp * 35) % 140);
|
|
1204
1249
|
sCircle(sx, sy, 2, 600 + sp, '#fbbf24', 1, '#fbbf24');
|
|
1205
1250
|
}
|
|
1206
1251
|
|
|
1207
|
-
//
|
|
1252
|
+
// Seated human stitching hide with bone needle
|
|
1208
1253
|
drawDoodleCharacter({
|
|
1209
|
-
x: cx -
|
|
1254
|
+
x: cx - 120,
|
|
1210
1255
|
y: cy + 140,
|
|
1211
1256
|
scale: 1.35,
|
|
1212
1257
|
hairStyle: 'wild',
|
|
@@ -1216,39 +1261,57 @@ function drawMainScene(t) {
|
|
|
1216
1261
|
seed: 104,
|
|
1217
1262
|
});
|
|
1218
1263
|
|
|
1219
|
-
// Zoomed
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
ctx.fill();
|
|
1227
|
-
ctx.strokeStyle = '#d4af37';
|
|
1228
|
-
ctx.lineWidth = 5;
|
|
1229
|
-
ctx.stroke();
|
|
1264
|
+
// Zoomed loupe expands open in Beat 2
|
|
1265
|
+
if (loupeSpring > 0.05) {
|
|
1266
|
+
ctx.save();
|
|
1267
|
+
const lensX = cx + 270;
|
|
1268
|
+
const lensY = cy - 20;
|
|
1269
|
+
ctx.translate(lensX, lensY);
|
|
1270
|
+
ctx.scale(loupeSpring, loupeSpring);
|
|
1230
1271
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1272
|
+
const lensR = 140;
|
|
1273
|
+
ctx.fillStyle = 'rgba(15, 23, 42, 0.92)';
|
|
1274
|
+
ctx.beginPath();
|
|
1275
|
+
ctx.arc(0, 0, lensR, 0, Math.PI * 2);
|
|
1276
|
+
ctx.fill();
|
|
1277
|
+
ctx.strokeStyle = '#d4af37';
|
|
1278
|
+
ctx.lineWidth = 5;
|
|
1279
|
+
ctx.stroke();
|
|
1234
1280
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
sCircle(lensX - 40 + nWiggle, lensY - 58, 4, 653, '#0f172a', 1.5, '#0f172a'); // Eyelet
|
|
1281
|
+
// Two thick leather pelt pieces being stitched
|
|
1282
|
+
sLine(-85, -20, 85, -20, 650, '#92400e', 8);
|
|
1283
|
+
sLine(-85, 20, 85, 20, 651, '#78350f', 8);
|
|
1239
1284
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1285
|
+
// Drilled bone needle piercing through with animated wiggle
|
|
1286
|
+
const nWiggle = Math.sin(t * 4.5) * 18;
|
|
1287
|
+
sLine(-45 + nWiggle, -65, 25 + nWiggle, 55, 652, '#fef08a', 6);
|
|
1288
|
+
sCircle(-40 + nWiggle, -58, 4, 653, '#0f172a', 1.5, '#0f172a');
|
|
1289
|
+
|
|
1290
|
+
// Stitches across pelts
|
|
1291
|
+
for (let st = 0; st < 6; st++) {
|
|
1292
|
+
const stX = -70 + st * 28;
|
|
1293
|
+
sLine(stX, -22, stX + 8, 22, 660 + st, '#ffffff', 3.5);
|
|
1294
|
+
}
|
|
1295
|
+
ctx.restore();
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Timeline badge in Beat 3
|
|
1299
|
+
if (badgeSpring > 0.05) {
|
|
1300
|
+
ctx.save();
|
|
1301
|
+
ctx.globalAlpha = Math.min(1, badgeSpring);
|
|
1302
|
+
sRect(cx + 120, cy + 140, 300, 42, 670, '#d4af37', 2.5, 'rgba(15, 23, 42, 0.85)');
|
|
1303
|
+
ctx.font = 'bold 16px monospace';
|
|
1304
|
+
ctx.fillStyle = '#fef08a';
|
|
1305
|
+
ctx.textAlign = 'center';
|
|
1306
|
+
ctx.fillText('BONE NEEDLE • 120,000 YEARS', cx + 270, cy + 167);
|
|
1307
|
+
ctx.restore();
|
|
1244
1308
|
}
|
|
1245
|
-
ctx.font = 'bold 18px monospace';
|
|
1246
|
-
ctx.fillStyle = '#d4af37';
|
|
1247
|
-
ctx.textAlign = 'center';
|
|
1248
|
-
ctx.fillText('BONE NEEDLE • 120,000 YEARS', lensX, lensY + lensR + 32);
|
|
1249
1309
|
|
|
1250
1310
|
} else if (COMP_THEME === 'doodle_shame_reflection') {
|
|
1251
|
-
// Water
|
|
1311
|
+
// Beat 1: Looking down -> Beat 2: Water reflection & blushing -> Beat 3: Theory of Mind halo
|
|
1312
|
+
const refSpring = spring(t, window.DURATION * 0.30, 180, 15);
|
|
1313
|
+
const haloSpring = spring(t, window.DURATION * 0.65, 190, 15);
|
|
1314
|
+
|
|
1252
1315
|
const poolX = cx;
|
|
1253
1316
|
const poolY = cy + 180;
|
|
1254
1317
|
ctx.fillStyle = 'rgba(14, 165, 233, 0.15)';
|
|
@@ -1269,22 +1332,24 @@ function drawMainScene(t) {
|
|
|
1269
1332
|
ctx.stroke();
|
|
1270
1333
|
}
|
|
1271
1334
|
|
|
1272
|
-
// Inverted rippling reflection of character in water
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1335
|
+
// Inverted rippling reflection of character in water (clarifies in Beat 2)
|
|
1336
|
+
if (refSpring > 0.1) {
|
|
1337
|
+
ctx.save();
|
|
1338
|
+
ctx.globalAlpha = Math.min(0.45, refSpring * 0.45);
|
|
1339
|
+
ctx.translate(cx, poolY + 20);
|
|
1340
|
+
ctx.scale(1.2, -0.75);
|
|
1341
|
+
drawDoodleCharacter({
|
|
1342
|
+
x: 0,
|
|
1343
|
+
y: 0,
|
|
1344
|
+
scale: 1.0,
|
|
1345
|
+
hairStyle: 'wild',
|
|
1346
|
+
expression: 'surprised',
|
|
1347
|
+
clothing: 'none',
|
|
1348
|
+
armPose: 'cover_body',
|
|
1349
|
+
seed: 700,
|
|
1350
|
+
});
|
|
1351
|
+
ctx.restore();
|
|
1352
|
+
}
|
|
1288
1353
|
|
|
1289
1354
|
// Standing character looking down and covering body
|
|
1290
1355
|
drawDoodleCharacter({
|
|
@@ -1292,77 +1357,98 @@ function drawMainScene(t) {
|
|
|
1292
1357
|
y: cy + 120,
|
|
1293
1358
|
scale: 1.35,
|
|
1294
1359
|
hairStyle: 'wild',
|
|
1295
|
-
expression: 'surprised',
|
|
1360
|
+
expression: refSpring > 0.4 ? 'surprised' : 'talking',
|
|
1296
1361
|
clothing: 'none',
|
|
1297
|
-
armPose: 'cover_body',
|
|
1362
|
+
armPose: refSpring > 0.4 ? 'cover_body' : 'natural',
|
|
1298
1363
|
seed: 105,
|
|
1299
1364
|
});
|
|
1300
1365
|
|
|
1301
|
-
// Blushing cheeks
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1366
|
+
// Blushing cheeks appear in Beat 2
|
|
1367
|
+
if (refSpring > 0.3) {
|
|
1368
|
+
const blushA = Math.min(0.7, refSpring * 0.7);
|
|
1369
|
+
sCircle(cx - 18, cy - 8, 7, 750, 'rgba(244,63,94,' + blushA + ')', 1, 'rgba(244,63,94,' + blushA + ')');
|
|
1370
|
+
sCircle(cx + 18, cy - 8, 7, 751, 'rgba(244,63,94,' + blushA + ')', 1, 'rgba(244,63,94,' + blushA + ')');
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Theory of Mind Thought Halo in Beat 3
|
|
1374
|
+
if (haloSpring > 0.05) {
|
|
1375
|
+
ctx.save();
|
|
1376
|
+
const haloX = cx + 180;
|
|
1377
|
+
const haloY = cy - 120;
|
|
1378
|
+
ctx.translate(haloX, haloY);
|
|
1379
|
+
ctx.scale(haloSpring, haloSpring);
|
|
1380
|
+
sCircle(0, 0, 48, 760, '#38bdf8', 3);
|
|
1381
|
+
sCircle(0, 0, 18, 761, '#ffffff', 2.5);
|
|
1382
|
+
sCircle(0, 0, 8, 762, '#38bdf8', 1, '#38bdf8');
|
|
1383
|
+
ctx.font = 'bold 16px monospace';
|
|
1384
|
+
ctx.fillStyle = '#38bdf8';
|
|
1385
|
+
ctx.textAlign = 'center';
|
|
1386
|
+
ctx.fillText('THEORY OF MIND', 0, 72);
|
|
1387
|
+
ctx.restore();
|
|
1388
|
+
}
|
|
1316
1389
|
|
|
1317
1390
|
} else if (COMP_THEME === 'doodle_tribe_meeting') {
|
|
1318
|
-
//
|
|
1391
|
+
// Beat 1: Walking toward center -> Beat 2: Greet & check patterns -> Beat 3: Social language arc
|
|
1392
|
+
const approach = spring(t, 0.05, 140, 16);
|
|
1393
|
+
const greet = spring(t, window.DURATION * 0.38, 190, 15);
|
|
1394
|
+
const arcSpring = spring(t, window.DURATION * 0.68, 200, 15);
|
|
1395
|
+
|
|
1319
1396
|
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1320
1397
|
|
|
1321
|
-
// Left Clan Member
|
|
1398
|
+
// Left Clan Member walks toward center
|
|
1399
|
+
const leftX = cx - 380 + Math.min(1, approach) * 160;
|
|
1322
1400
|
drawDoodleCharacter({
|
|
1323
|
-
x:
|
|
1401
|
+
x: leftX,
|
|
1324
1402
|
y: cy + 140,
|
|
1325
1403
|
scale: 1.3,
|
|
1326
1404
|
hairStyle: 'wild',
|
|
1327
1405
|
expression: 'talking',
|
|
1328
1406
|
clothing: 'pelt',
|
|
1329
|
-
armPose: 'peace_gesture',
|
|
1407
|
+
armPose: greet > 0.5 ? 'peace_gesture' : 'natural',
|
|
1330
1408
|
seed: 106,
|
|
1331
1409
|
});
|
|
1332
1410
|
|
|
1333
|
-
// Right Clan Member
|
|
1411
|
+
// Right Clan Member walks toward center
|
|
1412
|
+
const rightX = cx + 380 - Math.min(1, approach) * 160;
|
|
1334
1413
|
drawDoodleCharacter({
|
|
1335
|
-
x:
|
|
1414
|
+
x: rightX,
|
|
1336
1415
|
y: cy + 140,
|
|
1337
1416
|
scale: 1.3,
|
|
1338
1417
|
hairStyle: 'feather',
|
|
1339
1418
|
expression: 'curious',
|
|
1340
1419
|
clothing: 'cloak',
|
|
1341
|
-
armPose: 'natural',
|
|
1420
|
+
armPose: greet > 0.5 ? 'peace_gesture' : 'natural',
|
|
1342
1421
|
seed: 107,
|
|
1343
1422
|
});
|
|
1344
1423
|
|
|
1345
|
-
// Glowing connection arc and
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1424
|
+
// Glowing connection arc and symbols in Beat 3
|
|
1425
|
+
if (arcSpring > 0.05) {
|
|
1426
|
+
ctx.save();
|
|
1427
|
+
ctx.globalAlpha = Math.min(1, arcSpring);
|
|
1428
|
+
ctx.strokeStyle = ACCENT;
|
|
1429
|
+
ctx.lineWidth = 3;
|
|
1430
|
+
ctx.setLineDash([8, 8]);
|
|
1431
|
+
ctx.beginPath();
|
|
1432
|
+
ctx.moveTo(leftX + 40, cy);
|
|
1433
|
+
ctx.quadraticCurveTo(cx, cy - 100, rightX - 40, cy);
|
|
1434
|
+
ctx.stroke();
|
|
1435
|
+
ctx.setLineDash([]);
|
|
1354
1436
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1437
|
+
sCircle(cx, cy - 65, 34, 800, ACCENT, 3, 'rgba(15, 23, 42, 0.85)');
|
|
1438
|
+
ctx.font = 'bold 22px monospace';
|
|
1439
|
+
ctx.fillStyle = ACCENT;
|
|
1440
|
+
ctx.textAlign = 'center';
|
|
1441
|
+
ctx.fillText('🤝', cx, cy - 56);
|
|
1442
|
+
ctx.font = 'bold 16px monospace';
|
|
1443
|
+
ctx.fillText('CLOTHING AS SOCIAL LANGUAGE', cx, cy + 15);
|
|
1444
|
+
ctx.restore();
|
|
1445
|
+
}
|
|
1363
1446
|
|
|
1364
1447
|
} else if (COMP_THEME === 'doodle_thinking_future') {
|
|
1365
|
-
//
|
|
1448
|
+
// Beat 1: Seated thinker by fire -> Beat 2: Thought cloud floats up -> Beat 3: Tomorrow's rack & tools
|
|
1449
|
+
const cloudSpring = spring(t, window.DURATION * 0.32, 170, 14);
|
|
1450
|
+
const ideaSpring = spring(t, window.DURATION * 0.65, 190, 15);
|
|
1451
|
+
|
|
1366
1452
|
// Starlit night and crescent moon
|
|
1367
1453
|
ctx.fillStyle = '#fef08a';
|
|
1368
1454
|
ctx.beginPath();
|
|
@@ -1391,102 +1477,120 @@ function drawMainScene(t) {
|
|
|
1391
1477
|
seed: 108,
|
|
1392
1478
|
});
|
|
1393
1479
|
|
|
1394
|
-
// Expanding thought cloud overhead
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
ctx.scale(cloudScale, cloudScale);
|
|
1480
|
+
// Expanding thought cloud overhead in Beat 2
|
|
1481
|
+
if (cloudSpring > 0.05) {
|
|
1482
|
+
const cloudX = cx + 180;
|
|
1483
|
+
const cloudY = cy - 80;
|
|
1484
|
+
ctx.save();
|
|
1485
|
+
ctx.translate(cloudX, cloudY);
|
|
1486
|
+
ctx.scale(cloudSpring, cloudSpring);
|
|
1402
1487
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
sCircle(-90, 70, 24, 852, '#ffffff', 2, '#ffffff');
|
|
1488
|
+
sCircle(-180, 160, 12, 850, '#ffffff', 2, '#ffffff');
|
|
1489
|
+
sCircle(-140, 120, 18, 851, '#ffffff', 2, '#ffffff');
|
|
1490
|
+
sCircle(-90, 70, 24, 852, '#ffffff', 2, '#ffffff');
|
|
1407
1491
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
ctx.fill();
|
|
1492
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
|
1493
|
+
ctx.beginPath();
|
|
1494
|
+
ctx.arc(-70, 0, 55, 0, Math.PI * 2);
|
|
1495
|
+
ctx.arc(0, -25, 65, 0, Math.PI * 2);
|
|
1496
|
+
ctx.arc(70, 0, 55, 0, Math.PI * 2);
|
|
1497
|
+
ctx.arc(0, 30, 60, 0, Math.PI * 2);
|
|
1498
|
+
ctx.fill();
|
|
1416
1499
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1500
|
+
// Inside thought cloud: Tomorrow's sunrise & stretched hide on rack in Beat 3
|
|
1501
|
+
if (ideaSpring > 0.1) {
|
|
1502
|
+
ctx.fillStyle = '#f59e0b';
|
|
1503
|
+
ctx.beginPath();
|
|
1504
|
+
ctx.arc(0, 15, 30, Math.PI, Math.PI * 2);
|
|
1505
|
+
ctx.fill();
|
|
1422
1506
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
sLine(-50, -30, 50, -30, 862, '#78350f', 4);
|
|
1427
|
-
sRect(-35, -28, 70, 40, 863, '#92400e', 3, '#b45309');
|
|
1507
|
+
sLine(-35, 15, -35, -25, 860, '#78350f', 3);
|
|
1508
|
+
sLine(35, 15, 35, -25, 861, '#78350f', 3);
|
|
1509
|
+
sLine(-40, -25, 40, -25, 862, '#78350f', 3);
|
|
1428
1510
|
|
|
1429
|
-
|
|
1511
|
+
ctx.fillStyle = '#b45309';
|
|
1512
|
+
ctx.fillRect(-28, -20, 56, 32);
|
|
1430
1513
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1514
|
+
ctx.font = 'bold 15px monospace';
|
|
1515
|
+
ctx.fillStyle = '#d97706';
|
|
1516
|
+
ctx.textAlign = 'center';
|
|
1517
|
+
ctx.fillText('💡 PLANNING FOR TOMORROW', 0, 52);
|
|
1518
|
+
}
|
|
1519
|
+
ctx.restore();
|
|
1520
|
+
}
|
|
1435
1521
|
|
|
1436
1522
|
} else if (COMP_THEME === 'doodle_microscope_lice') {
|
|
1437
|
-
//
|
|
1438
|
-
|
|
1523
|
+
// Beat 1: Scratching pelt -> Beat 2: Giant microscope slides in -> Beat 3: Divergence tree
|
|
1524
|
+
const scopeSpring = spring(t, window.DURATION * 0.30, 180, 15);
|
|
1525
|
+
const treeSpring = spring(t, window.DURATION * 0.65, 200, 15);
|
|
1526
|
+
|
|
1527
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1528
|
+
|
|
1529
|
+
// Early human scratching pelt with itch lines
|
|
1439
1530
|
drawDoodleCharacter({
|
|
1440
|
-
x: cx -
|
|
1531
|
+
x: cx - 240,
|
|
1441
1532
|
y: cy + 140,
|
|
1442
1533
|
scale: 1.35,
|
|
1443
1534
|
hairStyle: 'wild',
|
|
1444
|
-
expression: '
|
|
1535
|
+
expression: 'shivering',
|
|
1445
1536
|
clothing: 'pelt',
|
|
1446
1537
|
armPose: 'shiver',
|
|
1447
1538
|
seed: 109,
|
|
1448
1539
|
});
|
|
1449
1540
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
ctx.fillStyle = 'rgba(15, 23, 42, 0.94)';
|
|
1455
|
-
ctx.beginPath();
|
|
1456
|
-
ctx.arc(lensX, lensY, lensR, 0, Math.PI * 2);
|
|
1457
|
-
ctx.fill();
|
|
1458
|
-
ctx.strokeStyle = '#d4af37';
|
|
1459
|
-
ctx.lineWidth = 6;
|
|
1460
|
-
ctx.stroke();
|
|
1461
|
-
|
|
1462
|
-
// Fabric thread weave lines in lens background
|
|
1463
|
-
for (let f = -4; f <= 4; f++) {
|
|
1464
|
-
sLine(lensX - 140, lensY + f * 28, lensX + 140, lensY + f * 28, 900 + f, 'rgba(255,255,255,0.12)', 2);
|
|
1465
|
-
sLine(lensX + f * 28, lensY - 140, lensX + f * 28, lensY + 140, 920 + f, 'rgba(255,255,255,0.12)', 2);
|
|
1541
|
+
for (let it = 0; it < 3; it++) {
|
|
1542
|
+
const itX = cx - 290 - it * 12;
|
|
1543
|
+
const itY = cy + 50 + it * 20;
|
|
1544
|
+
sLine(itX, itY, itX + 18, itY - 14, 900 + it, '#f59e0b', 2.5);
|
|
1466
1545
|
}
|
|
1467
1546
|
|
|
1468
|
-
//
|
|
1469
|
-
|
|
1470
|
-
|
|
1547
|
+
// Microscope slides in in Beat 2
|
|
1548
|
+
if (scopeSpring > 0.05) {
|
|
1549
|
+
const lensX = cx + 180;
|
|
1550
|
+
const lensY = cy;
|
|
1551
|
+
const lensR = 150;
|
|
1471
1552
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
ctx.fillStyle = '#38bdf8';
|
|
1476
|
-
ctx.textAlign = 'left';
|
|
1477
|
-
ctx.fillText('HEAD LICE (HAIR)', lensX + 90, lensY - 56);
|
|
1553
|
+
ctx.save();
|
|
1554
|
+
ctx.translate(lensX, lensY);
|
|
1555
|
+
ctx.scale(scopeSpring, scopeSpring);
|
|
1478
1556
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1557
|
+
ctx.fillStyle = 'rgba(15, 23, 42, 0.95)';
|
|
1558
|
+
ctx.beginPath();
|
|
1559
|
+
ctx.arc(0, 0, lensR, 0, Math.PI * 2);
|
|
1560
|
+
ctx.fill();
|
|
1561
|
+
ctx.strokeStyle = '#d4af37';
|
|
1562
|
+
ctx.lineWidth = 6;
|
|
1563
|
+
ctx.stroke();
|
|
1483
1564
|
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1565
|
+
// Fabric weave grid lines inside lens
|
|
1566
|
+
for (let g = -4; g <= 4; g++) {
|
|
1567
|
+
sLine(g * 24, -lensR * 0.8, g * 24, lensR * 0.8, 920 + g, 'rgba(56, 189, 248, 0.15)', 2);
|
|
1568
|
+
sLine(-lensR * 0.8, g * 24, lensR * 0.8, g * 24, 930 + g, 'rgba(56, 189, 248, 0.15)', 2);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Divergence tree in Beat 3
|
|
1572
|
+
if (treeSpring > 0.1) {
|
|
1573
|
+
sLine(-100, 0, -20, 0, 940, '#f8fafc', 4);
|
|
1574
|
+
sCircle(-20, 0, 7, 941, '#d4af37', 2, '#d4af37');
|
|
1575
|
+
|
|
1576
|
+
sLine(-20, 0, 60, -45, 942, '#38bdf8', 4);
|
|
1577
|
+
ctx.font = 'bold 14px monospace';
|
|
1578
|
+
ctx.fillStyle = '#38bdf8';
|
|
1579
|
+
ctx.textAlign = 'left';
|
|
1580
|
+
ctx.fillText('HEAD LICE (HAIR)', 70, -42);
|
|
1581
|
+
|
|
1582
|
+
sLine(-20, 0, 60, 45, 943, '#f43f5e', 4);
|
|
1583
|
+
ctx.fillStyle = '#f43f5e';
|
|
1584
|
+
ctx.fillText('BODY LICE (CLOTHES)', 70, 48);
|
|
1585
|
+
|
|
1586
|
+
sRect(-90, 80, 180, 32, 950, '#d4af37', 2, 'rgba(212, 175, 55, 0.25)');
|
|
1587
|
+
ctx.font = 'bold 14px monospace';
|
|
1588
|
+
ctx.fillStyle = '#fef08a';
|
|
1589
|
+
ctx.textAlign = 'center';
|
|
1590
|
+
ctx.fillText('170,000 YEARS AGO', 0, 101);
|
|
1591
|
+
}
|
|
1592
|
+
ctx.restore();
|
|
1593
|
+
}
|
|
1490
1594
|
|
|
1491
1595
|
} else if (COMP_THEME === 'doodle_general_human') {
|
|
1492
1596
|
// General Expressive Doodled Human Narrator
|
|
@@ -1976,49 +2080,70 @@ function drawMainScene(t) {
|
|
|
1976
2080
|
ctx.restore();
|
|
1977
2081
|
}
|
|
1978
2082
|
|
|
1979
|
-
// ──
|
|
2083
|
+
// ── Sentence-Synchronized Broadcast Subtitles Overlay ───────────────────────
|
|
1980
2084
|
function drawCinematicOverlay(t) {
|
|
1981
2085
|
ctx.save();
|
|
1982
|
-
const textSpring = spring(t, 0.12, 190, 15);
|
|
1983
|
-
const headlineAlpha = Math.min(1, Math.max(0, textSpring));
|
|
1984
|
-
ctx.globalAlpha = headlineAlpha;
|
|
1985
2086
|
|
|
1986
|
-
//
|
|
1987
|
-
|
|
2087
|
+
// Subtitles / Spoken Narration - dynamic sentence progression across time
|
|
2088
|
+
if (NARRATION && NARRATION.trim().length > 0) {
|
|
2089
|
+
// Split narration into natural spoken sentence chunks
|
|
2090
|
+
const sentences = NARRATION.split(/[.!?;|]+/).map(s => s.trim()).filter(Boolean);
|
|
1988
2091
|
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2092
|
+
const count = Math.max(1, sentences.length);
|
|
2093
|
+
const sentDur = window.DURATION / count;
|
|
2094
|
+
const currIdx = Math.min(count - 1, Math.floor(t / sentDur));
|
|
2095
|
+
const activeText = sentences[currIdx] || NARRATION;
|
|
2096
|
+
|
|
2097
|
+
// Sentence local spring transition (smooth entrance per sentence)
|
|
2098
|
+
const localT = t - currIdx * sentDur;
|
|
2099
|
+
const sentSpring = spring(localT, 0.04, 210, 16);
|
|
2100
|
+
const sentAlpha = Math.min(1, Math.max(0, sentSpring));
|
|
2101
|
+
const sentSlideY = (1 - sentSpring) * 10;
|
|
1998
2102
|
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
|
|
2003
|
-
ctx.shadowBlur = 24;
|
|
2004
|
-
ctx.shadowOffsetY = 4;
|
|
2005
|
-
ctx.textAlign = 'left';
|
|
2006
|
-
const cleanCap = CAPTION.length > 60 ? CAPTION.slice(0, 60) + '...' : CAPTION;
|
|
2007
|
-
ctx.fillText(cleanCap, 80, capY + 30);
|
|
2008
|
-
|
|
2009
|
-
// Subtitle / Spoken Narration - bottom center clean broadcast subtitles
|
|
2010
|
-
if (NARRATION) {
|
|
2011
|
-
const subSpring = spring(t, 0.22, 180, 16);
|
|
2012
|
-
ctx.globalAlpha = Math.min(1, Math.max(0, subSpring));
|
|
2013
|
-
ctx.font = '500 28px ' + FONT_BODY;
|
|
2014
|
-
ctx.fillStyle = '#f8fafc';
|
|
2015
|
-
ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
|
|
2016
|
-
ctx.shadowBlur = 18;
|
|
2017
|
-
ctx.shadowOffsetY = 3;
|
|
2103
|
+
ctx.save();
|
|
2104
|
+
ctx.globalAlpha = sentAlpha;
|
|
2105
|
+
ctx.font = '500 32px ' + FONT_BODY;
|
|
2018
2106
|
ctx.textAlign = 'center';
|
|
2019
|
-
|
|
2020
|
-
|
|
2107
|
+
|
|
2108
|
+
// Word wrap if sentence is long (> 58 chars)
|
|
2109
|
+
const maxChars = 56;
|
|
2110
|
+
const words = activeText.split(' ');
|
|
2111
|
+
const lines = [];
|
|
2112
|
+
let currentLine = '';
|
|
2113
|
+
for (let w = 0; w < words.length; w++) {
|
|
2114
|
+
const testLine = currentLine ? currentLine + ' ' + words[w] : words[w];
|
|
2115
|
+
if (testLine.length > maxChars && currentLine.length > 0) {
|
|
2116
|
+
lines.push(currentLine);
|
|
2117
|
+
currentLine = words[w];
|
|
2118
|
+
} else {
|
|
2119
|
+
currentLine = testLine;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
if (currentLine) lines.push(currentLine);
|
|
2123
|
+
|
|
2124
|
+
const subBaseY = H * 0.90 + sentSlideY;
|
|
2125
|
+
const lineSpacing = 44;
|
|
2126
|
+
const startY = subBaseY - ((lines.length - 1) * lineSpacing) / 2;
|
|
2127
|
+
|
|
2128
|
+
for (let l = 0; l < lines.length; l++) {
|
|
2129
|
+
const ly = startY + l * lineSpacing;
|
|
2130
|
+
// Soft translucent background backing for broadcast legibility
|
|
2131
|
+
const textMetrics = ctx.measureText(lines[l]);
|
|
2132
|
+
const pillW = textMetrics.width + 40;
|
|
2133
|
+
ctx.fillStyle = 'rgba(6, 9, 14, 0.72)';
|
|
2134
|
+
ctx.beginPath();
|
|
2135
|
+
ctx.roundRect((W - pillW) / 2, ly - 28, pillW, 38, 8);
|
|
2136
|
+
ctx.fill();
|
|
2137
|
+
|
|
2138
|
+
// Subtitle text with subtle glow
|
|
2139
|
+
ctx.fillStyle = '#ffffff';
|
|
2140
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
|
|
2141
|
+
ctx.shadowBlur = 12;
|
|
2142
|
+
ctx.fillText(lines[l], W / 2, ly);
|
|
2143
|
+
}
|
|
2144
|
+
ctx.restore();
|
|
2021
2145
|
}
|
|
2146
|
+
|
|
2022
2147
|
ctx.restore();
|
|
2023
2148
|
|
|
2024
2149
|
// Bottom Progress Indicator (minimal line)
|