@amg555/frameforge-mcp 2.4.1 → 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 +40 -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 +978 -44
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ 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
|
+
|
|
35
|
+
## [2.4.2] — 2026-11-09
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- **Procedural Doodled Character Animation Engine (`src/video-renderer/scene-animator.js`)**:
|
|
39
|
+
- Pure-function-of-time character generator (`drawDoodleCharacter`) supporting expressive facial animations (talking, shivering, smiling, surprised), multiple hairstyles (wild, modern, feather), clothing types (none, pelt, shirt, cloak), and animated poses (hold_spear, shiver, sewing, dress_shirt, peace_gesture, thinker, natural).
|
|
40
|
+
- 12fps hand-drawn boiling wobble (`wb(seed, mag)`) and organic sketchy vector primitives (`sLine`, `sCircle`, `sRect`) conforming to the pure-$t$ rendering contract.
|
|
41
|
+
- 9 narrative-matched character scene themes: `doodle_dressing`, `doodle_shiver_cold`, `doodle_sewing_needle`, `doodle_hunter_savannah`, `doodle_shame_reflection`, `doodle_tribe_meeting`, `doodle_thinking_future`, `doodle_microscope_lice`, and `doodle_general_human`.
|
|
42
|
+
- Action-grounded theme detection in `detectComponentTheme`: Human narrative, evolution, psychology, and daily action scripts automatically map to physical character animations rather than abstract geometry.
|
|
43
|
+
- Enhanced LLM prompt in `generateSceneCodeWithLLM` directing AI models to animate expressive human figures actively performing narrated actions.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
7
47
|
## [2.4.1] — 2026-11-09
|
|
8
48
|
|
|
9
49
|
### Fixed
|
|
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
|
@@ -267,6 +267,61 @@ function detectComponentTheme({ visualDescription = '', caption = '', narration
|
|
|
267
267
|
if (imagePath) return 'image';
|
|
268
268
|
const text = `${visualDescription} ${caption} ${narration} ${topic}`.toLowerCase();
|
|
269
269
|
|
|
270
|
+
// ── Doodled Character Actions (Human Stories, History, Evolution, Daily Life) ──
|
|
271
|
+
// 1. Microscope / Lice Theory / Parasite / Genetics (Scene 7)
|
|
272
|
+
if (/(?:^|[^\w])(lice|louse|microscope|fiber|weave|parasite|lice evolution|genetic biology proof)(?:[^\w]|$)/i.test(text)
|
|
273
|
+
|| /പേൻ|പേൻ തിയറി|തലയിലെ പേൻ|ശരീരത്തിലെ പേൻ|ലൈസ് തിയറി|മൈക്രോസ്കോപ്പ്/.test(text)) {
|
|
274
|
+
return 'doodle_microscope_lice';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 2. Shivering / Cold / Protection Flaw / Winter / Freezing (Scene 2, 6)
|
|
278
|
+
if (/(?:^|[^\w])(shiver|shivering|cold|freeze|freezing|ice|winter|teeth|chatter|protection flaw)(?:[^\w]|$)/i.test(text)
|
|
279
|
+
|| /സാധാരണ ധാരണകൾ|തണുപ്പും സംരക്ഷണവും|തള്ളിക്കളയുന്നു|ശരീരത്തെ സംരക്ഷിക്കാൻ|ശരീര സംരക്ഷണം|വിറയ്ക്കുക|തണുപ്പിൽ നിന്ന് രക്ഷ നേടാൻ|തണുപ്പ് എന്ന കാരണം|തണുപ്പുള്ള സ്ഥലങ്ങളിലേക്ക്/.test(text)) {
|
|
280
|
+
return 'doodle_shiver_cold';
|
|
281
|
+
}
|
|
282
|
+
|
|
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
|
+
if (/(?:^|[^\w])(tribe|tribal|clan|stranger|meeting strangers|social language|status in society|hierarchy|friend or enemy)(?:[^\w]|$)/i.test(text)
|
|
291
|
+
|| /കൂട്ടമായി|സമൂഹം|അപരിചിത|ഗോത്രം|ശത്രു|സുഹൃത്ത്|സാമൂഹിക ഭാഷ|സാമൂഹിക|സ്ഥാനം തിരിച്ചറിഞ്ഞിരുന്നത്/.test(text)) {
|
|
292
|
+
return 'doodle_tribe_meeting';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 5. Future Planning / Cognitive Revolution / Tomorrow / Foresight / Campfire thinking (Scene 8)
|
|
296
|
+
if (/(?:^|[^\w])(planning for tomorrow|future planning|cognitive revolution|thinking future|contemplate|foresight|ian gilligan)(?:[^\w]|$)/i.test(text)
|
|
297
|
+
|| /നാളത്തേക്ക്|പ്ലാൻ|ഭാവി|ഭാവിയെ കുറിച്ച്|ബുദ്ധി ഉപയോഗിച്ച്|ചിന്തയിലെ മാറ്റം|ഇയാൻ ഗില്ലിഗൻ|ചിന്തിക്കാൻ തുടങ്ങി/.test(text)) {
|
|
298
|
+
return 'doodle_thinking_future';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 6. Bone Needle / Sewing / Stitching / Animal Pelt / Prehistoric Tools (Scene 4)
|
|
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)
|
|
303
|
+
|| /സൂചി|സൂചിയും|തുന്ന|തുന്നാൻ|തോൽ തുന്നാൻ|തുന്നൽ|തോൽ തുന്നാനുള്ള/.test(text)) {
|
|
304
|
+
return 'doodle_sewing_needle';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 7. Shame / Mirror / Reflection / Water / Self Awareness / Theory of Mind (Scene 3, 9)
|
|
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)
|
|
309
|
+
|| /കണ്ണാടി|പ്രതിഫലനം|മറ്റുള്ളവർ എന്നെ|തിയറി ഓഫ് മൈൻഡ്|സ്വന്തം ശരീരം മറച്ചു|വ്യക്തിത്വം|ശ്രദ്ധിക്കുന്നുണ്ട്|യഥാർത്ഥ രഹസ്യം|നാണം മറയ്ക്കാൻ/.test(text)) {
|
|
310
|
+
return 'doodle_shame_reflection';
|
|
311
|
+
}
|
|
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
|
+
// 9. General Human Character
|
|
320
|
+
if (/(?:^|[^\w])(human|person|man|woman|people|child|guy|doodle|character|actor)(?:[^\w]|$)/i.test(text)
|
|
321
|
+
|| /മനുഷ്യൻ|ആളുകൾ|വ്യക്തി/.test(text)) {
|
|
322
|
+
return 'doodle_general_human';
|
|
323
|
+
}
|
|
324
|
+
|
|
270
325
|
// Celestial / Orbiting satellites (e.g. GPS, orbital mechanics, planetary science)
|
|
271
326
|
if (/(?:^|[^\w])(orbit|satellite|satellites|gps|planet|earth|mars|moon|galaxy|universe|solar|astronomy|gravity|space|telescope|interstellar)(?:[^\w]|$)/.test(text)
|
|
272
327
|
|| /ബഹിരാകാശ|ഗ്രഹം|ഉപഗ്രഹ|ഭൂമി|നക്ഷത്ര|പ്രപഞ്ച|अंतरिक्ष|ग्रह|तारा|விண்வெளி/.test(text)) {
|
|
@@ -685,16 +740,873 @@ function drawBackground(t) {
|
|
|
685
740
|
// ── Full-Canvas Cinematic Visual Directors ─────────────────────────────────────
|
|
686
741
|
function drawMainScene(t) {
|
|
687
742
|
ctx.save();
|
|
743
|
+
const p = Math.min(1, Math.max(0, t / window.DURATION));
|
|
688
744
|
const enterSpring = spring(t, 0.05, 170, 13);
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
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);
|
|
692
750
|
ctx.translate(-W / 2, -H * 0.50);
|
|
693
751
|
|
|
694
752
|
const cx = W / 2;
|
|
695
753
|
const cy = H * 0.50;
|
|
696
754
|
|
|
697
|
-
|
|
755
|
+
// ── Hand-Drawn Boil Wobble & Sketch Helpers (12fps Hand-Drawn Animation) ──
|
|
756
|
+
function wb(seed, mag = 2.4) {
|
|
757
|
+
const step = Math.floor(t * 12);
|
|
758
|
+
return Math.sin(seed * 19.37 + step * 2.41) * mag;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function sLine(x1, y1, x2, y2, seed, color = '#f8fafc', width = 3.5) {
|
|
762
|
+
ctx.save();
|
|
763
|
+
ctx.strokeStyle = color;
|
|
764
|
+
ctx.lineWidth = width;
|
|
765
|
+
ctx.lineCap = 'round';
|
|
766
|
+
ctx.lineJoin = 'round';
|
|
767
|
+
const mx = (x1 + x2) / 2 + wb(seed, 3.5);
|
|
768
|
+
const my = (y1 + y2) / 2 + wb(seed + 1, 3.5);
|
|
769
|
+
ctx.beginPath();
|
|
770
|
+
ctx.moveTo(x1 + wb(seed + 2, 1.5), y1 + wb(seed + 3, 1.5));
|
|
771
|
+
ctx.quadraticCurveTo(mx, my, x2 + wb(seed + 4, 1.5), y2 + wb(seed + 5, 1.5));
|
|
772
|
+
ctx.stroke();
|
|
773
|
+
ctx.restore();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function sCircle(x, y, r, seed, color = '#f8fafc', width = 3, fillColor = null) {
|
|
777
|
+
ctx.save();
|
|
778
|
+
if (fillColor) {
|
|
779
|
+
ctx.fillStyle = fillColor;
|
|
780
|
+
ctx.beginPath();
|
|
781
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
782
|
+
ctx.fill();
|
|
783
|
+
}
|
|
784
|
+
ctx.strokeStyle = color;
|
|
785
|
+
ctx.lineWidth = width;
|
|
786
|
+
ctx.beginPath();
|
|
787
|
+
const segs = 10;
|
|
788
|
+
for (let s = 0; s <= segs; s++) {
|
|
789
|
+
const a = (s / segs) * Math.PI * 2;
|
|
790
|
+
const curR = r + wb(seed + s, 2.5);
|
|
791
|
+
const px = x + Math.cos(a) * curR;
|
|
792
|
+
const py = y + Math.sin(a) * curR;
|
|
793
|
+
if (s === 0) ctx.moveTo(px, py);
|
|
794
|
+
else ctx.lineTo(px, py);
|
|
795
|
+
}
|
|
796
|
+
ctx.closePath();
|
|
797
|
+
ctx.stroke();
|
|
798
|
+
ctx.restore();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function sRect(x, y, w, h, seed, color = '#f8fafc', width = 3, fillColor = null) {
|
|
802
|
+
ctx.save();
|
|
803
|
+
if (fillColor) {
|
|
804
|
+
ctx.fillStyle = fillColor;
|
|
805
|
+
ctx.fillRect(x, y, w, h);
|
|
806
|
+
}
|
|
807
|
+
sLine(x, y, x + w, y, seed, color, width);
|
|
808
|
+
sLine(x + w, y, x + w, y + h, seed + 1, color, width);
|
|
809
|
+
sLine(x + w, y + h, x, y + h, seed + 2, color, width);
|
|
810
|
+
sLine(x, y + h, x, y, seed + 3, color, width);
|
|
811
|
+
ctx.restore();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function drawDoodleCharacter({
|
|
815
|
+
x = cx,
|
|
816
|
+
y = cy,
|
|
817
|
+
scale = 1.0,
|
|
818
|
+
headRadius = 38,
|
|
819
|
+
torsoHeight = 90,
|
|
820
|
+
legLength = 95,
|
|
821
|
+
hairStyle = 'wild',
|
|
822
|
+
expression = 'talking',
|
|
823
|
+
clothing = 'none',
|
|
824
|
+
armPose = 'natural',
|
|
825
|
+
seed = 100,
|
|
826
|
+
} = {}) {
|
|
827
|
+
ctx.save();
|
|
828
|
+
ctx.translate(x, y);
|
|
829
|
+
ctx.scale(scale, scale);
|
|
830
|
+
|
|
831
|
+
const blink = (t % 3.2 < 0.14) ? 0.15 : 1.0;
|
|
832
|
+
const mouthTalk = Math.abs(Math.sin(t * 14)) * 4;
|
|
833
|
+
const torsoTopY = -torsoHeight;
|
|
834
|
+
const torsoBottomY = 0;
|
|
835
|
+
|
|
836
|
+
// Torso Clothing Fills
|
|
837
|
+
if (clothing === 'pelt') {
|
|
838
|
+
ctx.fillStyle = '#b45309';
|
|
839
|
+
ctx.beginPath();
|
|
840
|
+
ctx.moveTo(-24 + wb(seed, 2), torsoTopY);
|
|
841
|
+
ctx.lineTo(24 + wb(seed + 1, 2), torsoTopY);
|
|
842
|
+
ctx.lineTo(32 + wb(seed + 2, 2), torsoBottomY + 28);
|
|
843
|
+
ctx.lineTo(-28 + wb(seed + 3, 2), torsoBottomY + 28);
|
|
844
|
+
ctx.closePath();
|
|
845
|
+
ctx.fill();
|
|
846
|
+
sLine(-28, torsoBottomY + 2, 32, torsoBottomY + 2, seed + 4, '#78350f', 4);
|
|
847
|
+
} else if (clothing === 'shirt') {
|
|
848
|
+
ctx.fillStyle = ACCENT;
|
|
849
|
+
ctx.beginPath();
|
|
850
|
+
ctx.moveTo(-26 + wb(seed, 2), torsoTopY);
|
|
851
|
+
ctx.lineTo(26 + wb(seed + 1, 2), torsoTopY);
|
|
852
|
+
ctx.lineTo(28 + wb(seed + 2, 2), torsoBottomY + 12);
|
|
853
|
+
ctx.lineTo(-28 + wb(seed + 3, 2), torsoBottomY + 12);
|
|
854
|
+
ctx.closePath();
|
|
855
|
+
ctx.fill();
|
|
856
|
+
sLine(-14, torsoTopY, 0, torsoTopY + 18, seed + 4, '#ffffff', 3);
|
|
857
|
+
sLine(14, torsoTopY, 0, torsoTopY + 18, seed + 5, '#ffffff', 3);
|
|
858
|
+
sLine(0, torsoTopY + 18, 0, torsoBottomY + 10, seed + 6, '#ffffff', 2.5);
|
|
859
|
+
} else if (clothing === 'cloak') {
|
|
860
|
+
ctx.fillStyle = '#0f766e';
|
|
861
|
+
ctx.beginPath();
|
|
862
|
+
ctx.moveTo(-32, torsoTopY - 6);
|
|
863
|
+
ctx.lineTo(32, torsoTopY - 6);
|
|
864
|
+
ctx.lineTo(44, torsoBottomY + 32);
|
|
865
|
+
ctx.lineTo(-44, torsoBottomY + 32);
|
|
866
|
+
ctx.closePath();
|
|
867
|
+
ctx.fill();
|
|
868
|
+
} else {
|
|
869
|
+
sLine(0, torsoTopY, 0, torsoBottomY, seed + 7, '#fcd34d', 4);
|
|
870
|
+
sLine(-15, torsoTopY + 30, -2, torsoTopY + 40, seed + 8, 'rgba(255,255,255,0.3)', 2);
|
|
871
|
+
sLine(15, torsoTopY + 30, 2, torsoTopY + 40, seed + 9, 'rgba(255,255,255,0.3)', 2);
|
|
872
|
+
}
|
|
873
|
+
sLine(0, torsoTopY, 0, torsoBottomY, seed + 10, '#f8fafc', 4);
|
|
874
|
+
|
|
875
|
+
// Head
|
|
876
|
+
const headCenterY = torsoTopY - headRadius - 6 + wb(seed + 11, 1.5);
|
|
877
|
+
sCircle(0, headCenterY, headRadius, seed + 12, '#f8fafc', 3.5, '#fef3c7');
|
|
878
|
+
|
|
879
|
+
// Hair
|
|
880
|
+
if (hairStyle === 'wild') {
|
|
881
|
+
for (let h = 0; h < 6; h++) {
|
|
882
|
+
const hairAng = -Math.PI * 0.85 + (h / 5) * Math.PI * 0.7;
|
|
883
|
+
const hx1 = Math.cos(hairAng) * headRadius;
|
|
884
|
+
const hy1 = headCenterY + Math.sin(hairAng) * headRadius;
|
|
885
|
+
const hx2 = Math.cos(hairAng) * (headRadius + 22 + wb(seed + 13 + h, 4));
|
|
886
|
+
const hy2 = headCenterY + Math.sin(hairAng) * (headRadius + 22 + wb(seed + 14 + h, 4));
|
|
887
|
+
sLine(hx1, hy1, hx2, hy2, seed + 15 + h, '#78350f', 3.5);
|
|
888
|
+
}
|
|
889
|
+
} else if (hairStyle === 'modern') {
|
|
890
|
+
ctx.fillStyle = '#1e293b';
|
|
891
|
+
ctx.beginPath();
|
|
892
|
+
ctx.arc(0, headCenterY - 12, headRadius * 0.96, Math.PI, Math.PI * 2);
|
|
893
|
+
ctx.closePath();
|
|
894
|
+
ctx.fill();
|
|
895
|
+
sLine(-headRadius * 0.9, headCenterY - 12, headRadius * 0.9, headCenterY - 12, seed + 20, '#1e293b', 4);
|
|
896
|
+
} else if (hairStyle === 'feather') {
|
|
897
|
+
sLine(8, headCenterY - headRadius, 28, headCenterY - headRadius - 35, seed + 21, '#38bdf8', 4);
|
|
898
|
+
sLine(28, headCenterY - headRadius - 35, 18, headCenterY - headRadius - 20, seed + 22, '#f43f5e', 3);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Eyes
|
|
902
|
+
const eyeY = headCenterY + 4;
|
|
903
|
+
const eyeR = 4 * blink;
|
|
904
|
+
sCircle(-12, eyeY, Math.max(1, eyeR), seed + 23, '#0f172a', 2, '#0f172a');
|
|
905
|
+
sCircle(12, eyeY, Math.max(1, eyeR), seed + 24, '#0f172a', 2, '#0f172a');
|
|
906
|
+
|
|
907
|
+
// Eyebrows
|
|
908
|
+
if (expression === 'shivering') {
|
|
909
|
+
sLine(-18, eyeY - 12 + wb(seed, 2), -6, eyeY - 7 + wb(seed, 2), seed + 25, '#0f172a', 2.5);
|
|
910
|
+
sLine(6, eyeY - 7 + wb(seed, 2), 18, eyeY - 12 + wb(seed, 2), seed + 26, '#0f172a', 2.5);
|
|
911
|
+
} else if (expression === 'surprised' || expression === 'curious') {
|
|
912
|
+
sLine(-18, eyeY - 15, -6, eyeY - 13, seed + 27, '#0f172a', 2.5);
|
|
913
|
+
sLine(6, eyeY - 13, 18, eyeY - 15, seed + 28, '#0f172a', 2.5);
|
|
914
|
+
} else {
|
|
915
|
+
sLine(-17, eyeY - 9, -7, eyeY - 10, seed + 29, '#0f172a', 2.5);
|
|
916
|
+
sLine(7, eyeY - 10, 17, eyeY - 9, seed + 30, '#0f172a', 2.5);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Mouth
|
|
920
|
+
const mouthY = headCenterY + 22;
|
|
921
|
+
if (expression === 'shivering') {
|
|
922
|
+
const mChatter = Math.sin(t * 32) * 2.5;
|
|
923
|
+
sLine(-10, mouthY + mChatter, -4, mouthY - 3 + mChatter, seed + 31, '#0f172a', 2.5);
|
|
924
|
+
sLine(-4, mouthY - 3 + mChatter, 3, mouthY + 3 + mChatter, seed + 32, '#0f172a', 2.5);
|
|
925
|
+
sLine(3, mouthY + 3 + mChatter, 10, mouthY + mChatter, seed + 33, '#0f172a', 2.5);
|
|
926
|
+
} else if (expression === 'talking') {
|
|
927
|
+
ctx.fillStyle = '#0f172a';
|
|
928
|
+
ctx.beginPath();
|
|
929
|
+
ctx.ellipse(0, mouthY, 7, Math.max(2, mouthTalk), 0, 0, Math.PI * 2);
|
|
930
|
+
ctx.fill();
|
|
931
|
+
} else if (expression === 'smiling') {
|
|
932
|
+
ctx.beginPath();
|
|
933
|
+
ctx.arc(0, mouthY - 4, 10, 0.2 * Math.PI, 0.8 * Math.PI);
|
|
934
|
+
ctx.strokeStyle = '#0f172a';
|
|
935
|
+
ctx.lineWidth = 2.5;
|
|
936
|
+
ctx.stroke();
|
|
937
|
+
} else {
|
|
938
|
+
sLine(-8, mouthY, 8, mouthY, seed + 34, '#0f172a', 2.5);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Legs
|
|
942
|
+
const hipY = torsoBottomY;
|
|
943
|
+
if (armPose === 'sewing' || armPose === 'thinker') {
|
|
944
|
+
sLine(-10, hipY, -45, hipY + 45, seed + 35, '#f8fafc', 4);
|
|
945
|
+
sLine(-45, hipY + 45, 10, hipY + 50, seed + 36, '#f8fafc', 4);
|
|
946
|
+
sLine(10, hipY, 45, hipY + 45, seed + 37, '#f8fafc', 4);
|
|
947
|
+
sLine(45, hipY + 45, -10, hipY + 50, seed + 38, '#f8fafc', 4);
|
|
948
|
+
} else if (armPose === 'hold_spear') {
|
|
949
|
+
const legSwing = Math.sin(t * 5.5);
|
|
950
|
+
const l1KneeX = -15 + legSwing * 22;
|
|
951
|
+
const l1KneeY = hipY + legLength * 0.55;
|
|
952
|
+
const l1FootX = -15 + legSwing * 38;
|
|
953
|
+
const l1FootY = hipY + legLength;
|
|
954
|
+
sLine(-10, hipY, l1KneeX, l1KneeY, seed + 39, '#f8fafc', 4);
|
|
955
|
+
sLine(l1KneeX, l1KneeY, l1FootX, l1FootY, seed + 40, '#f8fafc', 4);
|
|
956
|
+
|
|
957
|
+
const l2KneeX = 15 - legSwing * 22;
|
|
958
|
+
const l2KneeY = hipY + legLength * 0.55;
|
|
959
|
+
const l2FootX = 15 - legSwing * 38;
|
|
960
|
+
const l2FootY = hipY + legLength;
|
|
961
|
+
sLine(10, hipY, l2KneeX, l2KneeY, seed + 41, '#f8fafc', 4);
|
|
962
|
+
sLine(l2KneeX, l2KneeY, l2FootX, l2FootY, seed + 42, '#f8fafc', 4);
|
|
963
|
+
} else {
|
|
964
|
+
sLine(-12, hipY, -14, hipY + legLength, seed + 43, '#f8fafc', 4);
|
|
965
|
+
sLine(12, hipY, 14, hipY + legLength, seed + 44, '#f8fafc', 4);
|
|
966
|
+
sLine(-14, hipY + legLength, -24, hipY + legLength, seed + 45, '#f8fafc', 3.5);
|
|
967
|
+
sLine(14, hipY + legLength, 24, hipY + legLength, seed + 46, '#f8fafc', 3.5);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Arms & Hands
|
|
971
|
+
const shoulderY = torsoTopY + 12;
|
|
972
|
+
if (armPose === 'shiver') {
|
|
973
|
+
const shivA = Math.sin(t * 34) * 3;
|
|
974
|
+
sLine(-20, shoulderY, 16 + shivA, shoulderY + 35, seed + 47, '#f8fafc', 4);
|
|
975
|
+
sLine(20, shoulderY, -16 + shivA, shoulderY + 35, seed + 48, '#f8fafc', 4);
|
|
976
|
+
} else if (armPose === 'hold_spear') {
|
|
977
|
+
const spearX = -45;
|
|
978
|
+
sLine(-18, shoulderY, -35, shoulderY + 30, seed + 49, '#f8fafc', 4);
|
|
979
|
+
sLine(-35, shoulderY + 30, spearX, shoulderY + 5, seed + 50, '#f8fafc', 4);
|
|
980
|
+
sLine(spearX, -torsoHeight - 130, spearX, hipY + legLength + 10, seed + 51, '#92400e', 5);
|
|
981
|
+
ctx.fillStyle = '#64748b';
|
|
982
|
+
ctx.beginPath();
|
|
983
|
+
ctx.moveTo(spearX, -torsoHeight - 165);
|
|
984
|
+
ctx.lineTo(spearX - 10, -torsoHeight - 128);
|
|
985
|
+
ctx.lineTo(spearX + 10, -torsoHeight - 128);
|
|
986
|
+
ctx.closePath();
|
|
987
|
+
ctx.fill();
|
|
988
|
+
sLine(18, shoulderY, 35, shoulderY + 25 + Math.sin(t * 5.5) * 20, seed + 52, '#f8fafc', 4);
|
|
989
|
+
sLine(35, shoulderY + 25 + Math.sin(t * 5.5) * 20, 48, shoulderY + 45, seed + 53, '#f8fafc', 4);
|
|
990
|
+
} else if (armPose === 'sewing') {
|
|
991
|
+
sLine(-18, shoulderY, -35, shoulderY + 40, seed + 54, '#f8fafc', 4);
|
|
992
|
+
sLine(-35, shoulderY + 40, -18, shoulderY + 55, seed + 55, '#f8fafc', 4);
|
|
993
|
+
const sewCycle = Math.sin(t * 4);
|
|
994
|
+
const handX = 20 + sewCycle * 25;
|
|
995
|
+
const handY = shoulderY + 45 - Math.cos(t * 4) * 15;
|
|
996
|
+
sLine(18, shoulderY, 38, shoulderY + 25, seed + 56, '#f8fafc', 4);
|
|
997
|
+
sLine(38, shoulderY + 25, handX, handY, seed + 57, '#f8fafc', 4);
|
|
998
|
+
sLine(handX - 5, handY - 10, handX + 18, handY + 12, seed + 58, '#fef08a', 3.5);
|
|
999
|
+
ctx.strokeStyle = '#ffffff';
|
|
1000
|
+
ctx.lineWidth = 1.5;
|
|
1001
|
+
ctx.beginPath();
|
|
1002
|
+
ctx.moveTo(handX - 5, handY - 10);
|
|
1003
|
+
ctx.quadraticCurveTo(handX - 25, handY - 25, 0, hipY + 10);
|
|
1004
|
+
ctx.stroke();
|
|
1005
|
+
} else if (armPose === 'cover_body') {
|
|
1006
|
+
sLine(-18, shoulderY, -12, hipY - 10, seed + 59, '#f8fafc', 4);
|
|
1007
|
+
sLine(18, shoulderY, 12, hipY - 10, seed + 60, '#f8fafc', 4);
|
|
1008
|
+
} else if (armPose === 'dress_shirt') {
|
|
1009
|
+
sLine(-18, shoulderY, -38, shoulderY + 30, seed + 61, '#f8fafc', 4);
|
|
1010
|
+
sLine(-38, shoulderY + 30, -22, hipY - 5, seed + 62, '#f8fafc', 4);
|
|
1011
|
+
const adjustY = shoulderY + 8 + Math.sin(t * 3) * 6;
|
|
1012
|
+
sLine(18, shoulderY, 35, shoulderY + 15, seed + 63, '#f8fafc', 4);
|
|
1013
|
+
sLine(35, shoulderY + 15, 10, adjustY, seed + 64, '#f8fafc', 4);
|
|
1014
|
+
} else if (armPose === 'peace_gesture') {
|
|
1015
|
+
sLine(-18, shoulderY, -26, shoulderY + 40, seed + 65, '#f8fafc', 4);
|
|
1016
|
+
sLine(18, shoulderY, 40, shoulderY - 10 + Math.sin(t * 2) * 5, seed + 66, '#f8fafc', 4);
|
|
1017
|
+
sLine(40, shoulderY - 10 + Math.sin(t * 2) * 5, 45, shoulderY - 35, seed + 67, '#f8fafc', 4);
|
|
1018
|
+
} else if (armPose === 'thinker') {
|
|
1019
|
+
sLine(18, shoulderY, 36, hipY + 15, seed + 68, '#f8fafc', 4);
|
|
1020
|
+
sLine(36, hipY + 15, 8, headCenterY + 18, seed + 69, '#f8fafc', 4);
|
|
1021
|
+
sLine(-18, shoulderY, -30, hipY + 25, seed + 70, '#f8fafc', 4);
|
|
1022
|
+
} else {
|
|
1023
|
+
const gArm = Math.sin(t * 2.5) * 12;
|
|
1024
|
+
sLine(-18, shoulderY, -35, shoulderY + 35, seed + 71, '#f8fafc', 4);
|
|
1025
|
+
sLine(-35, shoulderY + 35, -42, shoulderY + 60, seed + 72, '#f8fafc', 4);
|
|
1026
|
+
sLine(18, shoulderY, 35, shoulderY + 20 - gArm, seed + 73, '#f8fafc', 4);
|
|
1027
|
+
sLine(35, shoulderY + 20 - gArm, 45, shoulderY + 5 - gArm, seed + 74, '#f8fafc', 4);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
ctx.restore();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ── Doodled Character Narrative Themes (3-Beat Progressive Choreography) ─
|
|
1034
|
+
if (COMP_THEME === 'doodle_dressing') {
|
|
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
|
+
|
|
1039
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.25)', 2.5);
|
|
1040
|
+
|
|
1041
|
+
// Clothes rack on left (sways in beat 1)
|
|
1042
|
+
sLine(cx - 360, cy + 220, cx - 360, cy - 140, 11, '#94a3b8', 5);
|
|
1043
|
+
sLine(cx - 410, cy + 220, cx - 310, cy + 220, 12, '#94a3b8', 5);
|
|
1044
|
+
sLine(cx - 420, cy - 140, cx - 180, cy - 140, 13, '#cbd5e1', 5);
|
|
1045
|
+
|
|
1046
|
+
// 3 shirts on hangers
|
|
1047
|
+
const shirtColors = ['#38bdf8', '#f43f5e', '#fbbf24'];
|
|
1048
|
+
for (let c = 0; c < 3; c++) {
|
|
1049
|
+
const hx = cx - 380 + c * 80;
|
|
1050
|
+
const sway = Math.sin(t * 2.5 + c * 1.5) * 5;
|
|
1051
|
+
sLine(hx, cy - 140, hx + sway, cy - 110, 20 + c, '#e2e8f0', 2.5);
|
|
1052
|
+
ctx.fillStyle = shirtColors[c];
|
|
1053
|
+
ctx.beginPath();
|
|
1054
|
+
ctx.moveTo(hx + sway - 22, cy - 110);
|
|
1055
|
+
ctx.lineTo(hx + sway + 22, cy - 110);
|
|
1056
|
+
ctx.lineTo(hx + sway + 28, cy - 10);
|
|
1057
|
+
ctx.lineTo(hx + sway - 28, cy - 10);
|
|
1058
|
+
ctx.closePath();
|
|
1059
|
+
ctx.fill();
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Mirror on right - emerges and glows in Beat 2 & 3
|
|
1063
|
+
const mirrorX = cx + 320;
|
|
1064
|
+
const mirrorY = cy + 20;
|
|
1065
|
+
ctx.fillStyle = 'rgba(56, 189, 248, ' + (0.05 + beat2 * 0.08) + ')';
|
|
1066
|
+
ctx.beginPath();
|
|
1067
|
+
ctx.roundRect(mirrorX - 85, mirrorY - 210, 170, 390, 24);
|
|
1068
|
+
ctx.fill();
|
|
1069
|
+
ctx.strokeStyle = beat3 > 0.5 ? '#f59e0b' : '#94a3b8';
|
|
1070
|
+
ctx.lineWidth = 4;
|
|
1071
|
+
ctx.stroke();
|
|
1072
|
+
|
|
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
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Main character moves progressively from rack -> center -> mirror
|
|
1091
|
+
const charX = cx - 220 + beat2 * 170 + beat3 * 60;
|
|
1092
|
+
drawDoodleCharacter({
|
|
1093
|
+
x: charX,
|
|
1094
|
+
y: cy + 140,
|
|
1095
|
+
scale: 1.35,
|
|
1096
|
+
hairStyle: 'modern',
|
|
1097
|
+
expression: beat3 > 0.5 ? 'smiling' : 'talking',
|
|
1098
|
+
clothing: 'shirt',
|
|
1099
|
+
armPose: beat2 > 0.5 ? 'dress_shirt' : 'natural',
|
|
1100
|
+
seed: 101,
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
} else if (COMP_THEME === 'doodle_shiver_cold') {
|
|
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
|
+
|
|
1108
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1109
|
+
|
|
1110
|
+
// Freezing wind streaks rushing across
|
|
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);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Drifting snow particles
|
|
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');
|
|
1122
|
+
}
|
|
1123
|
+
|
|
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
|
+
}
|
|
1134
|
+
|
|
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;
|
|
1139
|
+
drawDoodleCharacter({
|
|
1140
|
+
x: cx - 120 + vibX,
|
|
1141
|
+
y: cy + 140 + vibY,
|
|
1142
|
+
scale: 1.35,
|
|
1143
|
+
hairStyle: 'wild',
|
|
1144
|
+
expression: isShivering ? 'shivering' : 'curious',
|
|
1145
|
+
clothing: 'none',
|
|
1146
|
+
armPose: isShivering ? 'shiver' : 'thinker',
|
|
1147
|
+
seed: 102,
|
|
1148
|
+
});
|
|
1149
|
+
|
|
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
|
+
}
|
|
1181
|
+
|
|
1182
|
+
} else if (COMP_THEME === 'doodle_hunter_savannah') {
|
|
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
|
|
1188
|
+
const sunR = 75 + Math.sin(t * 2) * 6;
|
|
1189
|
+
ctx.fillStyle = '#f59e0b';
|
|
1190
|
+
ctx.beginPath();
|
|
1191
|
+
ctx.arc(cx - 380, cy - 180, sunR, 0, Math.PI * 2);
|
|
1192
|
+
ctx.fill();
|
|
1193
|
+
for (let r = 0; r < 8; r++) {
|
|
1194
|
+
const rayA = (r / 8) * Math.PI * 2 + t * 0.4;
|
|
1195
|
+
const rx1 = cx - 380 + Math.cos(rayA) * (sunR + 10);
|
|
1196
|
+
const ry1 = cy - 180 + Math.sin(rayA) * (sunR + 10);
|
|
1197
|
+
const rx2 = cx - 380 + Math.cos(rayA) * (sunR + 32);
|
|
1198
|
+
const ry2 = cy - 180 + Math.sin(rayA) * (sunR + 32);
|
|
1199
|
+
sLine(rx1, ry1, rx2, ry2, 450 + r, '#fbbf24', 3);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Ground and tall swaying savannah grass
|
|
1203
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, '#78350f', 3);
|
|
1204
|
+
for (let g = 0; g < 32; g++) {
|
|
1205
|
+
const gx = cx - 550 + g * 36;
|
|
1206
|
+
const sway = Math.sin(t * 3.2 + g * 0.4) * 16;
|
|
1207
|
+
sLine(gx, cy + 220, gx + sway, cy + 140, 500 + g, '#d97706', 2.5);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
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');
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Walking early hunter with spear
|
|
1219
|
+
drawDoodleCharacter({
|
|
1220
|
+
x: charX,
|
|
1221
|
+
y: cy + 140,
|
|
1222
|
+
scale: 1.35,
|
|
1223
|
+
hairStyle: 'wild',
|
|
1224
|
+
expression: swatting ? 'shivering' : 'talking',
|
|
1225
|
+
clothing: 'none',
|
|
1226
|
+
armPose: 'hold_spear',
|
|
1227
|
+
seed: 103,
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
} else if (COMP_THEME === 'doodle_sewing_needle') {
|
|
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
|
+
|
|
1235
|
+
// Campfire with animated flames and sparks
|
|
1236
|
+
const fireX = cx - 380;
|
|
1237
|
+
const fireY = cy + 180;
|
|
1238
|
+
for (let f = 0; f < 5; f++) {
|
|
1239
|
+
const flH = 50 + Math.sin(t * 12 + f) * 22;
|
|
1240
|
+
ctx.fillStyle = f % 2 === 0 ? '#ef4444' : '#f59e0b';
|
|
1241
|
+
ctx.beginPath();
|
|
1242
|
+
ctx.moveTo(fireX - 30 + f * 12, fireY);
|
|
1243
|
+
ctx.quadraticCurveTo(fireX + (f - 2) * 8, fireY - flH, fireX - 10 + f * 12, fireY);
|
|
1244
|
+
ctx.fill();
|
|
1245
|
+
}
|
|
1246
|
+
for (let sp = 0; sp < 8; sp++) {
|
|
1247
|
+
const sx = fireX + Math.sin(t * 4 + sp) * 35;
|
|
1248
|
+
const sy = fireY - 30 - ((t * 80 + sp * 35) % 140);
|
|
1249
|
+
sCircle(sx, sy, 2, 600 + sp, '#fbbf24', 1, '#fbbf24');
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Seated human stitching hide with bone needle
|
|
1253
|
+
drawDoodleCharacter({
|
|
1254
|
+
x: cx - 120,
|
|
1255
|
+
y: cy + 140,
|
|
1256
|
+
scale: 1.35,
|
|
1257
|
+
hairStyle: 'wild',
|
|
1258
|
+
expression: 'talking',
|
|
1259
|
+
clothing: 'pelt',
|
|
1260
|
+
armPose: 'sewing',
|
|
1261
|
+
seed: 104,
|
|
1262
|
+
});
|
|
1263
|
+
|
|
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);
|
|
1271
|
+
|
|
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();
|
|
1280
|
+
|
|
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);
|
|
1284
|
+
|
|
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();
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
} else if (COMP_THEME === 'doodle_shame_reflection') {
|
|
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
|
+
|
|
1315
|
+
const poolX = cx;
|
|
1316
|
+
const poolY = cy + 180;
|
|
1317
|
+
ctx.fillStyle = 'rgba(14, 165, 233, 0.15)';
|
|
1318
|
+
ctx.beginPath();
|
|
1319
|
+
ctx.ellipse(poolX, poolY, 260, 60, 0, 0, Math.PI * 2);
|
|
1320
|
+
ctx.fill();
|
|
1321
|
+
ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)';
|
|
1322
|
+
ctx.lineWidth = 2.5;
|
|
1323
|
+
ctx.stroke();
|
|
1324
|
+
|
|
1325
|
+
// Ripples in pool
|
|
1326
|
+
for (let rip = 1; rip <= 3; rip++) {
|
|
1327
|
+
const ripR = (rip * 65 + t * 45) % 250;
|
|
1328
|
+
const ripAlpha = (1 - ripR / 250) * 0.4;
|
|
1329
|
+
ctx.strokeStyle = 'rgba(56, 189, 248, ' + ripAlpha + ')';
|
|
1330
|
+
ctx.beginPath();
|
|
1331
|
+
ctx.ellipse(poolX, poolY, ripR, ripR * 0.23, 0, 0, Math.PI * 2);
|
|
1332
|
+
ctx.stroke();
|
|
1333
|
+
}
|
|
1334
|
+
|
|
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
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Standing character looking down and covering body
|
|
1355
|
+
drawDoodleCharacter({
|
|
1356
|
+
x: cx,
|
|
1357
|
+
y: cy + 120,
|
|
1358
|
+
scale: 1.35,
|
|
1359
|
+
hairStyle: 'wild',
|
|
1360
|
+
expression: refSpring > 0.4 ? 'surprised' : 'talking',
|
|
1361
|
+
clothing: 'none',
|
|
1362
|
+
armPose: refSpring > 0.4 ? 'cover_body' : 'natural',
|
|
1363
|
+
seed: 105,
|
|
1364
|
+
});
|
|
1365
|
+
|
|
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
|
+
}
|
|
1389
|
+
|
|
1390
|
+
} else if (COMP_THEME === 'doodle_tribe_meeting') {
|
|
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
|
+
|
|
1396
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1397
|
+
|
|
1398
|
+
// Left Clan Member walks toward center
|
|
1399
|
+
const leftX = cx - 380 + Math.min(1, approach) * 160;
|
|
1400
|
+
drawDoodleCharacter({
|
|
1401
|
+
x: leftX,
|
|
1402
|
+
y: cy + 140,
|
|
1403
|
+
scale: 1.3,
|
|
1404
|
+
hairStyle: 'wild',
|
|
1405
|
+
expression: 'talking',
|
|
1406
|
+
clothing: 'pelt',
|
|
1407
|
+
armPose: greet > 0.5 ? 'peace_gesture' : 'natural',
|
|
1408
|
+
seed: 106,
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
// Right Clan Member walks toward center
|
|
1412
|
+
const rightX = cx + 380 - Math.min(1, approach) * 160;
|
|
1413
|
+
drawDoodleCharacter({
|
|
1414
|
+
x: rightX,
|
|
1415
|
+
y: cy + 140,
|
|
1416
|
+
scale: 1.3,
|
|
1417
|
+
hairStyle: 'feather',
|
|
1418
|
+
expression: 'curious',
|
|
1419
|
+
clothing: 'cloak',
|
|
1420
|
+
armPose: greet > 0.5 ? 'peace_gesture' : 'natural',
|
|
1421
|
+
seed: 107,
|
|
1422
|
+
});
|
|
1423
|
+
|
|
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([]);
|
|
1436
|
+
|
|
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
|
+
}
|
|
1446
|
+
|
|
1447
|
+
} else if (COMP_THEME === 'doodle_thinking_future') {
|
|
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
|
+
|
|
1452
|
+
// Starlit night and crescent moon
|
|
1453
|
+
ctx.fillStyle = '#fef08a';
|
|
1454
|
+
ctx.beginPath();
|
|
1455
|
+
ctx.arc(cx - 360, cy - 190, 50, 0, Math.PI * 2);
|
|
1456
|
+
ctx.fill();
|
|
1457
|
+
ctx.fillStyle = BG2;
|
|
1458
|
+
ctx.beginPath();
|
|
1459
|
+
ctx.arc(cx - 345, cy - 200, 48, 0, Math.PI * 2);
|
|
1460
|
+
ctx.fill();
|
|
1461
|
+
|
|
1462
|
+
// Large rock boulder for thinker
|
|
1463
|
+
ctx.fillStyle = '#334155';
|
|
1464
|
+
ctx.beginPath();
|
|
1465
|
+
ctx.ellipse(cx - 140, cy + 175, 85, 55, 0, 0, Math.PI * 2);
|
|
1466
|
+
ctx.fill();
|
|
1467
|
+
|
|
1468
|
+
// Thinker seated on rock
|
|
1469
|
+
drawDoodleCharacter({
|
|
1470
|
+
x: cx - 140,
|
|
1471
|
+
y: cy + 130,
|
|
1472
|
+
scale: 1.35,
|
|
1473
|
+
hairStyle: 'wild',
|
|
1474
|
+
expression: 'talking',
|
|
1475
|
+
clothing: 'pelt',
|
|
1476
|
+
armPose: 'thinker',
|
|
1477
|
+
seed: 108,
|
|
1478
|
+
});
|
|
1479
|
+
|
|
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);
|
|
1487
|
+
|
|
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');
|
|
1491
|
+
|
|
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();
|
|
1499
|
+
|
|
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();
|
|
1506
|
+
|
|
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);
|
|
1510
|
+
|
|
1511
|
+
ctx.fillStyle = '#b45309';
|
|
1512
|
+
ctx.fillRect(-28, -20, 56, 32);
|
|
1513
|
+
|
|
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
|
+
}
|
|
1521
|
+
|
|
1522
|
+
} else if (COMP_THEME === 'doodle_microscope_lice') {
|
|
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
|
|
1530
|
+
drawDoodleCharacter({
|
|
1531
|
+
x: cx - 240,
|
|
1532
|
+
y: cy + 140,
|
|
1533
|
+
scale: 1.35,
|
|
1534
|
+
hairStyle: 'wild',
|
|
1535
|
+
expression: 'shivering',
|
|
1536
|
+
clothing: 'pelt',
|
|
1537
|
+
armPose: 'shiver',
|
|
1538
|
+
seed: 109,
|
|
1539
|
+
});
|
|
1540
|
+
|
|
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);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
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;
|
|
1552
|
+
|
|
1553
|
+
ctx.save();
|
|
1554
|
+
ctx.translate(lensX, lensY);
|
|
1555
|
+
ctx.scale(scopeSpring, scopeSpring);
|
|
1556
|
+
|
|
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();
|
|
1564
|
+
|
|
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
|
+
}
|
|
1594
|
+
|
|
1595
|
+
} else if (COMP_THEME === 'doodle_general_human') {
|
|
1596
|
+
// General Expressive Doodled Human Narrator
|
|
1597
|
+
sLine(cx - W * 0.4, cy + 220, cx + W * 0.4, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1598
|
+
drawDoodleCharacter({
|
|
1599
|
+
x: cx,
|
|
1600
|
+
y: cy + 140,
|
|
1601
|
+
scale: 1.4,
|
|
1602
|
+
hairStyle: 'wild',
|
|
1603
|
+
expression: 'talking',
|
|
1604
|
+
clothing: 'pelt',
|
|
1605
|
+
armPose: 'natural',
|
|
1606
|
+
seed: 110,
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
} else if (COMP_THEME === 'celestial_orbit') {
|
|
698
1610
|
// 3D Celestial Body (Earth) with orbiting satellites & signal pulses
|
|
699
1611
|
const earthR = Math.min(W, H) * 0.22;
|
|
700
1612
|
|
|
@@ -1168,49 +2080,70 @@ function drawMainScene(t) {
|
|
|
1168
2080
|
ctx.restore();
|
|
1169
2081
|
}
|
|
1170
2082
|
|
|
1171
|
-
// ──
|
|
2083
|
+
// ── Sentence-Synchronized Broadcast Subtitles Overlay ───────────────────────
|
|
1172
2084
|
function drawCinematicOverlay(t) {
|
|
1173
2085
|
ctx.save();
|
|
1174
|
-
const textSpring = spring(t, 0.12, 190, 15);
|
|
1175
|
-
const headlineAlpha = Math.min(1, Math.max(0, textSpring));
|
|
1176
|
-
ctx.globalAlpha = headlineAlpha;
|
|
1177
2086
|
|
|
1178
|
-
//
|
|
1179
|
-
|
|
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);
|
|
1180
2091
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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;
|
|
1190
2102
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
|
|
1195
|
-
ctx.shadowBlur = 24;
|
|
1196
|
-
ctx.shadowOffsetY = 4;
|
|
1197
|
-
ctx.textAlign = 'left';
|
|
1198
|
-
const cleanCap = CAPTION.length > 60 ? CAPTION.slice(0, 60) + '...' : CAPTION;
|
|
1199
|
-
ctx.fillText(cleanCap, 80, capY + 30);
|
|
1200
|
-
|
|
1201
|
-
// Subtitle / Spoken Narration - bottom center clean broadcast subtitles
|
|
1202
|
-
if (NARRATION) {
|
|
1203
|
-
const subSpring = spring(t, 0.22, 180, 16);
|
|
1204
|
-
ctx.globalAlpha = Math.min(1, Math.max(0, subSpring));
|
|
1205
|
-
ctx.font = '500 28px ' + FONT_BODY;
|
|
1206
|
-
ctx.fillStyle = '#f8fafc';
|
|
1207
|
-
ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
|
|
1208
|
-
ctx.shadowBlur = 18;
|
|
1209
|
-
ctx.shadowOffsetY = 3;
|
|
2103
|
+
ctx.save();
|
|
2104
|
+
ctx.globalAlpha = sentAlpha;
|
|
2105
|
+
ctx.font = '500 32px ' + FONT_BODY;
|
|
1210
2106
|
ctx.textAlign = 'center';
|
|
1211
|
-
|
|
1212
|
-
|
|
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();
|
|
1213
2145
|
}
|
|
2146
|
+
|
|
1214
2147
|
ctx.restore();
|
|
1215
2148
|
|
|
1216
2149
|
// Bottom Progress Indicator (minimal line)
|
|
@@ -1265,10 +2198,11 @@ AVAILABLE HELPERS:
|
|
|
1265
2198
|
- easeIn(t), easeOut(t), easeInOut(t), boilSeed(t, seed), hash(i)
|
|
1266
2199
|
RULES:
|
|
1267
2200
|
1. Every element must animate smoothly over time t [0..DURATION].
|
|
1268
|
-
2.
|
|
1269
|
-
3.
|
|
1270
|
-
4.
|
|
1271
|
-
5.
|
|
2201
|
+
2. If narration, key phrase, or visual direction involves human actions, history, emotions, characters, or daily life (e.g. choosing clothes, shivering in cold, sewing hides, meeting another tribe, mirror reflection, thinking by fire), ALWAYS animate expressive hand-drawn doodled human figures actively performing those actions (limbs, head, gestures, facial expressions, setting props) rather than sterile abstract diagrams.
|
|
2202
|
+
3. Fill the canvas with rich visual geometry, fluid strokes, animated characters, particles, or cinematic diagrams matching the prompt.
|
|
2203
|
+
4. NEVER use Math.random(), setTimeout, setInterval, Date.now(), performance.now(). All motion must be a pure mathematical function of time t.
|
|
2204
|
+
5. Output ONLY raw executable JavaScript code for the body of window.seek = async (t) => { ... }.
|
|
2205
|
+
6. NO markdown backticks, NO explanations. Start directly with JavaScript.`;
|
|
1272
2206
|
|
|
1273
2207
|
const user = `TOPIC: ${topic || 'Visual Explainer'}
|
|
1274
2208
|
VISUAL DIRECTION: ${visualDescription || caption || 'Dynamic motion graphics'}
|