@amg555/frameforge-mcp 2.4.4 → 2.4.5

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,30 @@ All notable changes to FrameForge are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## [2.4.5] — 2026-11-10
8
+
9
+ ### Fixed
10
+ - **Browser Code Animation Dispatch in `src/video-renderer/renderer.js`**:
11
+ - Fixed `ReferenceError: camera is not defined` inside `_renderCodeAnimationScene`. Previously, missing parameter destructuring caused the browser animation engine to throw on every scene, silently falling back to static FFmpeg image slideshows.
12
+ - Added full pass-through of `camera`, `visualType`, and `focalPoint` into `_renderCodeAnimationScene`.
13
+ - **Manifest Ingest Preservation (`src/workflows/ingest.js`, `src/workflows/assets.js`)**:
14
+ - `normaliseIngestScene` now retains `prompt`, `style`, and `theme`, and falls back to `raw.prompt` for `visual_description`.
15
+ - Added `--theme` and `--aesthetic` CLI options to `src/cli/make.js`.
16
+
17
+ ### Enhanced
18
+ - **Audio-Driven Continuous Camera Director (`src/video-renderer/scene-animator.js`)**:
19
+ - Replaced hardcoded timer modulo cuts with continuous trajectory over `p = t / window.DURATION`.
20
+ - Camera movements (`slow_zoom_in`, `slow_zoom_out`, `pan_left`, `pan_right`, `tilt_up`, `tilt_down`, `ken_burns`, `static`, and `auto`) now smoothly scale to exact narration length.
21
+ - **Max-Quality Organic Doodle Anatomy (`src/video-renderer/scene-animator.js`)**:
22
+ - Overhauled `drawDoodleCharacter`: replaced straight vertical spine line with contoured human torso (shoulders, ribcage, waist, hips), clavicle accents, articulated knees, and planted feet.
23
+ - Removed forced ground drop shadows, forced rosy cheek blush, and forced specular eye highlights.
24
+ - **Light Parchment Hand-Drawn Inking Aesthetic (`hand_drawn_doodle`)**:
25
+ - Warm parchment paper backgrounds (`#faf7f2`, `#f6f1e8`, `#fdfbf7`) with dark charcoal inking (`#1c1917`), subtle line grain, and organic 12fps line boil.
26
+ - Removed all unwanted text stamps and banners (`NOT FOR COLD`, `THEORY OF MIND`, etc.) from narrative doodle themes.
27
+ - Added `doodle_modern_vs_prehistoric` split-screen comparison theme.
28
+
29
+ ---
30
+
7
31
  ## [2.4.4] — 2026-11-09
8
32
 
9
33
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amg555/frameforge-mcp",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
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"
@@ -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.4', code.includes("version: '2.4.4'"));
23
+ check('SERVER_INFO version 2.4.5', code.includes("version: '2.4.5'"));
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.4', cfWorker.includes("'2.4.4'"));
302
+ check('cf-worker version 2.4.5', cfWorker.includes("'2.4.5'"));
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
+ theme: '',
101
102
  orientation: 'landscape',
102
103
  captions: false, subtitles: false,
103
104
  scenesFile: '', scriptFile: '', mediaDir: '',
@@ -120,6 +121,8 @@ function parseArgs(argv) {
120
121
  case '--subtitles':
121
122
  case '--with-captions': args.captions = true; break;
122
123
  case '--style': args.style = argv[++i] || args.style; break;
124
+ case '--theme':
125
+ case '--aesthetic': args.theme = argv[++i] || ''; break;
123
126
  case '--channels': args.channels = (argv[++i] || 'file').split(',').map(s => s.trim()); break;
124
127
  case '--no-research': args.noResearch = true; break;
125
128
  case '--no-script': args.noScript = true; break;
@@ -423,6 +426,7 @@ async function runSingleVideo(args, topic, outDir, workflows) {
423
426
  topic,
424
427
  title,
425
428
  style: args.style || '',
429
+ theme: args.theme || '',
426
430
  language: args.language,
427
431
  voice: resolvedVoice,
428
432
  });
