@amg555/frameforge-mcp 2.4.0 → 2.4.2

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 CHANGED
@@ -4,6 +4,31 @@ All notable changes to FrameForge are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## [2.4.2] — 2026-11-09
8
+
9
+ ### Added
10
+ - **Procedural Doodled Character Animation Engine (`src/video-renderer/scene-animator.js`)**:
11
+ - 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).
12
+ - 12fps hand-drawn boiling wobble (`wb(seed, mag)`) and organic sketchy vector primitives (`sLine`, `sCircle`, `sRect`) conforming to the pure-$t$ rendering contract.
13
+ - 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`.
14
+ - Action-grounded theme detection in `detectComponentTheme`: Human narrative, evolution, psychology, and daily action scripts automatically map to physical character animations rather than abstract geometry.
15
+ - Enhanced LLM prompt in `generateSceneCodeWithLLM` directing AI models to animate expressive human figures actively performing narrated actions.
16
+
17
+ ---
18
+
19
+ ## [2.4.1] — 2026-11-09
20
+
21
+ ### Fixed
22
+ - **CLI & npx Invocation Handling (`src/mcp-server/stdio.js`, `package.json`)**:
23
+ - Registered `frameforge-make` binary alias in `package.json` so `npx --package=@amg555/frameforge-mcp frameforge-make` works immediately.
24
+ - Added CLI dispatch interception in `stdio.js` when flags or subcommands (`make`, `--topic`, `doctor`, `setup`, `harness`) are passed, preventing npx CLI invocations from hanging on stdio JSON-RPC stdin.
25
+ - **Language & Orientation Forwarding in CLI (`src/cli/make.js`)**:
26
+ - Added `--orientation <landscape|portrait>` and `--portrait` flags.
27
+ - Forwarded `language` and auto-selected `voice` (`ml-IN-MidhunNeural`, `hi-IN-MadhurNeural`, etc.) to `runAssets` and `runRender`.
28
+ - Forwarded `renderWidth` and `renderHeight` to `runRender` so canvas dimensions match requested orientation.
29
+
30
+ ---
31
+
7
32
  ## [2.4.0] — 2026-11-09
8
33
 
9
34
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amg555/frameforge-mcp",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
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"
@@ -8,6 +8,7 @@
8
8
  "main": "src/mcp-server/server.js",
9
9
  "bin": {
10
10
  "frameforge": "src/cli/make.js",
11
+ "frameforge-make": "src/cli/make.js",
11
12
  "frameforge-mcp": "src/mcp-server/stdio.js",
12
13
  "frameforge-setup": "src/cli/setup-keys.js",
13
14
  "frameforge-doctor": "src/cli/doctor.js",
@@ -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.0', code.includes("version: '2.4.0'"));
23
+ check('SERVER_INFO version 2.4.2', code.includes("version: '2.4.2'"));
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.0', cfWorker.includes("'2.4.0'"));
302
+ check('cf-worker version 2.4.2', cfWorker.includes("'2.4.2'"));
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/cli/make.js CHANGED
@@ -98,6 +98,7 @@ function parseArgs(argv) {
98
98
  mode: 'full-auto',
99
99
  topic: '', topicsFile: '', language: 'en',
100
100
  style: 'educational comedy', channels: ['file'],
101
+ orientation: 'landscape',
101
102
  scenesFile: '', scriptFile: '', mediaDir: '',
102
103
  shorts: true, dryRun: false,
103
104
  noResearch: false, noScript: false, noImages: false, noTts: false, noPublish: false,
@@ -112,6 +113,8 @@ function parseArgs(argv) {
112
113
  case '--script-file': args.scriptFile = argv[++i] || ''; break;
113
114
  case '--media-dir': args.mediaDir = argv[++i] || ''; break;
114
115
  case '--language': args.language = (argv[++i] || 'en').toLowerCase(); break;
116
+ case '--orientation': args.orientation = (argv[++i] || 'landscape').toLowerCase(); break;
117
+ case '--portrait': args.orientation = 'portrait'; break;
115
118
  case '--style': args.style = argv[++i] || args.style; break;
116
119
  case '--channels': args.channels = (argv[++i] || 'file').split(',').map(s => s.trim()); break;
117
120
  case '--no-research': args.noResearch = true; break;
@@ -399,10 +402,13 @@ async function runSingleVideo(args, topic, outDir, workflows) {
399
402
  // Pass stage-skip flags into scenes so runAssets can honour them
400
403
  const preparedScenes = scenes.map(s => ({
401
404
  ...s,
405
+ language: s.language || args.language,
402
406
  _skipImageGen: skipImages,
403
407
  _skipTts: args.noTts,
404
408
  }));
405
409
 
410
+ const resolvedVoice = LANGUAGE_VOICES[args.language] || null;
411
+
406
412
  const assetsResult = await runAssets({
407
413
  productionId,
408
414
  scenes: preparedScenes,
@@ -413,17 +419,25 @@ async function runSingleVideo(args, topic, outDir, workflows) {
413
419
  topic,
414
420
  title,
415
421
  style: args.style || '',
422
+ language: args.language,
423
+ voice: resolvedVoice,
416
424
  });
417
425
  log('Assets', `✅ ${assetsResult.sceneCount} scenes prepared`);
418
426
 
419
427
  // ── Stage D: Render ────────────────────────────────────────────────────────
420
428
  logSection('Stage D: Video Render (FFmpeg)');
429
+ const renderWidth = args.orientation === 'portrait' ? 1080 : 1920;
430
+ const renderHeight = args.orientation === 'portrait' ? 1920 : 1080;
431
+
421
432
  const renderResult = await runRender({
422
433
  productionId,
423
434
  sceneAssets: assetsResult.sceneAssets,
424
435
  topic,
425
436
  title,
426
437
  style: args.style || '',
438
+ language: args.language,
439
+ renderWidth,
440
+ renderHeight,
427
441
  });
428
442
  if (!renderResult.passed) {
429
443
  throw new Error(`Quality gate FAILED: ${JSON.stringify(renderResult.qcReport?.checks?.filter(c => !c.passed).map(c => c.name))}`);
@@ -1633,7 +1633,7 @@ Allows the agent in Claude Code, Cursor, or any IDE to inspect the visual output
1633
1633
 
1634
1634
  const SERVER_INFO = {
1635
1635
  name: 'video-pipeline',
1636
- version: '2.4.0',
1636
+ version: '2.4.2',
1637
1637
  };
1638
1638
 
1639
1639
  const CAPABILITIES = {
@@ -34,6 +34,34 @@
34
34
  // Load config BEFORE requiring server.js so env vars are set when providers initialise
35
35
  require('../config/load-config').loadConfig();
36
36
 
37
+ // ── CLI Dispatch Fallback ──────────────────────────────────────────────────
38
+ // When run via `npx @amg555/frameforge-mcp` with CLI arguments (e.g. `make`, `--topic`,
39
+ // `doctor`, `setup`, `harness`, or `--help`), dispatch to the CLI tool instead of
40
+ // hanging on stdin JSON-RPC.
41
+ const firstArg = process.argv[2];
42
+ if (firstArg && (firstArg === 'make' || firstArg.startsWith('-') || firstArg === 'doctor' || firstArg === 'setup' || firstArg === 'harness')) {
43
+ if (firstArg === 'doctor') {
44
+ require('../cli/doctor');
45
+ return;
46
+ }
47
+ if (firstArg === 'setup') {
48
+ require('../cli/setup-keys');
49
+ return;
50
+ }
51
+ if (firstArg === 'harness') {
52
+ const harnessBin = require('path').resolve(__dirname, '../harness/bin/harness.mjs');
53
+ const { spawnSync } = require('child_process');
54
+ const res = spawnSync(process.execPath, [harnessBin, ...process.argv.slice(3)], { stdio: 'inherit' });
55
+ process.exit(res.status ?? 0);
56
+ }
57
+ // Otherwise dispatch to make.js (stripping 'make' if present)
58
+ if (firstArg === 'make') {
59
+ process.argv.splice(2, 1);
60
+ }
61
+ require('../cli/make');
62
+ return;
63
+ }
64
+
37
65
  const readline = require('readline');
38
66
  const { handleMCPRequest } = require('./server');
39
67
 
@@ -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 (170,000 Years Lice Proof)
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. Dressing / Wardrobe / Wearing Clothes / Outfit / Morning Routine / Choosing clothes
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)
284
+ if (/(?:^|[^\w])(shiver|shivering|cold|freeze|freezing|ice|winter|teeth|chatter|protection flaw)(?:[^\w]|$)/i.test(text)
285
+ || /തണുപ്പ്|വിറയ്ക്കുക|തണുപ്പിൽ|തണുപ്പിൽ നിന്ന്|പരിക്കു|തള്ളിക്കളയുന്നു|ശരീരത്തെ സംരക്ഷിക്കാൻ|ശരീര സംരക്ഷണം|സാധാരണ ധാരണകൾ/.test(text)) {
286
+ return 'doodle_shiver_cold';
287
+ }
288
+
289
+ // 4. Meeting Tribes / Social Language / Status / Community / Strangers
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
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. Hunter in Savannah / Wild / Blazing Sun / Spear / Mosquitoes / Naked in Jungle
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
308
+ 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
+ || /സൂചി|സൂചിയും|തുന്ന|തുന്നാൻ|തോൽ തുന്നാൻ|തുന്നൽ|തോൽ തുന്നാനുള്ള/.test(text)) {
310
+ return 'doodle_sewing_needle';
311
+ }
312
+
313
+ // 8. Shame / Mirror / Reflection / Water / Self Awareness / Theory of Mind
314
+ 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
+ || /നാണം|നാണം മറയ്ക്കാൻ|കണ്ണാടി|പ്രതിഫലനം|മറ്റുള്ളവർ എന്നെ|തിയറി ഓഫ് മൈൻഡ്|സ്വന്തം ശരീരം മറച്ചു|വ്യക്തിത്വം|ശ്രദ്ധിക്കുന്നുണ്ട്|യഥാർത്ഥ രഹസ്യം|മനഃശാസ്ത്ര/.test(text)) {
316
+ return 'doodle_shame_reflection';
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)) {
@@ -694,7 +749,760 @@ function drawMainScene(t) {
694
749
  const cx = W / 2;
695
750
  const cy = H * 0.50;
696
751
 
697
- if (COMP_THEME === 'celestial_orbit') {
752
+ // ── Hand-Drawn Boil Wobble & Sketch Helpers (12fps Hand-Drawn Animation) ──
753
+ function wb(seed, mag = 2.4) {
754
+ const step = Math.floor(t * 12);
755
+ return Math.sin(seed * 19.37 + step * 2.41) * mag;
756
+ }
757
+
758
+ function sLine(x1, y1, x2, y2, seed, color = '#f8fafc', width = 3.5) {
759
+ ctx.save();
760
+ ctx.strokeStyle = color;
761
+ ctx.lineWidth = width;
762
+ ctx.lineCap = 'round';
763
+ ctx.lineJoin = 'round';
764
+ const mx = (x1 + x2) / 2 + wb(seed, 3.5);
765
+ const my = (y1 + y2) / 2 + wb(seed + 1, 3.5);
766
+ ctx.beginPath();
767
+ ctx.moveTo(x1 + wb(seed + 2, 1.5), y1 + wb(seed + 3, 1.5));
768
+ ctx.quadraticCurveTo(mx, my, x2 + wb(seed + 4, 1.5), y2 + wb(seed + 5, 1.5));
769
+ ctx.stroke();
770
+ ctx.restore();
771
+ }
772
+
773
+ function sCircle(x, y, r, seed, color = '#f8fafc', width = 3, fillColor = null) {
774
+ ctx.save();
775
+ if (fillColor) {
776
+ ctx.fillStyle = fillColor;
777
+ ctx.beginPath();
778
+ ctx.arc(x, y, r, 0, Math.PI * 2);
779
+ ctx.fill();
780
+ }
781
+ ctx.strokeStyle = color;
782
+ ctx.lineWidth = width;
783
+ ctx.beginPath();
784
+ const segs = 10;
785
+ for (let s = 0; s <= segs; s++) {
786
+ const a = (s / segs) * Math.PI * 2;
787
+ const curR = r + wb(seed + s, 2.5);
788
+ const px = x + Math.cos(a) * curR;
789
+ const py = y + Math.sin(a) * curR;
790
+ if (s === 0) ctx.moveTo(px, py);
791
+ else ctx.lineTo(px, py);
792
+ }
793
+ ctx.closePath();
794
+ ctx.stroke();
795
+ ctx.restore();
796
+ }
797
+
798
+ function sRect(x, y, w, h, seed, color = '#f8fafc', width = 3, fillColor = null) {
799
+ ctx.save();
800
+ if (fillColor) {
801
+ ctx.fillStyle = fillColor;
802
+ ctx.fillRect(x, y, w, h);
803
+ }
804
+ sLine(x, y, x + w, y, seed, color, width);
805
+ sLine(x + w, y, x + w, y + h, seed + 1, color, width);
806
+ sLine(x + w, y + h, x, y + h, seed + 2, color, width);
807
+ sLine(x, y + h, x, y, seed + 3, color, width);
808
+ ctx.restore();
809
+ }
810
+
811
+ function drawDoodleCharacter({
812
+ x = cx,
813
+ y = cy,
814
+ scale = 1.0,
815
+ headRadius = 38,
816
+ torsoHeight = 90,
817
+ legLength = 95,
818
+ hairStyle = 'wild',
819
+ expression = 'talking',
820
+ clothing = 'none',
821
+ armPose = 'natural',
822
+ seed = 100,
823
+ } = {}) {
824
+ ctx.save();
825
+ ctx.translate(x, y);
826
+ ctx.scale(scale, scale);
827
+
828
+ const blink = (t % 3.2 < 0.14) ? 0.15 : 1.0;
829
+ const mouthTalk = Math.abs(Math.sin(t * 14)) * 4;
830
+ const torsoTopY = -torsoHeight;
831
+ const torsoBottomY = 0;
832
+
833
+ // Torso Clothing Fills
834
+ if (clothing === 'pelt') {
835
+ ctx.fillStyle = '#b45309';
836
+ ctx.beginPath();
837
+ ctx.moveTo(-24 + wb(seed, 2), torsoTopY);
838
+ ctx.lineTo(24 + wb(seed + 1, 2), torsoTopY);
839
+ ctx.lineTo(32 + wb(seed + 2, 2), torsoBottomY + 28);
840
+ ctx.lineTo(-28 + wb(seed + 3, 2), torsoBottomY + 28);
841
+ ctx.closePath();
842
+ ctx.fill();
843
+ sLine(-28, torsoBottomY + 2, 32, torsoBottomY + 2, seed + 4, '#78350f', 4);
844
+ } else if (clothing === 'shirt') {
845
+ ctx.fillStyle = ACCENT;
846
+ ctx.beginPath();
847
+ ctx.moveTo(-26 + wb(seed, 2), torsoTopY);
848
+ ctx.lineTo(26 + wb(seed + 1, 2), torsoTopY);
849
+ ctx.lineTo(28 + wb(seed + 2, 2), torsoBottomY + 12);
850
+ ctx.lineTo(-28 + wb(seed + 3, 2), torsoBottomY + 12);
851
+ ctx.closePath();
852
+ ctx.fill();
853
+ sLine(-14, torsoTopY, 0, torsoTopY + 18, seed + 4, '#ffffff', 3);
854
+ sLine(14, torsoTopY, 0, torsoTopY + 18, seed + 5, '#ffffff', 3);
855
+ sLine(0, torsoTopY + 18, 0, torsoBottomY + 10, seed + 6, '#ffffff', 2.5);
856
+ } else if (clothing === 'cloak') {
857
+ ctx.fillStyle = '#0f766e';
858
+ ctx.beginPath();
859
+ ctx.moveTo(-32, torsoTopY - 6);
860
+ ctx.lineTo(32, torsoTopY - 6);
861
+ ctx.lineTo(44, torsoBottomY + 32);
862
+ ctx.lineTo(-44, torsoBottomY + 32);
863
+ ctx.closePath();
864
+ ctx.fill();
865
+ } else {
866
+ sLine(0, torsoTopY, 0, torsoBottomY, seed + 7, '#fcd34d', 4);
867
+ sLine(-15, torsoTopY + 30, -2, torsoTopY + 40, seed + 8, 'rgba(255,255,255,0.3)', 2);
868
+ sLine(15, torsoTopY + 30, 2, torsoTopY + 40, seed + 9, 'rgba(255,255,255,0.3)', 2);
869
+ }
870
+ sLine(0, torsoTopY, 0, torsoBottomY, seed + 10, '#f8fafc', 4);
871
+
872
+ // Head
873
+ const headCenterY = torsoTopY - headRadius - 6 + wb(seed + 11, 1.5);
874
+ sCircle(0, headCenterY, headRadius, seed + 12, '#f8fafc', 3.5, '#fef3c7');
875
+
876
+ // Hair
877
+ if (hairStyle === 'wild') {
878
+ for (let h = 0; h < 6; h++) {
879
+ const hairAng = -Math.PI * 0.85 + (h / 5) * Math.PI * 0.7;
880
+ const hx1 = Math.cos(hairAng) * headRadius;
881
+ const hy1 = headCenterY + Math.sin(hairAng) * headRadius;
882
+ const hx2 = Math.cos(hairAng) * (headRadius + 22 + wb(seed + 13 + h, 4));
883
+ const hy2 = headCenterY + Math.sin(hairAng) * (headRadius + 22 + wb(seed + 14 + h, 4));
884
+ sLine(hx1, hy1, hx2, hy2, seed + 15 + h, '#78350f', 3.5);
885
+ }
886
+ } else if (hairStyle === 'modern') {
887
+ ctx.fillStyle = '#1e293b';
888
+ ctx.beginPath();
889
+ ctx.arc(0, headCenterY - 12, headRadius * 0.96, Math.PI, Math.PI * 2);
890
+ ctx.closePath();
891
+ ctx.fill();
892
+ sLine(-headRadius * 0.9, headCenterY - 12, headRadius * 0.9, headCenterY - 12, seed + 20, '#1e293b', 4);
893
+ } else if (hairStyle === 'feather') {
894
+ sLine(8, headCenterY - headRadius, 28, headCenterY - headRadius - 35, seed + 21, '#38bdf8', 4);
895
+ sLine(28, headCenterY - headRadius - 35, 18, headCenterY - headRadius - 20, seed + 22, '#f43f5e', 3);
896
+ }
897
+
898
+ // Eyes
899
+ const eyeY = headCenterY + 4;
900
+ const eyeR = 4 * blink;
901
+ sCircle(-12, eyeY, Math.max(1, eyeR), seed + 23, '#0f172a', 2, '#0f172a');
902
+ sCircle(12, eyeY, Math.max(1, eyeR), seed + 24, '#0f172a', 2, '#0f172a');
903
+
904
+ // Eyebrows
905
+ if (expression === 'shivering') {
906
+ sLine(-18, eyeY - 12 + wb(seed, 2), -6, eyeY - 7 + wb(seed, 2), seed + 25, '#0f172a', 2.5);
907
+ sLine(6, eyeY - 7 + wb(seed, 2), 18, eyeY - 12 + wb(seed, 2), seed + 26, '#0f172a', 2.5);
908
+ } else if (expression === 'surprised' || expression === 'curious') {
909
+ sLine(-18, eyeY - 15, -6, eyeY - 13, seed + 27, '#0f172a', 2.5);
910
+ sLine(6, eyeY - 13, 18, eyeY - 15, seed + 28, '#0f172a', 2.5);
911
+ } else {
912
+ sLine(-17, eyeY - 9, -7, eyeY - 10, seed + 29, '#0f172a', 2.5);
913
+ sLine(7, eyeY - 10, 17, eyeY - 9, seed + 30, '#0f172a', 2.5);
914
+ }
915
+
916
+ // Mouth
917
+ const mouthY = headCenterY + 22;
918
+ if (expression === 'shivering') {
919
+ const mChatter = Math.sin(t * 32) * 2.5;
920
+ sLine(-10, mouthY + mChatter, -4, mouthY - 3 + mChatter, seed + 31, '#0f172a', 2.5);
921
+ sLine(-4, mouthY - 3 + mChatter, 3, mouthY + 3 + mChatter, seed + 32, '#0f172a', 2.5);
922
+ sLine(3, mouthY + 3 + mChatter, 10, mouthY + mChatter, seed + 33, '#0f172a', 2.5);
923
+ } else if (expression === 'talking') {
924
+ ctx.fillStyle = '#0f172a';
925
+ ctx.beginPath();
926
+ ctx.ellipse(0, mouthY, 7, Math.max(2, mouthTalk), 0, 0, Math.PI * 2);
927
+ ctx.fill();
928
+ } else if (expression === 'smiling') {
929
+ ctx.beginPath();
930
+ ctx.arc(0, mouthY - 4, 10, 0.2 * Math.PI, 0.8 * Math.PI);
931
+ ctx.strokeStyle = '#0f172a';
932
+ ctx.lineWidth = 2.5;
933
+ ctx.stroke();
934
+ } else {
935
+ sLine(-8, mouthY, 8, mouthY, seed + 34, '#0f172a', 2.5);
936
+ }
937
+
938
+ // Legs
939
+ const hipY = torsoBottomY;
940
+ if (armPose === 'sewing' || armPose === 'thinker') {
941
+ sLine(-10, hipY, -45, hipY + 45, seed + 35, '#f8fafc', 4);
942
+ sLine(-45, hipY + 45, 10, hipY + 50, seed + 36, '#f8fafc', 4);
943
+ sLine(10, hipY, 45, hipY + 45, seed + 37, '#f8fafc', 4);
944
+ sLine(45, hipY + 45, -10, hipY + 50, seed + 38, '#f8fafc', 4);
945
+ } else if (armPose === 'hold_spear') {
946
+ const legSwing = Math.sin(t * 5.5);
947
+ const l1KneeX = -15 + legSwing * 22;
948
+ const l1KneeY = hipY + legLength * 0.55;
949
+ const l1FootX = -15 + legSwing * 38;
950
+ const l1FootY = hipY + legLength;
951
+ sLine(-10, hipY, l1KneeX, l1KneeY, seed + 39, '#f8fafc', 4);
952
+ sLine(l1KneeX, l1KneeY, l1FootX, l1FootY, seed + 40, '#f8fafc', 4);
953
+
954
+ const l2KneeX = 15 - legSwing * 22;
955
+ const l2KneeY = hipY + legLength * 0.55;
956
+ const l2FootX = 15 - legSwing * 38;
957
+ const l2FootY = hipY + legLength;
958
+ sLine(10, hipY, l2KneeX, l2KneeY, seed + 41, '#f8fafc', 4);
959
+ sLine(l2KneeX, l2KneeY, l2FootX, l2FootY, seed + 42, '#f8fafc', 4);
960
+ } else {
961
+ sLine(-12, hipY, -14, hipY + legLength, seed + 43, '#f8fafc', 4);
962
+ sLine(12, hipY, 14, hipY + legLength, seed + 44, '#f8fafc', 4);
963
+ sLine(-14, hipY + legLength, -24, hipY + legLength, seed + 45, '#f8fafc', 3.5);
964
+ sLine(14, hipY + legLength, 24, hipY + legLength, seed + 46, '#f8fafc', 3.5);
965
+ }
966
+
967
+ // Arms & Hands
968
+ const shoulderY = torsoTopY + 12;
969
+ if (armPose === 'shiver') {
970
+ const shivA = Math.sin(t * 34) * 3;
971
+ sLine(-20, shoulderY, 16 + shivA, shoulderY + 35, seed + 47, '#f8fafc', 4);
972
+ sLine(20, shoulderY, -16 + shivA, shoulderY + 35, seed + 48, '#f8fafc', 4);
973
+ } else if (armPose === 'hold_spear') {
974
+ const spearX = -45;
975
+ sLine(-18, shoulderY, -35, shoulderY + 30, seed + 49, '#f8fafc', 4);
976
+ sLine(-35, shoulderY + 30, spearX, shoulderY + 5, seed + 50, '#f8fafc', 4);
977
+ sLine(spearX, -torsoHeight - 130, spearX, hipY + legLength + 10, seed + 51, '#92400e', 5);
978
+ ctx.fillStyle = '#64748b';
979
+ ctx.beginPath();
980
+ ctx.moveTo(spearX, -torsoHeight - 165);
981
+ ctx.lineTo(spearX - 10, -torsoHeight - 128);
982
+ ctx.lineTo(spearX + 10, -torsoHeight - 128);
983
+ ctx.closePath();
984
+ ctx.fill();
985
+ sLine(18, shoulderY, 35, shoulderY + 25 + Math.sin(t * 5.5) * 20, seed + 52, '#f8fafc', 4);
986
+ sLine(35, shoulderY + 25 + Math.sin(t * 5.5) * 20, 48, shoulderY + 45, seed + 53, '#f8fafc', 4);
987
+ } else if (armPose === 'sewing') {
988
+ sLine(-18, shoulderY, -35, shoulderY + 40, seed + 54, '#f8fafc', 4);
989
+ sLine(-35, shoulderY + 40, -18, shoulderY + 55, seed + 55, '#f8fafc', 4);
990
+ const sewCycle = Math.sin(t * 4);
991
+ const handX = 20 + sewCycle * 25;
992
+ const handY = shoulderY + 45 - Math.cos(t * 4) * 15;
993
+ sLine(18, shoulderY, 38, shoulderY + 25, seed + 56, '#f8fafc', 4);
994
+ sLine(38, shoulderY + 25, handX, handY, seed + 57, '#f8fafc', 4);
995
+ sLine(handX - 5, handY - 10, handX + 18, handY + 12, seed + 58, '#fef08a', 3.5);
996
+ ctx.strokeStyle = '#ffffff';
997
+ ctx.lineWidth = 1.5;
998
+ ctx.beginPath();
999
+ ctx.moveTo(handX - 5, handY - 10);
1000
+ ctx.quadraticCurveTo(handX - 25, handY - 25, 0, hipY + 10);
1001
+ ctx.stroke();
1002
+ } else if (armPose === 'cover_body') {
1003
+ sLine(-18, shoulderY, -12, hipY - 10, seed + 59, '#f8fafc', 4);
1004
+ sLine(18, shoulderY, 12, hipY - 10, seed + 60, '#f8fafc', 4);
1005
+ } else if (armPose === 'dress_shirt') {
1006
+ sLine(-18, shoulderY, -38, shoulderY + 30, seed + 61, '#f8fafc', 4);
1007
+ sLine(-38, shoulderY + 30, -22, hipY - 5, seed + 62, '#f8fafc', 4);
1008
+ const adjustY = shoulderY + 8 + Math.sin(t * 3) * 6;
1009
+ sLine(18, shoulderY, 35, shoulderY + 15, seed + 63, '#f8fafc', 4);
1010
+ sLine(35, shoulderY + 15, 10, adjustY, seed + 64, '#f8fafc', 4);
1011
+ } else if (armPose === 'peace_gesture') {
1012
+ sLine(-18, shoulderY, -26, shoulderY + 40, seed + 65, '#f8fafc', 4);
1013
+ sLine(18, shoulderY, 40, shoulderY - 10 + Math.sin(t * 2) * 5, seed + 66, '#f8fafc', 4);
1014
+ sLine(40, shoulderY - 10 + Math.sin(t * 2) * 5, 45, shoulderY - 35, seed + 67, '#f8fafc', 4);
1015
+ } else if (armPose === 'thinker') {
1016
+ sLine(18, shoulderY, 36, hipY + 15, seed + 68, '#f8fafc', 4);
1017
+ sLine(36, hipY + 15, 8, headCenterY + 18, seed + 69, '#f8fafc', 4);
1018
+ sLine(-18, shoulderY, -30, hipY + 25, seed + 70, '#f8fafc', 4);
1019
+ } else {
1020
+ const gArm = Math.sin(t * 2.5) * 12;
1021
+ sLine(-18, shoulderY, -35, shoulderY + 35, seed + 71, '#f8fafc', 4);
1022
+ sLine(-35, shoulderY + 35, -42, shoulderY + 60, seed + 72, '#f8fafc', 4);
1023
+ sLine(18, shoulderY, 35, shoulderY + 20 - gArm, seed + 73, '#f8fafc', 4);
1024
+ sLine(35, shoulderY + 20 - gArm, 45, shoulderY + 5 - gArm, seed + 74, '#f8fafc', 4);
1025
+ }
1026
+
1027
+ ctx.restore();
1028
+ }
1029
+
1030
+ // ── Doodled Character Narrative Themes ────────────────────────────────────
1031
+ if (COMP_THEME === 'doodle_dressing') {
1032
+ // Modern Dressing Room & Mirror Reflection
1033
+ sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.25)', 2.5);
1034
+
1035
+ // Clothes rack on left
1036
+ sLine(cx - 360, cy + 220, cx - 360, cy - 140, 11, '#94a3b8', 5);
1037
+ sLine(cx - 410, cy + 220, cx - 310, cy + 220, 12, '#94a3b8', 5);
1038
+ sLine(cx - 420, cy - 140, cx - 180, cy - 140, 13, '#cbd5e1', 5);
1039
+
1040
+ // 3 shirts on hangers
1041
+ const shirtColors = ['#38bdf8', '#f43f5e', '#fbbf24'];
1042
+ for (let c = 0; c < 3; c++) {
1043
+ const hx = cx - 380 + c * 80;
1044
+ const sway = Math.sin(t * 2.5 + c * 1.5) * 5;
1045
+ sLine(hx, cy - 140, hx + sway, cy - 110, 20 + c, '#e2e8f0', 2.5);
1046
+ ctx.fillStyle = shirtColors[c];
1047
+ ctx.beginPath();
1048
+ ctx.moveTo(hx + sway - 22, cy - 110);
1049
+ ctx.lineTo(hx + sway + 22, cy - 110);
1050
+ ctx.lineTo(hx + sway + 28, cy - 10);
1051
+ ctx.lineTo(hx + sway - 28, cy - 10);
1052
+ ctx.closePath();
1053
+ ctx.fill();
1054
+ }
1055
+
1056
+ // Mirror on right
1057
+ const mirrorX = cx + 320;
1058
+ const mirrorY = cy + 20;
1059
+ ctx.fillStyle = 'rgba(56, 189, 248, 0.08)';
1060
+ ctx.beginPath();
1061
+ ctx.roundRect(mirrorX - 85, mirrorY - 210, 170, 390, 24);
1062
+ ctx.fill();
1063
+ ctx.strokeStyle = '#d4af37';
1064
+ ctx.lineWidth = 4;
1065
+ ctx.stroke();
1066
+
1067
+ // Reflected character in mirror (faint glow)
1068
+ ctx.save();
1069
+ ctx.globalAlpha = 0.55;
1070
+ drawDoodleCharacter({
1071
+ x: mirrorX,
1072
+ y: cy + 120,
1073
+ scale: 1.05,
1074
+ hairStyle: 'modern',
1075
+ expression: 'smiling',
1076
+ clothing: 'shirt',
1077
+ armPose: 'dress_shirt',
1078
+ seed: 88,
1079
+ });
1080
+ ctx.restore();
1081
+
1082
+ // Main character in center
1083
+ drawDoodleCharacter({
1084
+ x: cx - 40,
1085
+ y: cy + 140,
1086
+ scale: 1.35,
1087
+ hairStyle: 'modern',
1088
+ expression: 'talking',
1089
+ clothing: 'shirt',
1090
+ armPose: 'dress_shirt',
1091
+ seed: 101,
1092
+ });
1093
+
1094
+ } else if (COMP_THEME === 'doodle_shiver_cold') {
1095
+ // Freezing Wind & Shivering Early Human
1096
+ sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
1097
+
1098
+ // Freezing wind streaks rushing across
1099
+ for (let w = 0; w < 12; w++) {
1100
+ const windX = ((t * 700 + w * 220) % (W + 600)) - 300;
1101
+ const windY = cy - 250 + (w * 45) + Math.sin(t * 3 + w) * 15;
1102
+ sLine(windX, windY, windX + 160, windY + 5, 200 + w, 'rgba(255,255,255,0.3)', 2);
1103
+ }
1104
+
1105
+ // Drifting snow particles
1106
+ for (let p = 0; p < 25; p++) {
1107
+ const px = ((p * 85 + t * 450) % W);
1108
+ const py = (p * 45 + t * 90) % H;
1109
+ sCircle(px, py, 2.5, 300 + p, '#e0f2fe', 1, '#ffffff');
1110
+ }
1111
+
1112
+ // Shivering breath vapor cloud
1113
+ const breathPhase = (t * 2.2) % 1;
1114
+ const bCloudX = cx + 25 + breathPhase * 60;
1115
+ const bCloudY = cy - 90 - breathPhase * 25;
1116
+ ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.5) + ')';
1117
+ ctx.beginPath();
1118
+ ctx.arc(bCloudX, bCloudY, 15 + breathPhase * 25, 0, Math.PI * 2);
1119
+ ctx.fill();
1120
+
1121
+ // Main shivering character
1122
+ const vibX = Math.sin(t * 38) * 4.5;
1123
+ const vibY = Math.cos(t * 32) * 2;
1124
+ drawDoodleCharacter({
1125
+ x: cx + vibX,
1126
+ y: cy + 140 + vibY,
1127
+ scale: 1.35,
1128
+ hairStyle: 'wild',
1129
+ expression: 'shivering',
1130
+ clothing: 'none',
1131
+ armPose: 'shiver',
1132
+ seed: 102,
1133
+ });
1134
+
1135
+ // Big crossed out cold icon on right
1136
+ sCircle(cx + 340, cy - 80, 55, 400, 'rgba(239,68,68,0.85)', 4);
1137
+ sLine(cx + 300, cy - 120, cx + 380, cy - 40, 401, 'rgba(239,68,68,0.95)', 4.5);
1138
+ ctx.font = 'bold 22px monospace';
1139
+ ctx.fillStyle = '#ef4444';
1140
+ ctx.textAlign = 'center';
1141
+ ctx.fillText('NOT FOR COLD', cx + 340, cy - 5);
1142
+
1143
+ } else if (COMP_THEME === 'doodle_hunter_savannah') {
1144
+ // Blazing African Savannah Hunter with Spear
1145
+ // Blazing midday sun
1146
+ const sunR = 75 + Math.sin(t * 2) * 6;
1147
+ ctx.fillStyle = '#f59e0b';
1148
+ ctx.beginPath();
1149
+ ctx.arc(cx - 380, cy - 180, sunR, 0, Math.PI * 2);
1150
+ ctx.fill();
1151
+ for (let r = 0; r < 8; r++) {
1152
+ const rayA = (r / 8) * Math.PI * 2 + t * 0.4;
1153
+ const rx1 = cx - 380 + Math.cos(rayA) * (sunR + 10);
1154
+ const ry1 = cy - 180 + Math.sin(rayA) * (sunR + 10);
1155
+ const rx2 = cx - 380 + Math.cos(rayA) * (sunR + 32);
1156
+ const ry2 = cy - 180 + Math.sin(rayA) * (sunR + 32);
1157
+ sLine(rx1, ry1, rx2, ry2, 450 + r, '#fbbf24', 3);
1158
+ }
1159
+
1160
+ // Ground and tall swaying savannah grass
1161
+ sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, '#78350f', 3);
1162
+ for (let g = 0; g < 32; g++) {
1163
+ const gx = cx - 550 + g * 36;
1164
+ const sway = Math.sin(t * 3.2 + g * 0.4) * 16;
1165
+ sLine(gx, cy + 220, gx + sway, cy + 140, 500 + g, '#d97706', 2.5);
1166
+ }
1167
+
1168
+ // Buzzing mosquitoes orbiting
1169
+ for (let m = 0; m < 3; m++) {
1170
+ const mx = cx + 40 + Math.sin(t * 8 + m * 2) * 65;
1171
+ const my = cy - 80 + Math.cos(t * 7 + m * 2) * 45;
1172
+ sCircle(mx, my, 2, 550 + m, '#0f172a', 1.5, '#ef4444');
1173
+ }
1174
+
1175
+ // Walking early hunter
1176
+ drawDoodleCharacter({
1177
+ x: cx - 40,
1178
+ y: cy + 140,
1179
+ scale: 1.35,
1180
+ hairStyle: 'wild',
1181
+ expression: 'talking',
1182
+ clothing: 'none',
1183
+ armPose: 'hold_spear',
1184
+ seed: 103,
1185
+ });
1186
+
1187
+ } else if (COMP_THEME === 'doodle_sewing_needle') {
1188
+ // Prehistoric Campfire & Bone Needle Sewing
1189
+ // Campfire with animated flames and sparks
1190
+ const fireX = cx - 380;
1191
+ const fireY = cy + 180;
1192
+ for (let f = 0; f < 5; f++) {
1193
+ const flH = 50 + Math.sin(t * 12 + f) * 22;
1194
+ ctx.fillStyle = f % 2 === 0 ? '#ef4444' : '#f59e0b';
1195
+ ctx.beginPath();
1196
+ ctx.moveTo(fireX - 30 + f * 12, fireY);
1197
+ ctx.quadraticCurveTo(fireX + (f - 2) * 8, fireY - flH, fireX - 10 + f * 12, fireY);
1198
+ ctx.fill();
1199
+ }
1200
+ // Floating fire sparks
1201
+ for (let sp = 0; sp < 8; sp++) {
1202
+ const sx = fireX + Math.sin(t * 4 + sp) * 35;
1203
+ const sy = fireY - 30 - ((t * 80 + sp * 35) % 140);
1204
+ sCircle(sx, sy, 2, 600 + sp, '#fbbf24', 1, '#fbbf24');
1205
+ }
1206
+
1207
+ // Sitting craftsman with hide on lap
1208
+ drawDoodleCharacter({
1209
+ x: cx - 110,
1210
+ y: cy + 140,
1211
+ scale: 1.35,
1212
+ hairStyle: 'wild',
1213
+ expression: 'talking',
1214
+ clothing: 'pelt',
1215
+ armPose: 'sewing',
1216
+ seed: 104,
1217
+ });
1218
+
1219
+ // Zoomed-in bone needle microscope circle on right
1220
+ const lensX = cx + 280;
1221
+ const lensY = cy - 10;
1222
+ const lensR = 145;
1223
+ ctx.fillStyle = 'rgba(15, 23, 42, 0.9)';
1224
+ ctx.beginPath();
1225
+ ctx.arc(lensX, lensY, lensR, 0, Math.PI * 2);
1226
+ ctx.fill();
1227
+ ctx.strokeStyle = '#d4af37';
1228
+ ctx.lineWidth = 5;
1229
+ ctx.stroke();
1230
+
1231
+ // Two thick leather pelt pieces being stitched
1232
+ sLine(lensX - 90, lensY - 20, lensX + 90, lensY - 20, 650, '#92400e', 8);
1233
+ sLine(lensX - 90, lensY + 20, lensX + 90, lensY + 20, 651, '#78350f', 8);
1234
+
1235
+ // Drilled bone needle piercing through
1236
+ const nWiggle = Math.sin(t * 4) * 20;
1237
+ sLine(lensX - 45 + nWiggle, lensY - 65, lensX + 25 + nWiggle, lensY + 55, 652, '#fef08a', 6);
1238
+ sCircle(lensX - 40 + nWiggle, lensY - 58, 4, 653, '#0f172a', 1.5, '#0f172a'); // Eyelet
1239
+
1240
+ // Stitches
1241
+ for (let st = 0; st < 6; st++) {
1242
+ const stX = lensX - 70 + st * 28;
1243
+ sLine(stX, lensY - 22, stX + 8, lensY + 22, 660 + st, '#ffffff', 3.5);
1244
+ }
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
+
1250
+ } else if (COMP_THEME === 'doodle_shame_reflection') {
1251
+ // Water Pool Reflection & Theory of Mind
1252
+ const poolX = cx;
1253
+ const poolY = cy + 180;
1254
+ ctx.fillStyle = 'rgba(14, 165, 233, 0.15)';
1255
+ ctx.beginPath();
1256
+ ctx.ellipse(poolX, poolY, 260, 60, 0, 0, Math.PI * 2);
1257
+ ctx.fill();
1258
+ ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)';
1259
+ ctx.lineWidth = 2.5;
1260
+ ctx.stroke();
1261
+
1262
+ // Ripples in pool
1263
+ for (let rip = 1; rip <= 3; rip++) {
1264
+ const ripR = (rip * 65 + t * 45) % 250;
1265
+ const ripAlpha = (1 - ripR / 250) * 0.4;
1266
+ ctx.strokeStyle = 'rgba(56, 189, 248, ' + ripAlpha + ')';
1267
+ ctx.beginPath();
1268
+ ctx.ellipse(poolX, poolY, ripR, ripR * 0.23, 0, 0, Math.PI * 2);
1269
+ ctx.stroke();
1270
+ }
1271
+
1272
+ // Inverted rippling reflection of character in water
1273
+ ctx.save();
1274
+ ctx.globalAlpha = 0.35;
1275
+ ctx.translate(cx, poolY + 20);
1276
+ ctx.scale(1.2, -0.75);
1277
+ drawDoodleCharacter({
1278
+ x: 0,
1279
+ y: 0,
1280
+ scale: 1.0,
1281
+ hairStyle: 'wild',
1282
+ expression: 'surprised',
1283
+ clothing: 'none',
1284
+ armPose: 'cover_body',
1285
+ seed: 700,
1286
+ });
1287
+ ctx.restore();
1288
+
1289
+ // Standing character looking down and covering body
1290
+ drawDoodleCharacter({
1291
+ x: cx,
1292
+ y: cy + 120,
1293
+ scale: 1.35,
1294
+ hairStyle: 'wild',
1295
+ expression: 'surprised',
1296
+ clothing: 'none',
1297
+ armPose: 'cover_body',
1298
+ seed: 105,
1299
+ });
1300
+
1301
+ // Blushing cheeks
1302
+ sCircle(cx - 18, cy - 10, 7, 750, 'rgba(244,63,94,0.6)', 1, 'rgba(244,63,94,0.6)');
1303
+ sCircle(cx + 18, cy - 10, 7, 751, 'rgba(244,63,94,0.6)', 1, 'rgba(244,63,94,0.6)');
1304
+
1305
+ // Overhead Theory of Mind Thought Halo
1306
+ const haloX = cx + 180;
1307
+ const haloY = cy - 120;
1308
+ sCircle(haloX, haloY, 55, 760, '#38bdf8', 3);
1309
+ // Eye icon inside halo
1310
+ sCircle(haloX, haloY, 20, 761, '#ffffff', 2.5);
1311
+ sCircle(haloX, haloY, 8, 762, '#38bdf8', 1, '#38bdf8');
1312
+ ctx.font = 'bold 18px monospace';
1313
+ ctx.fillStyle = '#38bdf8';
1314
+ ctx.textAlign = 'center';
1315
+ ctx.fillText('THEORY OF MIND', haloX, haloY + 78);
1316
+
1317
+ } else if (COMP_THEME === 'doodle_tribe_meeting') {
1318
+ // Two Clans Meeting & Communicating via Clothing Language
1319
+ sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
1320
+
1321
+ // Left Clan Member (Mountain Clan with Pelt & Peace Gesture)
1322
+ drawDoodleCharacter({
1323
+ x: cx - 220,
1324
+ y: cy + 140,
1325
+ scale: 1.3,
1326
+ hairStyle: 'wild',
1327
+ expression: 'talking',
1328
+ clothing: 'pelt',
1329
+ armPose: 'peace_gesture',
1330
+ seed: 106,
1331
+ });
1332
+
1333
+ // Right Clan Member (River Clan with Cloak & Feather)
1334
+ drawDoodleCharacter({
1335
+ x: cx + 220,
1336
+ y: cy + 140,
1337
+ scale: 1.3,
1338
+ hairStyle: 'feather',
1339
+ expression: 'curious',
1340
+ clothing: 'cloak',
1341
+ armPose: 'natural',
1342
+ seed: 107,
1343
+ });
1344
+
1345
+ // Glowing connection arc and communication symbols between them
1346
+ ctx.strokeStyle = ACCENT;
1347
+ ctx.lineWidth = 3;
1348
+ ctx.setLineDash([8, 8]);
1349
+ ctx.beginPath();
1350
+ ctx.moveTo(cx - 160, cy);
1351
+ ctx.quadraticCurveTo(cx, cy - 100, cx + 160, cy);
1352
+ ctx.stroke();
1353
+ ctx.setLineDash([]);
1354
+
1355
+ // Central status emblem
1356
+ sCircle(cx, cy - 65, 36, 800, ACCENT, 3, 'rgba(15, 23, 42, 0.85)');
1357
+ ctx.font = 'bold 24px monospace';
1358
+ ctx.fillStyle = ACCENT;
1359
+ ctx.textAlign = 'center';
1360
+ ctx.fillText('🤝', cx, cy - 56);
1361
+ ctx.font = 'bold 18px monospace';
1362
+ ctx.fillText('CLOTHING AS SOCIAL LANGUAGE', cx, cy + 15);
1363
+
1364
+ } else if (COMP_THEME === 'doodle_thinking_future') {
1365
+ // Prehistoric Thinker & Planning for Tomorrow
1366
+ // Starlit night and crescent moon
1367
+ ctx.fillStyle = '#fef08a';
1368
+ ctx.beginPath();
1369
+ ctx.arc(cx - 360, cy - 190, 50, 0, Math.PI * 2);
1370
+ ctx.fill();
1371
+ ctx.fillStyle = BG2;
1372
+ ctx.beginPath();
1373
+ ctx.arc(cx - 345, cy - 200, 48, 0, Math.PI * 2);
1374
+ ctx.fill();
1375
+
1376
+ // Large rock boulder for thinker
1377
+ ctx.fillStyle = '#334155';
1378
+ ctx.beginPath();
1379
+ ctx.ellipse(cx - 140, cy + 175, 85, 55, 0, 0, Math.PI * 2);
1380
+ ctx.fill();
1381
+
1382
+ // Thinker seated on rock
1383
+ drawDoodleCharacter({
1384
+ x: cx - 140,
1385
+ y: cy + 130,
1386
+ scale: 1.35,
1387
+ hairStyle: 'wild',
1388
+ expression: 'talking',
1389
+ clothing: 'pelt',
1390
+ armPose: 'thinker',
1391
+ seed: 108,
1392
+ });
1393
+
1394
+ // Expanding thought cloud overhead
1395
+ const cloudX = cx + 180;
1396
+ const cloudY = cy - 80;
1397
+ const cloudScale = spring(t, 0.15, 170, 14);
1398
+
1399
+ ctx.save();
1400
+ ctx.translate(cloudX, cloudY);
1401
+ ctx.scale(cloudScale, cloudScale);
1402
+
1403
+ // Thought bubbles leading up
1404
+ sCircle(-180, 160, 12, 850, '#ffffff', 2, '#ffffff');
1405
+ sCircle(-140, 120, 18, 851, '#ffffff', 2, '#ffffff');
1406
+ sCircle(-90, 70, 24, 852, '#ffffff', 2, '#ffffff');
1407
+
1408
+ // Main cloud body
1409
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
1410
+ ctx.beginPath();
1411
+ ctx.arc(-70, 0, 55, 0, Math.PI * 2);
1412
+ ctx.arc(0, -25, 65, 0, Math.PI * 2);
1413
+ ctx.arc(70, 0, 55, 0, Math.PI * 2);
1414
+ ctx.arc(0, 30, 60, 0, Math.PI * 2);
1415
+ ctx.fill();
1416
+
1417
+ // Inside thought cloud: Tomorrow's sunrise & stretched hide on rack
1418
+ ctx.fillStyle = '#f59e0b';
1419
+ ctx.beginPath();
1420
+ ctx.arc(0, 15, 30, Math.PI, Math.PI * 2);
1421
+ ctx.fill();
1422
+
1423
+ // Drying frame rack
1424
+ sLine(-45, 15, -45, -35, 860, '#78350f', 4);
1425
+ sLine(45, 15, 45, -35, 861, '#78350f', 4);
1426
+ sLine(-50, -30, 50, -30, 862, '#78350f', 4);
1427
+ sRect(-35, -28, 70, 40, 863, '#92400e', 3, '#b45309');
1428
+
1429
+ ctx.restore();
1430
+
1431
+ ctx.font = 'bold 20px monospace';
1432
+ ctx.fillStyle = '#facc15';
1433
+ ctx.textAlign = 'center';
1434
+ ctx.fillText('💡 PLANNING FOR TOMORROW', cloudX, cloudY + 95);
1435
+
1436
+ } else if (COMP_THEME === 'doodle_microscope_lice') {
1437
+ // The Lice Theory & Genetic Divergence Proof
1438
+ // Left scratching character
1439
+ drawDoodleCharacter({
1440
+ x: cx - 290,
1441
+ y: cy + 140,
1442
+ scale: 1.35,
1443
+ hairStyle: 'wild',
1444
+ expression: 'curious',
1445
+ clothing: 'pelt',
1446
+ armPose: 'shiver',
1447
+ seed: 109,
1448
+ });
1449
+
1450
+ // Massive microscope lens on right
1451
+ const lensX = cx + 140;
1452
+ const lensY = cy + 10;
1453
+ const lensR = 190;
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);
1466
+ }
1467
+
1468
+ // Branching evolutionary divergence diagram inside lens
1469
+ sLine(lensX - 120, lensY, lensX - 40, lensY, 940, '#ffffff', 4);
1470
+ sCircle(lensX - 40, lensY, 8, 941, '#d4af37', 2, '#d4af37'); // Divergence point
1471
+
1472
+ // Head lice branch
1473
+ sLine(lensX - 40, lensY, lensX + 80, lensY - 60, 942, '#38bdf8', 4);
1474
+ ctx.font = 'bold 16px monospace';
1475
+ ctx.fillStyle = '#38bdf8';
1476
+ ctx.textAlign = 'left';
1477
+ ctx.fillText('HEAD LICE (HAIR)', lensX + 90, lensY - 56);
1478
+
1479
+ // Body lice branch (clothes only)
1480
+ sLine(lensX - 40, lensY, lensX + 80, lensY + 60, 943, '#f43f5e', 4);
1481
+ ctx.fillStyle = '#f43f5e';
1482
+ ctx.fillText('BODY LICE (CLOTHES)', lensX + 90, lensY + 64);
1483
+
1484
+ // Divergence date highlight badge
1485
+ sRect(lensX - 110, lensY + 105, 220, 36, 950, '#d4af37', 2, 'rgba(212, 175, 55, 0.2)');
1486
+ ctx.font = 'bold 16px monospace';
1487
+ ctx.fillStyle = '#fef08a';
1488
+ ctx.textAlign = 'center';
1489
+ ctx.fillText('170,000 YEARS AGO', lensX, lensY + 129);
1490
+
1491
+ } else if (COMP_THEME === 'doodle_general_human') {
1492
+ // General Expressive Doodled Human Narrator
1493
+ sLine(cx - W * 0.4, cy + 220, cx + W * 0.4, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
1494
+ drawDoodleCharacter({
1495
+ x: cx,
1496
+ y: cy + 140,
1497
+ scale: 1.4,
1498
+ hairStyle: 'wild',
1499
+ expression: 'talking',
1500
+ clothing: 'pelt',
1501
+ armPose: 'natural',
1502
+ seed: 110,
1503
+ });
1504
+
1505
+ } else if (COMP_THEME === 'celestial_orbit') {
698
1506
  // 3D Celestial Body (Earth) with orbiting satellites & signal pulses
699
1507
  const earthR = Math.min(W, H) * 0.22;
700
1508
 
@@ -1265,10 +2073,11 @@ AVAILABLE HELPERS:
1265
2073
  - easeIn(t), easeOut(t), easeInOut(t), boilSeed(t, seed), hash(i)
1266
2074
  RULES:
1267
2075
  1. Every element must animate smoothly over time t [0..DURATION].
1268
- 2. Fill the canvas with rich visual geometry, vectors, animated nodes, characters, particles, or cinematic diagrams matching the prompt.
1269
- 3. NEVER use Math.random(), setTimeout, setInterval, Date.now(), performance.now().
1270
- 4. Output ONLY raw executable JavaScript code for the body of window.seek = async (t) => { ... }.
1271
- 5. NO markdown backticks, NO explanations. Start directly with JavaScript.`;
2076
+ 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.
2077
+ 3. Fill the canvas with rich visual geometry, fluid strokes, animated characters, particles, or cinematic diagrams matching the prompt.
2078
+ 4. NEVER use Math.random(), setTimeout, setInterval, Date.now(), performance.now(). All motion must be a pure mathematical function of time t.
2079
+ 5. Output ONLY raw executable JavaScript code for the body of window.seek = async (t) => { ... }.
2080
+ 6. NO markdown backticks, NO explanations. Start directly with JavaScript.`;
1272
2081
 
1273
2082
  const user = `TOPIC: ${topic || 'Visual Explainer'}
1274
2083
  VISUAL DIRECTION: ${visualDescription || caption || 'Dynamic motion graphics'}