@@ -439,6 +443,7 @@ async function runSingleVideo(args, topic, outDir, workflows) {
439
443
  topic,
440
444
  title,
441
445
  style: args.style || '',
446
+ theme: args.theme || '',
442
447
  language: args.language,
443
448
  showCaptions: Boolean(args.captions || args.subtitles),
444
449
  renderWidth,
@@ -1637,7 +1637,7 @@ Allows the agent in Claude Code, Cursor, or any IDE to inspect the visual output
1637
1637
 
1638
1638
  const SERVER_INFO = {
1639
1639
  name: 'video-pipeline',
1640
- version: '2.4.4',
1640
+ version: '2.4.5',
1641
1641
  };
1642
1642
 
1643
1643
  const CAPABILITIES = {
@@ -278,11 +278,14 @@ class VideoRenderer {
278
278
  caption,
279
279
  narration,
280
280
  visualDescription,
281
+ visualType,
281
282
  topic,
282
283
  style,
283
284
  theme,
284
285
  title,
285
286
  language,
287
+ camera,
288
+ focalPoint,
286
289
  html,
287
290
  code,
288
291
  clampedDur,
@@ -360,11 +363,14 @@ class VideoRenderer {
360
363
  caption,
361
364
  narration,
362
365
  visualDescription,
366
+ visualType = 'code_animation',
363
367
  topic = '',
364
368
  style = '',
365
369
  theme = '',
366
370
  title = '',
367
371
  language = '',
372
+ camera = 'auto',
373
+ focalPoint = null,
368
374
  html = null,
369
375
  code = null,
370
376
  clampedDur,
@@ -393,6 +399,7 @@ class VideoRenderer {
393
399
  topic,
394
400
  });
395
401
  } else {
402
+ const effectiveCam = camera || selectCameraHint(sceneIndex, focalPoint || {});
396
403
  htmlContent = generateSceneHtml({
397
404
  caption,
398
405
  narration,
@@ -409,6 +416,8 @@ class VideoRenderer {
409
416
  sceneIndex,
410
417
  musicMood,
411
418
  showCaptions,
419
+ camera: effectiveCam,
420
+ cameraHint: effectiveCam,
412
421
  });
413
422
  }
414
423
 
@@ -45,6 +45,19 @@ function getScriptFontStack(text = '', language = '') {
45
45
 
46
46
  // ── Aesthetic Definitions ──────────────────────────────────────────────────────
47
47
  const AESTHETICS = {
48
+ hand_drawn_doodle: {
49
+ name: 'Hand-Drawn Doodle',
50
+ palettes: [
51
+ { bg0: '#ffffff', bg1: '#faf7f2', bg2: '#f6f1e8', stroke: '#1c1917', strokeAlt: '#44403c', accent: '#c2410c', accent2: '#d97706', cardBg: 'rgba(255, 255, 255, 0.95)', cardBorder: 'rgba(28, 25, 23, 0.25)', tag: 'DOODLE', isLight: true },
52
+ { bg0: '#fdfbf7', bg1: '#f8f4eb', bg2: '#f3ece0', stroke: '#18181b', strokeAlt: '#3f3f46', accent: '#15803d', accent2: '#b45309', cardBg: 'rgba(255, 255, 255, 0.95)', cardBorder: 'rgba(24, 24, 27, 0.25)', tag: 'SKETCH', isLight: true },
53
+ { bg0: '#fcfaf5', bg1: '#f7f2e7', bg2: '#f0e8d8', stroke: '#0f172a', strokeAlt: '#334155', accent: '#0369a1', accent2: '#c2410c', cardBg: 'rgba(255, 255, 255, 0.95)', cardBorder: 'rgba(15, 23, 42, 0.25)', tag: 'INK', isLight: true },
54
+ ],
55
+ fontFamily: (script) => script ? `${script}, 'Caveat', 'Patrick Hand', 'Comic Sans MS', cursive, sans-serif` : `'Caveat', 'Patrick Hand', 'Segoe UI', cursive, sans-serif`,
56
+ fontDisplayWeight: '700',
57
+ backgroundType: 'paper_texture',
58
+ springTension: 160,
59
+ springFriction: 14,
60
+ },
48
61
  cyber_tech: {
49
62
  name: 'Cyber Tech',
50
63
  palettes: [
@@ -191,6 +204,7 @@ function detectAesthetic({
191
204
  // Explicit override wins
192
205
  const explicit = (theme || style || '').toLowerCase().trim();
193
206
  if (AESTHETICS[explicit]) return explicit;
207
+ if (/doodle|sketch|hand[-\s]?drawn|drawing|white background|cream background|cartoon|illustration/i.test(explicit)) return 'hand_drawn_doodle';
194
208
  if (/cyber|tech|code|ai|crypto|hacker|futur/i.test(explicit)) return 'cyber_tech';
195
209
  if (/doc|history|ancient|story|cinema|kerala|temple|culture/i.test(explicit)) return 'cinematic_documentary';
196
210
  if (/finan|market|stock|money|invest|crypto|business/i.test(explicit)) return 'finance_executive';
@@ -204,6 +218,12 @@ function detectAesthetic({
204
218
  // Keyword scan across all textual signals (English + Multilingual)
205
219
  const combined = `${topic} ${title} ${visualDescription} ${narration} ${caption} ${musicMood}`.toLowerCase();
206
220
 
221
+ // Hand-Drawn Doodle / Sketch / Cartoon
222
+ if (/(?:^|[^\w])(doodle|sketch|hand[-\s]?drawn|drawing|white background|cream background|paper|cartoon)(?:[^\w]|$)/.test(combined)
223
+ || /ഡൂഡിൽ|വര|വരച്ച|ചിത്രരചന|रेखाचित्र|कार्टून/.test(combined)) {
224
+ return 'hand_drawn_doodle';
225
+ }
226
+
207
227
  // Cosmic Space / Quantum / Orbital
208
228
  if (/(?:^|[^\w])(quantum|astronomy|galaxy|universe|space|mars|satellite|telescope|cosmos|interstellar|orbit|gps|satellites)(?:[^\w]|$)/.test(combined)
209
229
  || /പ്രപഞ്ച|ബഹിരാകാശ|നക്ഷത്ര|ഗ്രഹം|ഉപഗ്രഹ|ജ്യോതിശാസ്ത്ര|अंतरिक्ष|ब्रह्मांड|तारा|விண்வெளி|பிரபஞ்சம்/.test(combined)) {
@@ -263,9 +283,10 @@ function detectAesthetic({
263
283
  /**
264
284
  * Detect the graphic entity to render on the full canvas for a given scene.
265
285
  */
266
- function detectComponentTheme({ visualDescription = '', caption = '', narration = '', topic = '', imagePath = null, aesthetic = 'cyber_tech' } = {}) {
286
+ function detectComponentTheme({ visualDescription = '', visual_description = '', prompt = '', caption = '', narration = '', topic = '', imagePath = null, aesthetic = 'cyber_tech' } = {}) {
267
287
  if (imagePath) return 'image';
268
- const text = `${visualDescription} ${caption} ${narration} ${topic}`.toLowerCase();
288
+ const visDesc = visualDescription || visual_description || prompt || '';
289
+ const text = `${visDesc} ${caption} ${narration} ${topic}`.toLowerCase();
269
290
 
270
291
  // ── Doodled Character Actions (Human Stories, History, Evolution, Daily Life) ──
271
292
  // 1. Microscope / Lice Theory / Parasite / Genetics (Scene 7)
@@ -310,13 +331,19 @@ function detectComponentTheme({ visualDescription = '', caption = '', narration
310
331
  return 'doodle_shame_reflection';
311
332
  }
312
333
 
313
- // 8. Dressing / Wardrobe / Wearing Clothes / Outfit / Morning Routine (Scene 1, 11)
334
+ // 8. Modern vs Prehistoric Human Comparison (Scene 2)
335
+ if (/(?:^|[^\w])(modern vs ancient|modern human|fashionable|modern vs prehistoric|split-screen|split screen|transformation|then and now|evolution of human)(?:[^\w]|$)/i.test(text)
336
+ || /ഫാഷൻ|ആധുനിക മനുഷ്യൻ|പുരാതന മനുഷ്യൻ|കടകളോ|ആധുനികവും പുരാതനവും/.test(text)) {
337
+ return 'doodle_modern_vs_prehistoric';
338
+ }
339
+
340
+ // 9. Dressing / Wardrobe / Wearing Clothes / Outfit / Morning Routine (Scene 1, 11)
314
341
  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)) {
342
+ || /വസ്ത്രധാരണം|ധരിച്ചിട്ടുണ്ടാ|ധരിച്ച|വസ്ത്രം ധരിക്കാൻ|വസ്ത്രത്തിന്|വസ്ത്രം തിരഞ്ഞെടുക്കു|ഷർട്ട്|തിരഞ്ഞെടുത്തത്|തുണി എടുത്തിടുകയല്ല|കല്യാണത്തിന്|ഇന്റർവ്യൂ|ആമുഖം: വസ്ത്രധാരണം|വസ്ത്രം തിരഞ്ഞെടുക്കുമ്പോൾ|വസ്ത്രം തിരഞ്ഞെടുക്കും/.test(text)) {
316
343
  return 'doodle_dressing';
317
344
  }
318
345
 
319
- // 9. General Human Character
346
+ // 10. General Human Character
320
347
  if (/(?:^|[^\w])(human|person|man|woman|people|child|guy|doodle|character|actor)(?:[^\w]|$)/i.test(text)
321
348
  || /മനുഷ്യൻ|ആളുകൾ|വ്യക്തി/.test(text)) {
322
349
  return 'doodle_general_human';
@@ -520,6 +547,8 @@ function generateSceneHtml({
520
547
  title = '',
521
548
  language = '',
522
549
  musicMood = 'neutral',
550
+ camera = 'auto',
551
+ cameraHint = 'auto',
523
552
  duration = 3,
524
553
  width = 1920,
525
554
  height = 1080,
@@ -598,6 +627,7 @@ function generateSceneHtml({
598
627
  <canvas id="stage" width="${width}" height="${height}"></canvas>
599
628
  <script>
600
629
  window.DURATION = ${clampedDur};
630
+ window.CAMERA_HINT = ${JSON.stringify(camera || cameraHint || 'auto')};
601
631
  const canvas = document.getElementById('stage');
602
632
  const ctx = canvas.getContext('2d');
603
633
  const W = ${width};
@@ -658,6 +688,10 @@ const FONT_DISPLAY = ${JSON.stringify(fontDisplay)};
658
688
  const FONT_BODY = ${JSON.stringify(fontBody)};
659
689
  const FONT_WEIGHT = ${JSON.stringify(aesthetic.fontDisplayWeight)};
660
690
  const IMAGE_SRC = ${JSON.stringify(imageDataUri)};
691
+ const IS_LIGHT = ${Boolean(pal.isLight)};
692
+ const STROKE_COLOR = ${JSON.stringify(pal.isLight ? (pal.stroke || '#1c1917') : '#f8fafc')};
693
+ const STROKE_ALT = ${JSON.stringify(pal.isLight ? (pal.strokeAlt || '#44403c') : 'rgba(255, 255, 255, 0.7)')};
694
+ const AMBIENT_LINE = ${JSON.stringify(pal.isLight ? 'rgba(28, 25, 23, 0.22)' : 'rgba(255, 255, 255, 0.25)')};
661
695
 
662
696
  let cachedImage = null;
663
697
  if (IMAGE_SRC) {
@@ -677,7 +711,24 @@ function drawBackground(t) {
677
711
  ctx.fillRect(0, 0, W, H);
678
712
 
679
713
  ctx.save();
680
- if (BG_TYPE === 'grid_perspective') {
714
+ if (BG_TYPE === 'paper_texture' || IS_LIGHT) {
715
+ // Subtle paper grain / parchment texture
716
+ ctx.strokeStyle = 'rgba(120, 80, 40, 0.04)';
717
+ ctx.lineWidth = 1;
718
+ for (let y = 40; y < H; y += 40) {
719
+ const wobble = Math.sin(y * 0.1) * 3;
720
+ ctx.beginPath();
721
+ ctx.moveTo(0, y + wobble);
722
+ ctx.lineTo(W, y - wobble);
723
+ ctx.stroke();
724
+ }
725
+ // Subtle organic vignette
726
+ const vig = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.45, W / 2, H / 2, Math.max(W, H) * 0.75);
727
+ vig.addColorStop(0, 'rgba(0, 0, 0, 0)');
728
+ vig.addColorStop(1, 'rgba(120, 90, 60, 0.08)');
729
+ ctx.fillStyle = vig;
730
+ ctx.fillRect(0, 0, W, H);
731
+ } else if (BG_TYPE === 'grid_perspective') {
681
732
  // 3D Perspective Grid
682
733
  ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
683
734
  ctx.lineWidth = 1;
@@ -754,51 +805,61 @@ function drawBackground(t) {
754
805
  // ── Full-Canvas Cinematic Visual Directors ─────────────────────────────────────
755
806
  function drawMainScene(t) {
756
807
  ctx.save();
757
- const p = Math.min(1, Math.max(0, t / window.DURATION));
808
+ // Continuous audio-driven cinematic camera movement (no fixed interval timer cuts)
809
+ const dur = window.DURATION || 3;
810
+ const p = Math.min(1, Math.max(0, t / dur));
758
811
  const enterSpring = spring(t, 0.05, 170, 13);
759
-
760
- // Multi-Shot Dynamic Director: cut camera framing every 5-7s to keep pacing energetic
761
- const shotDur = 5.5;
762
- const shotIdx = Math.floor(t / shotDur);
763
- const shotLocalT = t - shotIdx * shotDur;
764
- const shotCutSpring = spring(shotLocalT, 0.02, 220, 18);
765
- const shotProgress = (t % shotDur) / shotDur;
812
+ const camHint = (window.CAMERA_HINT || 'auto').toLowerCase();
766
813
 
767
814
  let targetZoom = 1.0;
768
815
  let targetPanX = 0;
769
816
  let targetPanY = 0;
770
817
 
771
- if (window.DURATION > 6.5) {
772
- const angle = shotIdx % 4;
773
- if (angle === 1) {
774
- targetZoom = 1.14 + shotProgress * 0.03;
775
- targetPanX = -40 + Math.sin(t * 0.4) * 8;
776
- targetPanY = -12;
777
- } else if (angle === 2) {
778
- targetZoom = 1.20 + shotProgress * 0.03;
779
- targetPanX = 40 + Math.cos(t * 0.4) * 8;
780
- targetPanY = -18;
781
- } else if (angle === 3) {
782
- targetZoom = 1.08 + shotProgress * 0.02;
783
- targetPanX = Math.sin(t * 0.3) * 12;
784
- targetPanY = 12;
785
- } else {
786
- targetZoom = 1.02 + shotProgress * 0.03;
787
- targetPanX = Math.sin(t * 0.3) * 12;
788
- targetPanY = Math.cos(t * 0.25) * 6;
789
- }
818
+ if (camHint === 'slow_zoom_in' || camHint === 'zoom_in') {
819
+ targetZoom = 1.0 + p * 0.08;
820
+ targetPanX = Math.sin(t * 0.4) * 6;
821
+ targetPanY = Math.cos(t * 0.3) * 4;
822
+ } else if (camHint === 'slow_zoom_out' || camHint === 'zoom_out') {
823
+ targetZoom = 1.08 - p * 0.08;
824
+ targetPanX = Math.sin(t * 0.4) * 6;
825
+ targetPanY = Math.cos(t * 0.3) * 4;
826
+ } else if (camHint === 'pan_left') {
827
+ targetZoom = 1.02;
828
+ targetPanX = 25 - p * 50;
829
+ targetPanY = Math.cos(t * 0.3) * 3;
830
+ } else if (camHint === 'pan_right') {
831
+ targetZoom = 1.02;
832
+ targetPanX = -25 + p * 50;
833
+ targetPanY = Math.cos(t * 0.3) * 3;
834
+ } else if (camHint === 'tilt_up') {
835
+ targetZoom = 1.03;
836
+ targetPanX = Math.sin(t * 0.3) * 4;
837
+ targetPanY = 20 - p * 40;
838
+ } else if (camHint === 'tilt_down') {
839
+ targetZoom = 1.03;
840
+ targetPanX = Math.sin(t * 0.3) * 4;
841
+ targetPanY = -20 + p * 40;
842
+ } else if (camHint === 'ken_burns') {
843
+ targetZoom = 1.0 + p * 0.06;
844
+ targetPanX = -18 + p * 36;
845
+ targetPanY = 12 - p * 24;
846
+ } else if (camHint === 'static') {
847
+ targetZoom = 1.0;
848
+ targetPanX = 0;
849
+ targetPanY = 0;
790
850
  } else {
791
- targetZoom = 1.0 + Math.sin(t * 0.45) * 0.02 + p * 0.04;
792
- targetPanX = Math.sin(t * 0.35) * 12;
793
- targetPanY = Math.cos(t * 0.28) * 6;
851
+ // 'auto' or narrative default: subtle organic breathing and smooth forward progression
852
+ targetZoom = 1.0 + Math.sin(p * Math.PI * 0.5) * 0.04;
853
+ targetPanX = Math.sin(t * 0.35) * 8;
854
+ targetPanY = Math.cos(t * 0.28) * 4;
794
855
  }
795
856
 
796
- const camZoom = 1.0 + (targetZoom - 1.0) * Math.min(1, Math.max(0, shotCutSpring));
797
- const camPanX = targetPanX * Math.min(1, Math.max(0, shotCutSpring));
798
- const camPanY = targetPanY * Math.min(1, Math.max(0, shotCutSpring));
857
+ const camZoom = targetZoom;
858
+ const camPanX = targetPanX;
859
+ const camPanY = targetPanY;
799
860
 
800
861
  ctx.translate(W / 2 + camPanX, H * 0.50 + camPanY);
801
- ctx.scale(Math.min(1.04, Math.max(0.88, enterSpring)) * camZoom, Math.min(1.04, Math.max(0.88, enterSpring)) * camZoom);
862
+ ctx.scale(Math.min(1.02, Math.max(0.92, enterSpring)) * camZoom, Math.min(1.02, Math.max(0.92, enterSpring)) * camZoom);
802
863
  ctx.translate(-W / 2, -H * 0.50);
803
864
 
804
865
  const cx = W / 2;
@@ -810,7 +871,7 @@ function drawMainScene(t) {
810
871
  return Math.sin(seed * 19.37 + step * 2.41) * mag;
811
872
  }
812
873
 
813
- function sLine(x1, y1, x2, y2, seed, color = '#f8fafc', width = 3.5) {
874
+ function sLine(x1, y1, x2, y2, seed, color = STROKE_COLOR, width = 3.5) {
814
875
  ctx.save();
815
876
  ctx.strokeStyle = color;
816
877
  ctx.lineWidth = width;
@@ -825,7 +886,7 @@ function drawMainScene(t) {
825
886
  ctx.restore();
826
887
  }
827
888
 
828
- function sCircle(x, y, r, seed, color = '#f8fafc', width = 3, fillColor = null) {
889
+ function sCircle(x, y, r, seed, color = STROKE_COLOR, width = 3, fillColor = null) {
829
890
  ctx.save();
830
891
  if (fillColor) {
831
892
  ctx.fillStyle = fillColor;
@@ -850,7 +911,7 @@ function drawMainScene(t) {
850
911
  ctx.restore();
851
912
  }
852
913
 
853
- function sRect(x, y, w, h, seed, color = '#f8fafc', width = 3, fillColor = null) {
914
+ function sRect(x, y, w, h, seed, color = STROKE_COLOR, width = 3, fillColor = null) {
854
915
  ctx.save();
855
916
  if (fillColor) {
856
917
  ctx.fillStyle = fillColor;
@@ -874,75 +935,107 @@ function drawMainScene(t) {
874
935
  expression = 'talking',
875
936
  clothing = 'none',
876
937
  armPose = 'natural',
938
+ showShadow = false,
939
+ showBlush = false,
940
+ showSpecular = false,
877
941
  seed = 100,
878
942
  } = {}) {
879
943
  ctx.save();
880
944
  ctx.translate(x, y);
881
945
  ctx.scale(scale, scale);
882
946
 
883
- // Soft ground contact shadow
884
- ctx.fillStyle = 'rgba(0, 0, 0, 0.28)';
885
- ctx.beginPath();
886
- ctx.ellipse(0, legLength, 34, 7, 0, 0, Math.PI * 2);
887
- ctx.fill();
947
+ // Ground contact shadow: strictly conditional (default false)
948
+ if (showShadow) {
949
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.28)';
950
+ ctx.beginPath();
951
+ ctx.ellipse(0, legLength, 34, 7, 0, 0, Math.PI * 2);
952
+ ctx.fill();
953
+ }
888
954
 
889
955
  const blink = (t % 3.2 < 0.14) ? 0.15 : 1.0;
890
956
  const mouthTalk = Math.abs(Math.sin(t * 14)) * 4;
891
957
  const torsoTopY = -torsoHeight;
892
958
  const torsoBottomY = 0;
959
+ const shoulderW = 28;
893
960
 
894
- // Torso Clothing Fills
961
+ // High quality contoured torso silhouette (organic human form, never stick figure)
895
962
  if (clothing === 'pelt') {
963
+ // Prehistoric animal pelt tunic with textured ragged hem
896
964
  ctx.fillStyle = '#b45309';
897
965
  ctx.beginPath();
898
- ctx.moveTo(-24 + wb(seed, 2), torsoTopY);
899
- ctx.lineTo(24 + wb(seed + 1, 2), torsoTopY);
900
- ctx.lineTo(32 + wb(seed + 2, 2), torsoBottomY + 28);
901
- ctx.lineTo(-28 + wb(seed + 3, 2), torsoBottomY + 28);
966
+ ctx.moveTo(-shoulderW + wb(seed, 2), torsoTopY);
967
+ ctx.lineTo(shoulderW + wb(seed + 1, 2), torsoTopY);
968
+ ctx.lineTo(shoulderW + 6 + wb(seed + 2, 2), torsoBottomY + 22);
969
+ ctx.lineTo(-shoulderW - 6 + wb(seed + 3, 2), torsoBottomY + 22);
902
970
  ctx.closePath();
903
971
  ctx.fill();
904
- sLine(-28, torsoBottomY + 2, 32, torsoBottomY + 2, seed + 4, '#78350f', 4);
972
+
973
+ // Pelt border stitching & waist wrap
974
+ sLine(-shoulderW - 6, torsoBottomY + 22, shoulderW + 6, torsoBottomY + 22, seed + 4, '#78350f', 4);
975
+ sLine(-shoulderW, torsoBottomY - 8, shoulderW, torsoBottomY - 8, seed + 5, '#451a03', 3);
976
+ // Asymmetric fur shoulder wrap
977
+ sLine(-shoulderW, torsoTopY, shoulderW - 10, torsoTopY + 24, seed + 6, '#78350f', 3.5);
905
978
  } else if (clothing === 'shirt') {
979
+ // Modern tailored shirt/jacket with collar and button placket
906
980
  ctx.fillStyle = ACCENT;
907
981
  ctx.beginPath();
908
- ctx.moveTo(-26 + wb(seed, 2), torsoTopY);
909
- ctx.lineTo(26 + wb(seed + 1, 2), torsoTopY);
910
- ctx.lineTo(28 + wb(seed + 2, 2), torsoBottomY + 12);
911
- ctx.lineTo(-28 + wb(seed + 3, 2), torsoBottomY + 12);
982
+ ctx.moveTo(-shoulderW + wb(seed, 2), torsoTopY);
983
+ ctx.lineTo(shoulderW + wb(seed + 1, 2), torsoTopY);
984
+ ctx.lineTo(shoulderW + 2 + wb(seed + 2, 2), torsoBottomY + 12);
985
+ ctx.lineTo(-shoulderW - 2 + wb(seed + 3, 2), torsoBottomY + 12);
912
986
  ctx.closePath();
913
987
  ctx.fill();
988
+
989
+ // Collar lines & placket
914
990
  sLine(-14, torsoTopY, 0, torsoTopY + 18, seed + 4, '#ffffff', 3);
915
991
  sLine(14, torsoTopY, 0, torsoTopY + 18, seed + 5, '#ffffff', 3);
916
992
  sLine(0, torsoTopY + 18, 0, torsoBottomY + 10, seed + 6, '#ffffff', 2.5);
917
993
  } else if (clothing === 'cloak') {
994
+ // Flowing draped mantle
918
995
  ctx.fillStyle = '#0f766e';
919
996
  ctx.beginPath();
920
- ctx.moveTo(-32, torsoTopY - 6);
921
- ctx.lineTo(32, torsoTopY - 6);
922
- ctx.lineTo(44, torsoBottomY + 32);
923
- ctx.lineTo(-44, torsoBottomY + 32);
997
+ ctx.moveTo(-shoulderW - 8, torsoTopY - 6);
998
+ ctx.lineTo(shoulderW + 8, torsoTopY - 6);
999
+ ctx.lineTo(shoulderW + 18, torsoBottomY + 32);
1000
+ ctx.lineTo(-shoulderW - 18, torsoBottomY + 32);
924
1001
  ctx.closePath();
925
1002
  ctx.fill();
1003
+ sLine(-shoulderW - 8, torsoTopY - 6, -shoulderW - 18, torsoBottomY + 32, seed + 7, '#134e4a', 3.5);
1004
+ sLine(shoulderW + 8, torsoTopY - 6, shoulderW + 18, torsoBottomY + 32, seed + 8, '#134e4a', 3.5);
926
1005
  } else {
927
- sLine(0, torsoTopY, 0, torsoBottomY, seed + 7, '#fcd34d', 4);
928
- sLine(-15, torsoTopY + 30, -2, torsoTopY + 40, seed + 8, 'rgba(255,255,255,0.3)', 2);
929
- sLine(15, torsoTopY + 30, 2, torsoTopY + 40, seed + 9, 'rgba(255,255,255,0.3)', 2);
1006
+ // Natural contoured human torso (shoulders, ribcage, waist, hips)
1007
+ ctx.fillStyle = IS_LIGHT ? 'rgba(217, 119, 6, 0.12)' : 'rgba(254, 243, 199, 0.22)';
1008
+ ctx.beginPath();
1009
+ ctx.moveTo(-shoulderW + wb(seed, 1.5), torsoTopY + 5);
1010
+ ctx.lineTo(shoulderW + wb(seed + 1, 1.5), torsoTopY + 5);
1011
+ ctx.quadraticCurveTo(shoulderW - 6 + wb(seed + 2, 1.5), torsoTopY + torsoHeight * 0.5, shoulderW - 4, torsoBottomY);
1012
+ ctx.lineTo(-shoulderW + 4, torsoBottomY);
1013
+ ctx.quadraticCurveTo(-shoulderW + 6 + wb(seed + 3, 1.5), torsoTopY + torsoHeight * 0.5, -shoulderW + wb(seed, 1.5), torsoTopY + 5);
1014
+ ctx.closePath();
1015
+ ctx.fill();
1016
+
1017
+ // Contoured torso outline strokes
1018
+ sLine(-shoulderW, torsoTopY + 5, -shoulderW + 4, torsoBottomY, seed + 7, STROKE_COLOR, 3.5);
1019
+ sLine(shoulderW, torsoTopY + 5, shoulderW - 4, torsoBottomY, seed + 8, STROKE_COLOR, 3.5);
1020
+ sLine(-shoulderW + 4, torsoBottomY, shoulderW - 4, torsoBottomY, seed + 9, STROKE_COLOR, 3);
1021
+ // Subtle collarbone / rib accents
1022
+ sLine(-16, torsoTopY + 16, -2, torsoTopY + 22, seed + 10, STROKE_ALT, 2);
1023
+ sLine(16, torsoTopY + 16, 2, torsoTopY + 22, seed + 11, STROKE_ALT, 2);
930
1024
  }
931
- sLine(0, torsoTopY, 0, torsoBottomY, seed + 10, '#f8fafc', 4);
932
1025
 
933
- // Head
934
- const headCenterY = torsoTopY - headRadius - 6 + wb(seed + 11, 1.5);
935
- sCircle(0, headCenterY, headRadius, seed + 12, '#f8fafc', 3.5, '#fef3c7');
1026
+ // Head (expressive contoured skull)
1027
+ const headCenterY = torsoTopY - headRadius - 6 + wb(seed + 12, 1.5);
1028
+ sCircle(0, headCenterY, headRadius, seed + 13, STROKE_COLOR, 3.5, '#fef3c7');
936
1029
 
937
1030
  // Hair
938
1031
  if (hairStyle === 'wild') {
939
- for (let h = 0; h < 6; h++) {
940
- const hairAng = -Math.PI * 0.85 + (h / 5) * Math.PI * 0.7;
1032
+ for (let h = 0; h < 7; h++) {
1033
+ const hairAng = -Math.PI * 0.88 + (h / 6) * Math.PI * 0.76;
941
1034
  const hx1 = Math.cos(hairAng) * headRadius;
942
1035
  const hy1 = headCenterY + Math.sin(hairAng) * headRadius;
943
- const hx2 = Math.cos(hairAng) * (headRadius + 22 + wb(seed + 13 + h, 4));
944
- const hy2 = headCenterY + Math.sin(hairAng) * (headRadius + 22 + wb(seed + 14 + h, 4));
945
- sLine(hx1, hy1, hx2, hy2, seed + 15 + h, '#78350f', 3.5);
1036
+ const hx2 = Math.cos(hairAng) * (headRadius + 24 + wb(seed + 14 + h, 4));
1037
+ const hy2 = headCenterY + Math.sin(hairAng) * (headRadius + 24 + wb(seed + 15 + h, 4));
1038
+ sLine(hx1, hy1, hx2, hy2, seed + 16 + h, '#78350f', 3.5);
946
1039
  }
947
1040
  } else if (hairStyle === 'modern') {
948
1041
  ctx.fillStyle = '#1e293b';
@@ -958,12 +1051,12 @@ function drawMainScene(t) {
958
1051
 
959
1052
  // Eyes
960
1053
  const eyeY = headCenterY + 4;
961
- const eyeR = 4 * blink;
1054
+ const eyeR = 4.2 * blink;
962
1055
  sCircle(-12, eyeY, Math.max(1, eyeR), seed + 23, '#0f172a', 2, '#0f172a');
963
1056
  sCircle(12, eyeY, Math.max(1, eyeR), seed + 24, '#0f172a', 2, '#0f172a');
964
1057
 
965
- // Specular eye highlights
966
- if (blink > 0.4) {
1058
+ // Specular eye highlights: only when explicitly enabled
1059
+ if (showSpecular && blink > 0.4) {
967
1060
  ctx.fillStyle = '#ffffff';
968
1061
  ctx.beginPath();
969
1062
  ctx.arc(-14, eyeY - 1.5, 1.2, 0, Math.PI * 2);
@@ -971,12 +1064,14 @@ function drawMainScene(t) {
971
1064
  ctx.fill();
972
1065
  }
973
1066
 
974
- // Warm cheek blush
975
- ctx.fillStyle = 'rgba(251, 113, 133, 0.28)';
976
- ctx.beginPath();
977
- ctx.ellipse(-14, headCenterY + 12, 6, 3.5, 0, 0, Math.PI * 2);
978
- ctx.ellipse(14, headCenterY + 12, 6, 3.5, 0, 0, Math.PI * 2);
979
- ctx.fill();
1067
+ // Rosy cheek blush: strictly conditional (default false)
1068
+ if (showBlush || expression === 'blushing' || expression === 'shame') {
1069
+ ctx.fillStyle = 'rgba(251, 113, 133, 0.28)';
1070
+ ctx.beginPath();
1071
+ ctx.ellipse(-14, headCenterY + 12, 6, 3.5, 0, 0, Math.PI * 2);
1072
+ ctx.ellipse(14, headCenterY + 12, 6, 3.5, 0, 0, Math.PI * 2);
1073
+ ctx.fill();
1074
+ }
980
1075
 
981
1076
  // Eyebrows
982
1077
  if (expression === 'shivering') {
@@ -1012,93 +1107,102 @@ function drawMainScene(t) {
1012
1107
  sLine(-8, mouthY, 8, mouthY, seed + 34, '#0f172a', 2.5);
1013
1108
  }
1014
1109
 
1015
- // Legs
1110
+ // Legs with articulated knees and planted feet
1016
1111
  const hipY = torsoBottomY;
1017
1112
  if (armPose === 'sewing' || armPose === 'thinker') {
1018
- sLine(-10, hipY, -45, hipY + 45, seed + 35, '#f8fafc', 4);
1019
- sLine(-45, hipY + 45, 10, hipY + 50, seed + 36, '#f8fafc', 4);
1020
- sLine(10, hipY, 45, hipY + 45, seed + 37, '#f8fafc', 4);
1021
- sLine(45, hipY + 45, -10, hipY + 50, seed + 38, '#f8fafc', 4);
1113
+ // Cross-legged seated pose
1114
+ sLine(-14, hipY, -48, hipY + 45, seed + 35, STROKE_COLOR, 4);
1115
+ sLine(-48, hipY + 45, 12, hipY + 50, seed + 36, STROKE_COLOR, 4);
1116
+ sLine(14, hipY, 48, hipY + 45, seed + 37, STROKE_COLOR, 4);
1117
+ sLine(48, hipY + 45, -12, hipY + 50, seed + 38, STROKE_COLOR, 4);
1022
1118
  } else if (armPose === 'hold_spear') {
1119
+ // Dynamic walking gait
1023
1120
  const legSwing = Math.sin(t * 5.5);
1024
1121
  const l1KneeX = -15 + legSwing * 22;
1025
1122
  const l1KneeY = hipY + legLength * 0.55;
1026
1123
  const l1FootX = -15 + legSwing * 38;
1027
1124
  const l1FootY = hipY + legLength;
1028
- sLine(-10, hipY, l1KneeX, l1KneeY, seed + 39, '#f8fafc', 4);
1029
- sLine(l1KneeX, l1KneeY, l1FootX, l1FootY, seed + 40, '#f8fafc', 4);
1125
+ sLine(-14, hipY, l1KneeX, l1KneeY, seed + 39, STROKE_COLOR, 4);
1126
+ sLine(l1KneeX, l1KneeY, l1FootX, l1FootY, seed + 40, STROKE_COLOR, 4);
1127
+ sLine(l1FootX, l1FootY, l1FootX + 16, l1FootY, seed + 41, STROKE_COLOR, 3.5);
1030
1128
 
1031
1129
  const l2KneeX = 15 - legSwing * 22;
1032
1130
  const l2KneeY = hipY + legLength * 0.55;
1033
1131
  const l2FootX = 15 - legSwing * 38;
1034
1132
  const l2FootY = hipY + legLength;
1035
- sLine(10, hipY, l2KneeX, l2KneeY, seed + 41, '#f8fafc', 4);
1036
- sLine(l2KneeX, l2KneeY, l2FootX, l2FootY, seed + 42, '#f8fafc', 4);
1133
+ sLine(14, hipY, l2KneeX, l2KneeY, seed + 42, STROKE_COLOR, 4);
1134
+ sLine(l2KneeX, l2KneeY, l2FootX, l2FootY, seed + 43, STROKE_COLOR, 4);
1135
+ sLine(l2FootX, l2FootY, l2FootX + 16, l2FootY, seed + 44, STROKE_COLOR, 3.5);
1037
1136
  } else {
1038
- sLine(-12, hipY, -14, hipY + legLength, seed + 43, '#f8fafc', 4);
1039
- sLine(12, hipY, 14, hipY + legLength, seed + 44, '#f8fafc', 4);
1040
- sLine(-14, hipY + legLength, -24, hipY + legLength, seed + 45, '#f8fafc', 3.5);
1041
- sLine(14, hipY + legLength, 24, hipY + legLength, seed + 46, '#f8fafc', 3.5);
1137
+ // Standing legs with natural knee contour and planted feet
1138
+ const lKneeX = -15 + wb(seed + 39, 1);
1139
+ const rKneeX = 15 + wb(seed + 40, 1);
1140
+ const kneeY = hipY + legLength * 0.52;
1141
+ const footY = hipY + legLength;
1142
+ sLine(-16, hipY, lKneeX, kneeY, seed + 41, STROKE_COLOR, 4);
1143
+ sLine(lKneeX, kneeY, -16, footY, seed + 42, STROKE_COLOR, 4);
1144
+ sLine(-16, footY, -28, footY, seed + 43, STROKE_COLOR, 3.5);
1145
+
1146
+ sLine(16, hipY, rKneeX, kneeY, seed + 44, STROKE_COLOR, 4);
1147
+ sLine(rKneeX, kneeY, 16, footY, seed + 45, STROKE_COLOR, 4);
1148
+ sLine(16, footY, 28, footY, seed + 46, STROKE_COLOR, 3.5);
1042
1149
  }
1043
1150
 
1044
- // Arms & Hands
1151
+ // Arms & Hands with natural articulated elbows and gestures
1045
1152
  const shoulderY = torsoTopY + 12;
1046
1153
  if (armPose === 'shiver') {
1047
1154
  const shivA = Math.sin(t * 34) * 3;
1048
- sLine(-20, shoulderY, 16 + shivA, shoulderY + 35, seed + 47, '#f8fafc', 4);
1049
- sLine(20, shoulderY, -16 + shivA, shoulderY + 35, seed + 48, '#f8fafc', 4);
1155
+ sLine(-shoulderW, shoulderY, 16 + shivA, shoulderY + 35, seed + 47, STROKE_COLOR, 4);
1156
+ sLine(shoulderW, shoulderY, -16 + shivA, shoulderY + 35, seed + 48, STROKE_COLOR, 4);
1050
1157
  } else if (armPose === 'hold_spear') {
1051
- const spearX = -45;
1052
- sLine(-18, shoulderY, -35, shoulderY + 30, seed + 49, '#f8fafc', 4);
1053
- sLine(-35, shoulderY + 30, spearX, shoulderY + 5, seed + 50, '#f8fafc', 4);
1054
- sLine(spearX, -torsoHeight - 130, spearX, hipY + legLength + 10, seed + 51, '#92400e', 5);
1055
- ctx.fillStyle = '#64748b';
1158
+ // Holding spear with grip
1159
+ const spearSway = Math.sin(t * 5.5) * 6;
1160
+ sLine(-shoulderW, shoulderY, -10, shoulderY + 28, seed + 49, STROKE_COLOR, 4);
1161
+ sLine(-10, shoulderY + 28, 12, shoulderY + 14, seed + 50, STROKE_COLOR, 4);
1162
+ // Spear pole passing through hands
1163
+ const spearX = 18 + spearSway;
1164
+ sLine(spearX - 25, cy - 140, spearX + 35, cy + 180, seed + 51, '#78350f', 5.5);
1165
+ // Flint spearhead bound with sinew
1166
+ ctx.fillStyle = '#94a3b8';
1056
1167
  ctx.beginPath();
1057
- ctx.moveTo(spearX, -torsoHeight - 165);
1058
- ctx.lineTo(spearX - 10, -torsoHeight - 128);
1059
- ctx.lineTo(spearX + 10, -torsoHeight - 128);
1168
+ ctx.moveTo(spearX - 25, cy - 140);
1169
+ ctx.lineTo(spearX - 32, cy - 180);
1170
+ ctx.lineTo(spearX - 18, cy - 170);
1060
1171
  ctx.closePath();
1061
1172
  ctx.fill();
1062
- sLine(18, shoulderY, 35, shoulderY + 25 + Math.sin(t * 5.5) * 20, seed + 52, '#f8fafc', 4);
1063
- sLine(35, shoulderY + 25 + Math.sin(t * 5.5) * 20, 48, shoulderY + 45, seed + 53, '#f8fafc', 4);
1173
+ sLine(spearX - 28, cy - 140, spearX - 20, cy - 150, seed + 52, '#f59e0b', 3);
1064
1174
  } else if (armPose === 'sewing') {
1065
- sLine(-18, shoulderY, -35, shoulderY + 40, seed + 54, '#f8fafc', 4);
1066
- sLine(-35, shoulderY + 40, -18, shoulderY + 55, seed + 55, '#f8fafc', 4);
1067
- const sewCycle = Math.sin(t * 4);
1068
- const handX = 20 + sewCycle * 25;
1069
- const handY = shoulderY + 45 - Math.cos(t * 4) * 15;
1070
- sLine(18, shoulderY, 38, shoulderY + 25, seed + 56, '#f8fafc', 4);
1071
- sLine(38, shoulderY + 25, handX, handY, seed + 57, '#f8fafc', 4);
1072
- sLine(handX - 5, handY - 10, handX + 18, handY + 12, seed + 58, '#fef08a', 3.5);
1073
- ctx.strokeStyle = '#ffffff';
1074
- ctx.lineWidth = 1.5;
1075
- ctx.beginPath();
1076
- ctx.moveTo(handX - 5, handY - 10);
1077
- ctx.quadraticCurveTo(handX - 25, handY - 25, 0, hipY + 10);
1078
- ctx.stroke();
1175
+ // Left hand holds stretched pelt, right hand drives bone needle with sinew
1176
+ const sewPull = Math.sin(t * 4) * 14;
1177
+ sLine(-shoulderW, shoulderY, -40, shoulderY + 38, seed + 53, STROKE_COLOR, 4);
1178
+ sLine(-40, shoulderY + 38, -15, shoulderY + 45, seed + 54, STROKE_COLOR, 4);
1179
+ sLine(shoulderW, shoulderY, 35 + sewPull * 0.5, shoulderY + 28, seed + 55, STROKE_COLOR, 4);
1180
+ sLine(35 + sewPull * 0.5, shoulderY + 28, 10 + sewPull, shoulderY + 42, seed + 56, STROKE_COLOR, 4);
1079
1181
  } else if (armPose === 'cover_body') {
1080
- sLine(-18, shoulderY, -12, hipY - 10, seed + 59, '#f8fafc', 4);
1081
- sLine(18, shoulderY, 12, hipY - 10, seed + 60, '#f8fafc', 4);
1182
+ // Protective covering gesture
1183
+ sLine(-shoulderW, shoulderY, 10, shoulderY + 28, seed + 57, STROKE_COLOR, 4);
1184
+ sLine(shoulderW, shoulderY, -10, shoulderY + 48, seed + 58, STROKE_COLOR, 4);
1185
+ } else if (armPose === 'thinker') {
1186
+ // Rodin-style contemplative pose: hand under chin, elbow on knee
1187
+ sLine(shoulderW, shoulderY, 15, shoulderY + 42, seed + 59, STROKE_COLOR, 4);
1188
+ sLine(15, shoulderY + 42, 6, headCenterY + 20, seed + 60, STROKE_COLOR, 4);
1189
+ sLine(-shoulderW, shoulderY, -30, shoulderY + 45, seed + 61, STROKE_COLOR, 4);
1082
1190
  } else if (armPose === 'dress_shirt') {
1083
- sLine(-18, shoulderY, -38, shoulderY + 30, seed + 61, '#f8fafc', 4);
1084
- sLine(-38, shoulderY + 30, -22, hipY - 5, seed + 62, '#f8fafc', 4);
1085
- const adjustY = shoulderY + 8 + Math.sin(t * 3) * 6;
1086
- sLine(18, shoulderY, 35, shoulderY + 15, seed + 63, '#f8fafc', 4);
1087
- sLine(35, shoulderY + 15, 10, adjustY, seed + 64, '#f8fafc', 4);
1191
+ // Examining clothes, touching button / adjusting collar
1192
+ sLine(-shoulderW, shoulderY, -14, shoulderY + 22, seed + 62, STROKE_COLOR, 4);
1193
+ sLine(shoulderW, shoulderY, 14, shoulderY + 22, seed + 63, STROKE_COLOR, 4);
1088
1194
  } else if (armPose === 'peace_gesture') {
1089
- sLine(-18, shoulderY, -26, shoulderY + 40, seed + 65, '#f8fafc', 4);
1090
- sLine(18, shoulderY, 40, shoulderY - 10 + Math.sin(t * 2) * 5, seed + 66, '#f8fafc', 4);
1091
- sLine(40, shoulderY - 10 + Math.sin(t * 2) * 5, 45, shoulderY - 35, seed + 67, '#f8fafc', 4);
1092
- } else if (armPose === 'thinker') {
1093
- sLine(18, shoulderY, 36, hipY + 15, seed + 68, '#f8fafc', 4);
1094
- sLine(36, hipY + 15, 8, headCenterY + 18, seed + 69, '#f8fafc', 4);
1095
- sLine(-18, shoulderY, -30, hipY + 25, seed + 70, '#f8fafc', 4);
1195
+ // Open welcoming hand
1196
+ sLine(-shoulderW, shoulderY, -35, shoulderY + 30, seed + 64, STROKE_COLOR, 4);
1197
+ sLine(shoulderW, shoulderY, 35, shoulderY + 15, seed + 65, STROKE_COLOR, 4);
1198
+ sLine(35, shoulderY + 15, 52, shoulderY + 2, seed + 66, STROKE_COLOR, 3.5);
1096
1199
  } else {
1097
- const gArm = Math.sin(t * 2.5) * 12;
1098
- sLine(-18, shoulderY, -35, shoulderY + 35, seed + 71, '#f8fafc', 4);
1099
- sLine(-35, shoulderY + 35, -42, shoulderY + 60, seed + 72, '#f8fafc', 4);
1100
- sLine(18, shoulderY, 35, shoulderY + 20 - gArm, seed + 73, '#f8fafc', 4);
1101
- sLine(35, shoulderY + 20 - gArm, 45, shoulderY + 5 - gArm, seed + 74, '#f8fafc', 4);
1200
+ // Natural expressive speaking arms with subtle breathing gesture
1201
+ const gArm = Math.sin(t * 3.5) * 8;
1202
+ sLine(-shoulderW, shoulderY, -shoulderW - 8, shoulderY + 38, seed + 67, STROKE_COLOR, 4);
1203
+ sLine(-shoulderW - 8, shoulderY + 38, -shoulderW - 14, shoulderY + 68, seed + 68, STROKE_COLOR, 4);
1204
+ sLine(shoulderW, shoulderY, shoulderW + 12, shoulderY + 32 - gArm, seed + 69, STROKE_COLOR, 4);
1205
+ sLine(shoulderW + 12, shoulderY + 32 - gArm, shoulderW + 24, shoulderY + 18 - gArm, seed + 70, STROKE_COLOR, 4);
1102
1206
  }
1103
1207
 
1104
1208
  ctx.restore();
@@ -1110,7 +1214,7 @@ function drawMainScene(t) {
1110
1214
  const beat2 = spring(t, window.DURATION * 0.35, 180, 16);
1111
1215
  const beat3 = spring(t, window.DURATION * 0.70, 190, 16);
1112
1216
 
1113
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.25)', 2.5);
1217
+ sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2.5);
1114
1218
 
1115
1219
  // Clothes rack on left (sways in beat 1)
1116
1220
  sLine(cx - 360, cy + 220, cx - 360, cy - 140, 11, '#94a3b8', 5);
@@ -1174,44 +1278,120 @@ function drawMainScene(t) {
1174
1278
  seed: 101,
1175
1279
  });
1176
1280
 
1177
- } else if (COMP_THEME === 'doodle_shiver_cold') {
1178
- // Beat 1: Shivering in cold -> Beat 2: NOT FOR COLD stamp -> Beat 3: NOT FOR PROTECTION stamp
1179
- const stamp1 = spring(t, window.DURATION * 0.38, 220, 15);
1180
- const stamp2 = spring(t, window.DURATION * 0.72, 220, 15);
1281
+ } else if (COMP_THEME === 'doodle_modern_vs_prehistoric') {
1282
+ // Split-screen comparison: Modern stylish human on left vs Prehistoric human on right
1283
+ const dur = window.DURATION || 3;
1284
+ const bridgeSpring = spring(t, dur * 0.35, 170, 15);
1285
+
1286
+ // Natural ground line
1287
+ sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2.5);
1288
+
1289
+ // Subtle split-screen center divider
1290
+ ctx.save();
1291
+ ctx.setLineDash([10, 10]);
1292
+ sLine(cx, cy - 220, cx, cy + 200, 20, AMBIENT_LINE, 2);
1293
+ ctx.restore();
1294
+
1295
+ // Modern human on left
1296
+ drawDoodleCharacter({
1297
+ x: cx - 260,
1298
+ y: cy + 140,
1299
+ scale: 1.3,
1300
+ hairStyle: 'modern',
1301
+ expression: 'talking',
1302
+ clothing: 'shirt',
1303
+ armPose: 'dress_shirt',
1304
+ seed: 120,
1305
+ });
1181
1306
 
1182
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
1307
+ // Prehistoric human on right
1308
+ drawDoodleCharacter({
1309
+ x: cx + 260,
1310
+ y: cy + 140,
1311
+ scale: 1.3,
1312
+ hairStyle: 'wild',
1313
+ expression: 'curious',
1314
+ clothing: 'none',
1315
+ armPose: 'natural',
1316
+ seed: 121,
1317
+ });
1183
1318
 
1184
- // Freezing wind streaks rushing across
1185
- for (let w = 0; w < 14; w++) {
1186
- const windX = ((t * 750 + w * 210) % (W + 600)) - 300;
1187
- const windY = cy - 250 + (w * 42) + Math.sin(t * 3.5 + w) * 15;
1188
- sLine(windX, windY, windX + 170, windY + 5, 200 + w, 'rgba(255,255,255,0.3)', 2);
1319
+ // Dynamic timeline bridge / evolution arc connecting them
1320
+ if (bridgeSpring > 0.05) {
1321
+ ctx.save();
1322
+ ctx.globalAlpha = Math.min(0.85, bridgeSpring * 0.85);
1323
+ ctx.strokeStyle = ACCENT;
1324
+ ctx.lineWidth = 3;
1325
+ ctx.setLineDash([8, 8]);
1326
+ ctx.beginPath();
1327
+ ctx.moveTo(cx - 200, cy - 30);
1328
+ ctx.quadraticCurveTo(cx, cy - 140, cx + 200, cy - 30);
1329
+ ctx.stroke();
1330
+ ctx.setLineDash([]);
1331
+
1332
+ // Center compass / timeline knot
1333
+ sCircle(cx, cy - 85, 24, 820, ACCENT, 3, IS_LIGHT ? '#ffffff' : 'rgba(15, 23, 42, 0.9)');
1334
+ sCircle(cx, cy - 85, 10, 821, STROKE_COLOR, 2, STROKE_COLOR);
1335
+ ctx.restore();
1336
+ }
1337
+
1338
+ } else if (COMP_THEME === 'doodle_shiver_cold') {
1339
+ // Narrative beat progression based strictly on audio duration (window.DURATION)
1340
+ const dur = window.DURATION || 3;
1341
+ const beat2 = spring(t, dur * 0.45, 160, 15);
1342
+
1343
+ // Natural ground line
1344
+ sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
1345
+
1346
+ // Freezing wind streaks (rush during cold beat, calm as sun breaks)
1347
+ const windAlpha = Math.max(0, 1 - beat2 * 0.85);
1348
+ if (windAlpha > 0.05) {
1349
+ for (let w = 0; w < 14; w++) {
1350
+ const windX = ((t * 750 + w * 210) % (W + 600)) - 300;
1351
+ const windY = cy - 250 + (w * 42) + Math.sin(t * 3.5 + w) * 15;
1352
+ const windColor = (IS_LIGHT ? 'rgba(100,116,139,' : 'rgba(255,255,255,') + (0.35 * windAlpha) + ')';
1353
+ sLine(windX, windY, windX + 170, windY + 5, 200 + w, windColor, 2);
1354
+ }
1189
1355
  }
1190
1356
 
1191
1357
  // Drifting snow particles
1192
- for (let sp = 0; sp < 25; sp++) {
1193
- const px = ((sp * 85 + t * 450) % W);
1194
- const py = (sp * 45 + t * 90) % H;
1195
- sCircle(px, py, 2.5, 300 + sp, '#e0f2fe', 1, '#ffffff');
1358
+ for (let sp = 0; sp < 22; sp++) {
1359
+ const px = ((sp * 85 + t * 400) % W);
1360
+ const py = (sp * 45 + t * 80) % H;
1361
+ sCircle(px, py, 2.2, 300 + sp, '#e0f2fe', 1, '#ffffff');
1196
1362
  }
1197
1363
 
1198
- // Shivering breath vapor cloud (calms in Beat 3)
1199
- if (stamp2 < 0.8) {
1364
+ // Shivering breath vapor cloud (calms in Beat 2)
1365
+ if (beat2 < 0.7) {
1200
1366
  const breathPhase = (t * 2.2) % 1;
1201
1367
  const bCloudX = cx - 50 + breathPhase * 60;
1202
1368
  const bCloudY = cy - 90 - breathPhase * 25;
1203
- ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.5) + ')';
1369
+ ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.45) + ')';
1204
1370
  ctx.beginPath();
1205
1371
  ctx.arc(bCloudX, bCloudY, 15 + breathPhase * 25, 0, Math.PI * 2);
1206
1372
  ctx.fill();
1207
1373
  }
1208
1374
 
1209
- // Main character: shivers in Beat 1 & 2, then realizes / curious in Beat 3
1210
- const isShivering = stamp2 < 0.7;
1375
+ // Warm golden sunlight breaks in (visual myth-busting revelation)
1376
+ if (beat2 > 0.05) {
1377
+ ctx.save();
1378
+ ctx.globalAlpha = Math.min(0.6, beat2 * 0.6);
1379
+ const sunBeamGrad = ctx.createRadialGradient(cx + 180, cy - 220, 20, cx + 180, cy - 100, 320);
1380
+ sunBeamGrad.addColorStop(0, 'rgba(251, 191, 36, 0.4)');
1381
+ sunBeamGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
1382
+ ctx.fillStyle = sunBeamGrad;
1383
+ ctx.beginPath();
1384
+ ctx.arc(cx + 180, cy - 180, 260, 0, Math.PI * 2);
1385
+ ctx.fill();
1386
+ ctx.restore();
1387
+ }
1388
+
1389
+ // Main character: shivers in cold beat, then pauses and looks at arms in wonder as sun illuminates
1390
+ const isShivering = beat2 < 0.6;
1211
1391
  const vibX = isShivering ? Math.sin(t * 38) * 4.5 : 0;
1212
1392
  const vibY = isShivering ? Math.cos(t * 32) * 2 : 0;
1213
1393
  drawDoodleCharacter({
1214
- x: cx - 120 + vibX,
1394
+ x: cx - 100 + beat2 * 80 + vibX,
1215
1395
  y: cy + 140 + vibY,
1216
1396
  scale: 1.35,
1217
1397
  hairStyle: 'wild',
@@ -1221,38 +1401,6 @@ function drawMainScene(t) {
1221
1401
  seed: 102,
1222
1402
  });
1223
1403
 
1224
- // Stamp 1: NOT FOR COLD (slams down in Beat 2)
1225
- if (stamp1 > 0.05) {
1226
- ctx.save();
1227
- const s1X = cx + 240;
1228
- const s1Y = cy - 100;
1229
- ctx.translate(s1X, s1Y);
1230
- ctx.scale(stamp1, stamp1);
1231
- sCircle(0, 0, 50, 400, 'rgba(239,68,68,0.9)', 4);
1232
- sLine(-36, -36, 36, 36, 401, 'rgba(239,68,68,0.95)', 4.5);
1233
- ctx.font = 'bold 20px monospace';
1234
- ctx.fillStyle = '#ef4444';
1235
- ctx.textAlign = 'center';
1236
- ctx.fillText('NOT FOR COLD', 0, 75);
1237
- ctx.restore();
1238
- }
1239
-
1240
- // Stamp 2: NOT FOR PROTECTION (slams down in Beat 3)
1241
- if (stamp2 > 0.05) {
1242
- ctx.save();
1243
- const s2X = cx + 240;
1244
- const s2Y = cy + 90;
1245
- ctx.translate(s2X, s2Y);
1246
- ctx.scale(stamp2, stamp2);
1247
- sCircle(0, 0, 50, 410, 'rgba(239,68,68,0.9)', 4);
1248
- sLine(-36, -36, 36, 36, 411, 'rgba(239,68,68,0.95)', 4.5);
1249
- ctx.font = 'bold 20px monospace';
1250
- ctx.fillStyle = '#ef4444';
1251
- ctx.textAlign = 'center';
1252
- ctx.fillText('NOT FOR PROTECTION', 0, 75);
1253
- ctx.restore();
1254
- }
1255
-
1256
1404
  } else if (COMP_THEME === 'doodle_hunter_savannah') {
1257
1405
  // Beat 1: Walking with spear -> Beat 2: Swatting mosquitoes -> Beat 3: Pausing alert
1258
1406
  const walkProg = Math.min(1, Math.max(0, t / window.DURATION));
@@ -1369,15 +1517,12 @@ function drawMainScene(t) {
1369
1517
  ctx.restore();
1370
1518
  }
1371
1519
 
1372
- // Timeline badge in Beat 3
1520
+ // Ancient stone strata and bone needle engraving in Beat 3
1373
1521
  if (badgeSpring > 0.05) {
1374
1522
  ctx.save();
1375
- ctx.globalAlpha = Math.min(1, badgeSpring);
1376
- sRect(cx + 120, cy + 140, 300, 42, 670, '#d4af37', 2.5, 'rgba(15, 23, 42, 0.85)');
1377
- ctx.font = 'bold 16px monospace';
1378
- ctx.fillStyle = '#fef08a';
1379
- ctx.textAlign = 'center';
1380
- ctx.fillText('BONE NEEDLE • 120,000 YEARS', cx + 270, cy + 167);
1523
+ ctx.globalAlpha = Math.min(0.65, badgeSpring * 0.65);
1524
+ sLine(cx + 140, cy + 150, cx + 420, cy + 150, 670, '#d4af37', 2);
1525
+ sLine(cx + 180, cy + 168, cx + 380, cy + 168, 671, '#d4af37', 1.5);
1381
1526
  ctx.restore();
1382
1527
  }
1383
1528
 
@@ -1452,12 +1597,8 @@ function drawMainScene(t) {
1452
1597
  ctx.translate(haloX, haloY);
1453
1598
  ctx.scale(haloSpring, haloSpring);
1454
1599
  sCircle(0, 0, 48, 760, '#38bdf8', 3);
1455
- sCircle(0, 0, 18, 761, '#ffffff', 2.5);
1600
+ sCircle(0, 0, 24, 761, STROKE_ALT, 2);
1456
1601
  sCircle(0, 0, 8, 762, '#38bdf8', 1, '#38bdf8');
1457
- ctx.font = 'bold 16px monospace';
1458
- ctx.fillStyle = '#38bdf8';
1459
- ctx.textAlign = 'center';
1460
- ctx.fillText('THEORY OF MIND', 0, 72);
1461
1602
  ctx.restore();
1462
1603
  }
1463
1604
 
@@ -1467,7 +1608,7 @@ function drawMainScene(t) {
1467
1608
  const greet = spring(t, window.DURATION * 0.38, 190, 15);
1468
1609
  const arcSpring = spring(t, window.DURATION * 0.68, 200, 15);
1469
1610
 
1470
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
1611
+ sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
1471
1612
 
1472
1613
  // Left Clan Member walks toward center
1473
1614
  const leftX = cx - 380 + Math.min(1, approach) * 160;
@@ -1495,10 +1636,10 @@ function drawMainScene(t) {
1495
1636
  seed: 107,
1496
1637
  });
1497
1638
 
1498
- // Glowing connection arc and symbols in Beat 3
1639
+ // Glowing connection arc in Beat 3
1499
1640
  if (arcSpring > 0.05) {
1500
1641
  ctx.save();
1501
- ctx.globalAlpha = Math.min(1, arcSpring);
1642
+ ctx.globalAlpha = Math.min(0.85, arcSpring * 0.85);
1502
1643
  ctx.strokeStyle = ACCENT;
1503
1644
  ctx.lineWidth = 3;
1504
1645
  ctx.setLineDash([8, 8]);
@@ -1508,13 +1649,8 @@ function drawMainScene(t) {
1508
1649
  ctx.stroke();
1509
1650
  ctx.setLineDash([]);
1510
1651
 
1511
- sCircle(cx, cy - 65, 34, 800, ACCENT, 3, 'rgba(15, 23, 42, 0.85)');
1512
- ctx.font = 'bold 22px monospace';
1513
- ctx.fillStyle = ACCENT;
1514
- ctx.textAlign = 'center';
1515
- ctx.fillText('🤝', cx, cy - 56);
1516
- ctx.font = 'bold 16px monospace';
1517
- ctx.fillText('CLOTHING AS SOCIAL LANGUAGE', cx, cy + 15);
1652
+ sCircle(cx, cy - 65, 28, 800, ACCENT, 3, IS_LIGHT ? '#ffffff' : 'rgba(15, 23, 42, 0.85)');
1653
+ sCircle(cx, cy - 65, 12, 801, STROKE_COLOR, 2, STROKE_COLOR);
1518
1654
  ctx.restore();
1519
1655
  }
1520
1656
 
@@ -1534,7 +1670,7 @@ function drawMainScene(t) {
1534
1670
  ctx.fill();
1535
1671
 
1536
1672
  // Large rock boulder for thinker
1537
- ctx.fillStyle = '#334155';
1673
+ ctx.fillStyle = IS_LIGHT ? '#78716c' : '#334155';
1538
1674
  ctx.beginPath();
1539
1675
  ctx.ellipse(cx - 140, cy + 175, 85, 55, 0, 0, Math.PI * 2);
1540
1676
  ctx.fill();
@@ -1559,9 +1695,9 @@ function drawMainScene(t) {
1559
1695
  ctx.translate(cloudX, cloudY);
1560
1696
  ctx.scale(cloudSpring, cloudSpring);
1561
1697
 
1562
- sCircle(-180, 160, 12, 850, '#ffffff', 2, '#ffffff');
1563
- sCircle(-140, 120, 18, 851, '#ffffff', 2, '#ffffff');
1564
- sCircle(-90, 70, 24, 852, '#ffffff', 2, '#ffffff');
1698
+ sCircle(-180, 160, 12, 850, STROKE_COLOR, 2, '#ffffff');
1699
+ sCircle(-140, 120, 18, 851, STROKE_COLOR, 2, '#ffffff');
1700
+ sCircle(-90, 70, 24, 852, STROKE_COLOR, 2, '#ffffff');
1565
1701
 
1566
1702
  ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
1567
1703
  ctx.beginPath();
@@ -1570,6 +1706,9 @@ function drawMainScene(t) {
1570
1706
  ctx.arc(70, 0, 55, 0, Math.PI * 2);
1571
1707
  ctx.arc(0, 30, 60, 0, Math.PI * 2);
1572
1708
  ctx.fill();
1709
+ ctx.strokeStyle = STROKE_COLOR;
1710
+ ctx.lineWidth = 2.5;
1711
+ ctx.stroke();
1573
1712
 
1574
1713
  // Inside thought cloud: Tomorrow's sunrise & stretched hide on rack in Beat 3
1575
1714
  if (ideaSpring > 0.1) {
@@ -1584,11 +1723,6 @@ function drawMainScene(t) {
1584
1723
 
1585
1724
  ctx.fillStyle = '#b45309';
1586
1725
  ctx.fillRect(-28, -20, 56, 32);
1587
-
1588
- ctx.font = 'bold 15px monospace';
1589
- ctx.fillStyle = '#d97706';
1590
- ctx.textAlign = 'center';
1591
- ctx.fillText('💡 PLANNING FOR TOMORROW', 0, 52);
1592
1726
  }
1593
1727
  ctx.restore();
1594
1728
  }
@@ -1598,7 +1732,7 @@ function drawMainScene(t) {
1598
1732
  const scopeSpring = spring(t, window.DURATION * 0.30, 180, 15);
1599
1733
  const treeSpring = spring(t, window.DURATION * 0.65, 200, 15);
1600
1734
 
1601
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
1735
+ sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
1602
1736
 
1603
1737
  // Early human scratching pelt with itch lines
1604
1738
  drawDoodleCharacter({
@@ -1647,28 +1781,18 @@ function drawMainScene(t) {
1647
1781
  sLine(-100, 0, -20, 0, 940, '#f8fafc', 4);
1648
1782
  sCircle(-20, 0, 7, 941, '#d4af37', 2, '#d4af37');
1649
1783
 
1650
- sLine(-20, 0, 60, -45, 942, '#38bdf8', 4);
1651
- ctx.font = 'bold 14px monospace';
1652
- ctx.fillStyle = '#38bdf8';
1653
- ctx.textAlign = 'left';
1654
- ctx.fillText('HEAD LICE (HAIR)', 70, -42);
1655
-
1656
- sLine(-20, 0, 60, 45, 943, '#f43f5e', 4);
1657
- ctx.fillStyle = '#f43f5e';
1658
- ctx.fillText('BODY LICE (CLOTHES)', 70, 48);
1659
-
1660
- sRect(-90, 80, 180, 32, 950, '#d4af37', 2, 'rgba(212, 175, 55, 0.25)');
1661
- ctx.font = 'bold 14px monospace';
1662
- ctx.fillStyle = '#fef08a';
1663
- ctx.textAlign = 'center';
1664
- ctx.fillText('170,000 YEARS AGO', 0, 101);
1784
+ sLine(-20, 0, 75, -45, 942, '#38bdf8', 4);
1785
+ sCircle(82, -45, 8, 943, '#38bdf8', 2, '#38bdf8');
1786
+
1787
+ sLine(-20, 0, 75, 45, 944, '#f43f5e', 4);
1788
+ sCircle(82, 45, 8, 945, '#f43f5e', 2, '#f43f5e');
1665
1789
  }
1666
1790
  ctx.restore();
1667
1791
  }
1668
1792
 
1669
1793
  } else if (COMP_THEME === 'doodle_general_human') {
1670
1794
  // General Expressive Doodled Human Narrator
1671
- sLine(cx - W * 0.4, cy + 220, cx + W * 0.4, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
1795
+ sLine(cx - W * 0.4, cy + 220, cx + W * 0.4, cy + 220, 10, AMBIENT_LINE, 2);
1672
1796
  drawDoodleCharacter({
1673
1797
  x: cx,
1674
1798
  y: cy + 140,
@@ -116,7 +116,8 @@ async function runAssets({
116
116
  camera: scene.camera_hint || scene.camera || 'static',
117
117
  focalPoint: scene.focal_point || null,
118
118
  narration: scene.narration || '',
119
- visualDescription: scene.visual_description || scene.narration || '',
119
+ prompt: scene.prompt || '',
120
+ visualDescription: scene.visual_description || scene.prompt || scene.narration || '',
120
121
  topic: scene.topic || topic || '',
121
122
  style: scene.style || style || '',
122
123
  theme: scene.theme || theme || '',
@@ -167,8 +167,11 @@ function normaliseIngestScene(raw, index, baseDir = process.cwd()) {
167
167
  schema_version: '1.0.0',
168
168
  narration,
169
169
  caption,
170
+ prompt: String(raw.prompt || '').trim(),
171
+ style: String(raw.style || '').trim(),
172
+ theme: String(raw.theme || '').trim(),
170
173
  // visual_description used by AI image generation — blank means no AI image needed
171
- visual_description: String(raw.visual_description || raw.description || narration || '').trim(),
174
+ visual_description: String(raw.visual_description || raw.description || raw.prompt || narration || '').trim(),
172
175
  video_path: videoPath,
173
176
  image_path: imagePath,
174
177
  audio_path: audioPath,