@amg555/frameforge-mcp 2.4.4 → 2.4.6

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.
@@ -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;
@@ -863,6 +924,7 @@ function drawMainScene(t) {
863
924
  ctx.restore();
864
925
  }
865
926
 
927
+ // ── High-Quality Illustrated 2D Character Renderer (Volumetric Anatomy, Pure-t) ──
866
928
  function drawDoodleCharacter({
867
929
  x = cx,
868
930
  y = cy,
@@ -874,286 +936,827 @@ function drawMainScene(t) {
874
936
  expression = 'talking',
875
937
  clothing = 'none',
876
938
  armPose = 'natural',
939
+ showShadow = false,
940
+ showBlush = false,
941
+ showSpecular = false,
877
942
  seed = 100,
878
943
  } = {}) {
879
944
  ctx.save();
880
945
  ctx.translate(x, y);
881
946
  ctx.scale(scale, scale);
882
947
 
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();
948
+ // Ground contact drop shadow
949
+ if (showShadow) {
950
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.32)';
951
+ ctx.beginPath();
952
+ ctx.ellipse(0, legLength + 6, 44, 9, 0, 0, Math.PI * 2);
953
+ ctx.fill();
954
+ }
888
955
 
889
956
  const blink = (t % 3.2 < 0.14) ? 0.15 : 1.0;
890
- const mouthTalk = Math.abs(Math.sin(t * 14)) * 4;
891
957
  const torsoTopY = -torsoHeight;
892
958
  const torsoBottomY = 0;
959
+ const shoulderW = 34;
960
+
961
+ // Adaptive warm human skin tones & natural anatomical outline strokes
962
+ const isModern = (clothing === 'shirt' || hairStyle === 'modern');
963
+ const skinBase = isModern ? '#f3c7a5' : '#c98a58';
964
+ const skinShadow = isModern ? '#d49b73' : '#a26535';
965
+ const skinHighlight = isModern ? '#fce7d2' : '#deaa7c';
966
+
967
+ // Character outlines: natural anatomical tones
968
+ const charStroke = isModern ? '#0f172a' : '#382110';
969
+ const bodyStroke = isModern ? '#1e293b' : skinShadow;
970
+ const armStroke = (clothing === 'shirt') ? '#334155' : skinShadow;
971
+ const armFill = (clothing === 'shirt') ? '#0f172a' : skinBase;
972
+
973
+ // ── 1. LEGS & FEET (Volumetric contoured anatomy - never single stick lines) ──
974
+ const hipY = torsoBottomY;
975
+ if (armPose === 'sewing' || armPose === 'thinker') {
976
+ // Seated cross-legged posture with folded thighs, knees, and shins
977
+ ctx.save();
978
+ ctx.fillStyle = (clothing === 'shirt') ? '#1e293b' : skinBase;
979
+ ctx.beginPath();
980
+ ctx.moveTo(-16, hipY - 6);
981
+ ctx.quadraticCurveTo(-48, hipY + 12, -56, hipY + 44);
982
+ ctx.quadraticCurveTo(-38, hipY + 54, 8, hipY + 52);
983
+ ctx.quadraticCurveTo(14, hipY + 40, -10, hipY + 36);
984
+ ctx.quadraticCurveTo(-32, hipY + 32, -14, hipY);
985
+ ctx.closePath();
986
+ ctx.fill();
987
+ sLine(-16, hipY - 6, -56, hipY + 44, seed + 35, bodyStroke, 2.5);
988
+ sLine(-56, hipY + 44, 8, hipY + 52, seed + 36, bodyStroke, 2.5);
989
+
990
+ ctx.beginPath();
991
+ ctx.moveTo(16, hipY - 6);
992
+ ctx.quadraticCurveTo(48, hipY + 12, 56, hipY + 44);
993
+ ctx.quadraticCurveTo(38, hipY + 54, -8, hipY + 52);
994
+ ctx.quadraticCurveTo(-14, hipY + 40, 10, hipY + 36);
995
+ ctx.quadraticCurveTo(32, hipY + 32, 14, hipY);
996
+ ctx.closePath();
997
+ ctx.fill();
998
+ sLine(16, hipY - 6, 56, hipY + 44, seed + 37, bodyStroke, 2.5);
999
+ sLine(56, hipY + 44, -8, hipY + 52, seed + 38, bodyStroke, 2.5);
1000
+
1001
+ if (clothing !== 'shirt') {
1002
+ ctx.fillStyle = skinBase;
1003
+ ctx.beginPath(); ctx.ellipse(-14, hipY + 48, 10, 6, -0.2, 0, Math.PI * 2); ctx.fill();
1004
+ ctx.beginPath(); ctx.ellipse(14, hipY + 48, 10, 6, 0.2, 0, Math.PI * 2); ctx.fill();
1005
+ }
1006
+ ctx.restore();
1007
+ } else if (armPose === 'hold_spear') {
1008
+ const legSwing = Math.sin(t * 5.5);
1009
+ const l1KneeX = -18 + legSwing * 24;
1010
+ const l1KneeY = hipY + legLength * 0.52;
1011
+ const l1FootX = -18 + legSwing * 42;
1012
+ const l1FootY = hipY + legLength;
1013
+
1014
+ ctx.fillStyle = skinBase;
1015
+ ctx.beginPath();
1016
+ ctx.moveTo(-22, hipY);
1017
+ ctx.quadraticCurveTo(l1KneeX - 12, l1KneeY - 14, l1KneeX - 8, l1KneeY);
1018
+ ctx.quadraticCurveTo(l1FootX - 10, l1FootY - 12, l1FootX - 4, l1FootY);
1019
+ ctx.lineTo(l1FootX + 16, l1FootY);
1020
+ ctx.quadraticCurveTo(l1FootX + 4, l1FootY - 12, l1KneeX + 6, l1KneeY);
1021
+ ctx.quadraticCurveTo(l1KneeX + 8, l1KneeY - 16, -6, hipY);
1022
+ ctx.closePath();
1023
+ ctx.fill();
1024
+ sLine(-22, hipY, l1KneeX - 8, l1KneeY, seed + 39, bodyStroke, 2);
1025
+ sLine(l1KneeX - 8, l1KneeY, l1FootX - 4, l1FootY, seed + 40, bodyStroke, 2);
1026
+
1027
+ ctx.fillStyle = skinBase;
1028
+ ctx.beginPath();
1029
+ ctx.ellipse(l1FootX + 6, l1FootY + 2, 12, 5, 0, 0, Math.PI * 2);
1030
+ ctx.fill();
1031
+ for (let toe = 0; toe < 4; toe++) {
1032
+ sCircle(l1FootX + 10 + toe * 2.5, l1FootY + 2, 1.5, seed + 80 + toe, skinShadow, 1, skinHighlight);
1033
+ }
1034
+
1035
+ const l2KneeX = 18 - legSwing * 24;
1036
+ const l2KneeY = hipY + legLength * 0.52;
1037
+ const l2FootX = 18 - legSwing * 42;
1038
+ const l2FootY = hipY + legLength;
893
1039
 
894
- // Torso Clothing Fills
895
- if (clothing === 'pelt') {
896
- ctx.fillStyle = '#b45309';
897
1040
  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);
1041
+ ctx.moveTo(6, hipY);
1042
+ ctx.quadraticCurveTo(l2KneeX - 8, l2KneeY - 16, l2KneeX - 6, l2KneeY);
1043
+ ctx.quadraticCurveTo(l2FootX - 4, l2FootY - 12, l2FootX - 2, l2FootY);
1044
+ ctx.lineTo(l2FootX + 16, l2FootY);
1045
+ ctx.quadraticCurveTo(l2FootX + 10, l2FootY - 12, l2KneeX + 8, l2KneeY);
1046
+ ctx.quadraticCurveTo(l2KneeX + 12, l2KneeY - 14, 22, hipY);
902
1047
  ctx.closePath();
903
1048
  ctx.fill();
904
- sLine(-28, torsoBottomY + 2, 32, torsoBottomY + 2, seed + 4, '#78350f', 4);
905
- } else if (clothing === 'shirt') {
1049
+ sLine(6, hipY, l2KneeX - 6, l2KneeY, seed + 42, bodyStroke, 2);
1050
+ sLine(l2KneeX - 6, l2KneeY, l2FootX - 2, l2FootY, seed + 43, bodyStroke, 2);
1051
+
1052
+ ctx.beginPath();
1053
+ ctx.ellipse(l2FootX + 6, l2FootY + 2, 12, 5, 0, 0, Math.PI * 2);
1054
+ ctx.fill();
1055
+ for (let toe = 0; toe < 4; toe++) {
1056
+ sCircle(l2FootX + 10 + toe * 2.5, l2FootY + 2, 1.5, seed + 85 + toe, skinShadow, 1, skinHighlight);
1057
+ }
1058
+ } else {
1059
+ const kneeY = hipY + legLength * 0.52;
1060
+ const footY = hipY + legLength;
1061
+
1062
+ if (clothing === 'shirt') {
1063
+ const pantColor = '#1e293b';
1064
+ ctx.fillStyle = pantColor;
1065
+
1066
+ ctx.beginPath();
1067
+ ctx.moveTo(-30, hipY);
1068
+ ctx.quadraticCurveTo(-26, kneeY - 10, -22, kneeY);
1069
+ ctx.quadraticCurveTo(-20, footY - 10, -21, footY);
1070
+ ctx.lineTo(-7, footY);
1071
+ ctx.quadraticCurveTo(-8, kneeY + 10, -9, kneeY);
1072
+ ctx.quadraticCurveTo(-7, hipY + 10, -2, hipY);
1073
+ ctx.closePath();
1074
+ ctx.fill();
1075
+ sLine(-30, hipY, -22, kneeY, seed + 41, '#334155', 2);
1076
+ sLine(-22, kneeY, -21, footY, seed + 42, '#334155', 2);
1077
+ sLine(-15, hipY + 10, -14, footY - 2, seed + 43, '#475569', 1.5);
1078
+
1079
+ ctx.beginPath();
1080
+ ctx.moveTo(2, hipY);
1081
+ ctx.quadraticCurveTo(7, hipY + 10, 9, kneeY);
1082
+ ctx.quadraticCurveTo(8, footY - 10, 7, footY);
1083
+ ctx.lineTo(21, footY);
1084
+ ctx.quadraticCurveTo(20, footY - 10, 22, kneeY);
1085
+ ctx.quadraticCurveTo(26, kneeY - 10, 30, hipY);
1086
+ ctx.closePath();
1087
+ ctx.fill();
1088
+ sLine(30, hipY, 22, kneeY, seed + 44, '#334155', 2);
1089
+ sLine(22, kneeY, 21, footY, seed + 45, '#334155', 2);
1090
+ sLine(15, hipY + 10, 14, footY - 2, seed + 46, '#475569', 1.5);
1091
+
1092
+ ctx.fillStyle = '#0f172a';
1093
+ ctx.beginPath();
1094
+ ctx.moveTo(-22, footY);
1095
+ ctx.lineTo(-24, footY + 7);
1096
+ ctx.lineTo(-6, footY + 7);
1097
+ ctx.quadraticCurveTo(-4, footY + 4, -5, footY);
1098
+ ctx.closePath();
1099
+ ctx.fill();
1100
+ ctx.fillStyle = '#64748b';
1101
+ ctx.fillRect(-18, footY + 1, 8, 2);
1102
+
1103
+ ctx.fillStyle = '#0f172a';
1104
+ ctx.beginPath();
1105
+ ctx.moveTo(5, footY);
1106
+ ctx.lineTo(4, footY + 7);
1107
+ ctx.lineTo(24, footY + 7);
1108
+ ctx.lineTo(22, footY);
1109
+ ctx.closePath();
1110
+ ctx.fill();
1111
+ ctx.fillStyle = '#64748b';
1112
+ ctx.fillRect(10, footY + 1, 8, 2);
1113
+
1114
+ } else {
1115
+ ctx.fillStyle = skinBase;
1116
+ ctx.beginPath();
1117
+ ctx.moveTo(-26, hipY);
1118
+ ctx.quadraticCurveTo(-24, kneeY - 12, -20, kneeY);
1119
+ ctx.quadraticCurveTo(-22, kneeY + 18, -18, footY - 4);
1120
+ ctx.lineTo(-20, footY);
1121
+ ctx.lineTo(-6, footY);
1122
+ ctx.quadraticCurveTo(-7, footY - 10, -8, kneeY);
1123
+ ctx.quadraticCurveTo(-10, hipY + 14, -2, hipY);
1124
+ ctx.closePath();
1125
+ ctx.fill();
1126
+ sLine(-26, hipY, -20, kneeY, seed + 41, skinShadow, 2);
1127
+ sLine(-20, kneeY, -18, footY - 4, seed + 42, skinShadow, 2);
1128
+
1129
+ ctx.fillStyle = skinHighlight;
1130
+ ctx.beginPath(); ctx.ellipse(-14, kneeY, 4, 3, 0, 0, Math.PI * 2); ctx.fill();
1131
+
1132
+ ctx.fillStyle = skinBase;
1133
+ ctx.beginPath();
1134
+ ctx.moveTo(-20, footY);
1135
+ ctx.quadraticCurveTo(-24, footY + 6, -22, footY + 6);
1136
+ ctx.lineTo(-4, footY + 6);
1137
+ ctx.quadraticCurveTo(-4, footY + 2, -6, footY);
1138
+ ctx.closePath();
1139
+ ctx.fill();
1140
+ for (let toe = 0; toe < 5; toe++) {
1141
+ sCircle(-20 + toe * 3.6, footY + 5, 1.5, seed + 90 + toe, skinShadow, 0.8, skinHighlight);
1142
+ }
1143
+
1144
+ ctx.fillStyle = skinBase;
1145
+ ctx.beginPath();
1146
+ ctx.moveTo(2, hipY);
1147
+ ctx.quadraticCurveTo(10, hipY + 14, 8, kneeY);
1148
+ ctx.quadraticCurveTo(7, footY - 10, 6, footY);
1149
+ ctx.lineTo(20, footY);
1150
+ ctx.lineTo(18, footY - 4);
1151
+ ctx.quadraticCurveTo(22, kneeY + 18, 20, kneeY);
1152
+ ctx.quadraticCurveTo(24, kneeY - 12, 26, hipY);
1153
+ ctx.closePath();
1154
+ ctx.fill();
1155
+ sLine(26, hipY, 20, kneeY, seed + 44, skinShadow, 2);
1156
+ sLine(20, kneeY, 18, footY - 4, seed + 45, skinShadow, 2);
1157
+
1158
+ ctx.fillStyle = skinHighlight;
1159
+ ctx.beginPath(); ctx.ellipse(14, kneeY, 4, 3, 0, 0, Math.PI * 2); ctx.fill();
1160
+
1161
+ ctx.fillStyle = skinBase;
1162
+ ctx.beginPath();
1163
+ ctx.moveTo(6, footY);
1164
+ ctx.quadraticCurveTo(4, footY + 2, 4, footY + 6);
1165
+ ctx.lineTo(22, footY + 6);
1166
+ ctx.quadraticCurveTo(24, footY + 6, 20, footY);
1167
+ ctx.closePath();
1168
+ ctx.fill();
1169
+ for (let toe = 0; toe < 5; toe++) {
1170
+ sCircle(6 + toe * 3.6, footY + 5, 1.5, seed + 95 + toe, skinShadow, 0.8, skinHighlight);
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ // ── 2. TORSO & CLOTHING ──
1176
+ if (clothing === 'shirt') {
1177
+ ctx.fillStyle = '#0f172a';
1178
+ ctx.beginPath();
1179
+ ctx.moveTo(-shoulderW - 2, torsoTopY + 6);
1180
+ ctx.lineTo(shoulderW + 2, torsoTopY + 6);
1181
+ ctx.quadraticCurveTo(shoulderW - 2, torsoTopY + torsoHeight * 0.5, shoulderW - 4, torsoBottomY);
1182
+ ctx.lineTo(-shoulderW + 4, torsoBottomY);
1183
+ ctx.quadraticCurveTo(-shoulderW + 2, torsoTopY + torsoHeight * 0.5, -shoulderW - 2, torsoTopY + 6);
1184
+ ctx.closePath();
1185
+ ctx.fill();
1186
+
1187
+ ctx.fillStyle = '#f8fafc';
1188
+ ctx.beginPath();
1189
+ ctx.moveTo(-12, torsoTopY + 6);
1190
+ ctx.lineTo(12, torsoTopY + 6);
1191
+ ctx.lineTo(0, torsoTopY + 48);
1192
+ ctx.closePath();
1193
+ ctx.fill();
1194
+
906
1195
  ctx.fillStyle = ACCENT;
907
1196
  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);
1197
+ ctx.moveTo(-4, torsoTopY + 12);
1198
+ ctx.lineTo(4, torsoTopY + 12);
1199
+ ctx.lineTo(6, torsoTopY + 42);
1200
+ ctx.lineTo(0, torsoTopY + 52);
1201
+ ctx.lineTo(-6, torsoTopY + 42);
912
1202
  ctx.closePath();
913
1203
  ctx.fill();
914
- sLine(-14, torsoTopY, 0, torsoTopY + 18, seed + 4, '#ffffff', 3);
915
- sLine(14, torsoTopY, 0, torsoTopY + 18, seed + 5, '#ffffff', 3);
916
- sLine(0, torsoTopY + 18, 0, torsoBottomY + 10, seed + 6, '#ffffff', 2.5);
1204
+
1205
+ ctx.fillStyle = '#1e293b';
1206
+ ctx.beginPath();
1207
+ ctx.moveTo(-shoulderW - 2, torsoTopY + 6);
1208
+ ctx.lineTo(-14, torsoTopY + 6);
1209
+ ctx.lineTo(-4, torsoTopY + 46);
1210
+ ctx.lineTo(-18, torsoTopY + 42);
1211
+ ctx.closePath();
1212
+ ctx.fill();
1213
+ sLine(-14, torsoTopY + 6, -4, torsoTopY + 46, seed + 4, '#334155', 2);
1214
+
1215
+ ctx.beginPath();
1216
+ ctx.moveTo(shoulderW + 2, torsoTopY + 6);
1217
+ ctx.lineTo(14, torsoTopY + 6);
1218
+ ctx.lineTo(4, torsoTopY + 46);
1219
+ ctx.lineTo(18, torsoTopY + 42);
1220
+ ctx.closePath();
1221
+ ctx.fill();
1222
+ sLine(14, torsoTopY + 6, 4, torsoTopY + 46, seed + 5, '#334155', 2);
1223
+
1224
+ sLine(0, torsoTopY + 48, 0, torsoBottomY, seed + 6, '#334155', 2);
1225
+ sCircle(0, torsoTopY + 60, 2.5, seed + 7, '#cbd5e1', 1, '#94a3b8');
1226
+ sCircle(0, torsoTopY + 76, 2.5, seed + 8, '#cbd5e1', 1, '#94a3b8');
1227
+
1228
+ ctx.fillStyle = '#0f172a';
1229
+ ctx.fillRect(-shoulderW + 4, torsoBottomY - 8, (shoulderW - 4) * 2, 8);
1230
+ ctx.strokeStyle = '#d4af37';
1231
+ ctx.lineWidth = 2;
1232
+ ctx.strokeRect(-8, torsoBottomY - 9, 16, 10);
1233
+
1234
+ } else if (clothing === 'pelt') {
1235
+ ctx.fillStyle = '#b45309';
1236
+ ctx.beginPath();
1237
+ ctx.moveTo(-shoulderW + 2, torsoTopY + 8);
1238
+ ctx.lineTo(shoulderW - 4, torsoTopY + 8);
1239
+ ctx.quadraticCurveTo(shoulderW + 8, torsoTopY + torsoHeight * 0.5, shoulderW + 6, torsoBottomY + 18);
1240
+ for (let j = 0; j < 6; j++) {
1241
+ const jx = (shoulderW + 6) - j * ((shoulderW * 2 + 12) / 6);
1242
+ const jy = torsoBottomY + 18 + (j % 2 === 0 ? 6 : -4);
1243
+ ctx.lineTo(jx, jy);
1244
+ }
1245
+ ctx.quadraticCurveTo(-shoulderW - 6, torsoTopY + torsoHeight * 0.5, -shoulderW + 2, torsoTopY + 8);
1246
+ ctx.closePath();
1247
+ ctx.fill();
1248
+
1249
+ for (let st = 0; st < 5; st++) {
1250
+ const sy = torsoTopY + 22 + st * 14;
1251
+ sLine(-10, sy, 10, sy - 4, seed + 10 + st, '#78350f', 2.5);
1252
+ }
1253
+
1254
+ ctx.fillStyle = '#78350f';
1255
+ ctx.fillRect(-shoulderW - 2, torsoBottomY - 6, (shoulderW + 2) * 2, 8);
1256
+ ctx.fillStyle = '#fef08a';
1257
+ ctx.beginPath(); ctx.roundRect(-7, torsoBottomY - 8, 14, 12, 3); ctx.fill();
1258
+ sLine(-6, torsoBottomY - 2, 6, torsoBottomY - 2, seed + 18, '#78350f', 2);
1259
+
917
1260
  } else if (clothing === 'cloak') {
918
1261
  ctx.fillStyle = '#0f766e';
919
1262
  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);
1263
+ ctx.moveTo(-shoulderW - 6, torsoTopY + 4);
1264
+ ctx.lineTo(shoulderW + 6, torsoTopY + 4);
1265
+ ctx.quadraticCurveTo(shoulderW + 16, torsoTopY + 50, shoulderW + 20, torsoBottomY + 26);
1266
+ ctx.lineTo(-shoulderW - 20, torsoBottomY + 26);
1267
+ ctx.quadraticCurveTo(-shoulderW - 16, torsoTopY + 50, -shoulderW - 6, torsoTopY + 4);
924
1268
  ctx.closePath();
925
1269
  ctx.fill();
1270
+ sLine(-shoulderW - 6, torsoTopY + 4, -shoulderW - 20, torsoBottomY + 26, seed + 7, '#134e4a', 2.5);
1271
+ sLine(shoulderW + 6, torsoTopY + 4, shoulderW + 20, torsoBottomY + 26, seed + 8, '#134e4a', 2.5);
1272
+
926
1273
  } 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);
930
- }
931
- sLine(0, torsoTopY, 0, torsoBottomY, seed + 10, '#f8fafc', 4);
932
-
933
- // Head
934
- const headCenterY = torsoTopY - headRadius - 6 + wb(seed + 11, 1.5);
935
- sCircle(0, headCenterY, headRadius, seed + 12, '#f8fafc', 3.5, '#fef3c7');
936
-
937
- // Hair
938
- if (hairStyle === 'wild') {
939
- for (let h = 0; h < 6; h++) {
940
- const hairAng = -Math.PI * 0.85 + (h / 5) * Math.PI * 0.7;
941
- const hx1 = Math.cos(hairAng) * headRadius;
942
- 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);
946
- }
947
- } else if (hairStyle === 'modern') {
1274
+ ctx.fillStyle = skinBase;
1275
+ ctx.beginPath();
1276
+ ctx.moveTo(-shoulderW, torsoTopY + 8);
1277
+ ctx.lineTo(shoulderW, torsoTopY + 8);
1278
+ ctx.quadraticCurveTo(shoulderW - 4, torsoTopY + torsoHeight * 0.45, shoulderW - 6, torsoBottomY);
1279
+ ctx.lineTo(-shoulderW + 6, torsoBottomY);
1280
+ ctx.quadraticCurveTo(-shoulderW + 4, torsoTopY + torsoHeight * 0.45, -shoulderW, torsoTopY + 8);
1281
+ ctx.closePath();
1282
+ ctx.fill();
1283
+
1284
+ ctx.strokeStyle = skinShadow;
1285
+ ctx.lineWidth = 2;
1286
+ ctx.beginPath();
1287
+ ctx.arc(-14, torsoTopY + 36, 12, 0.1 * Math.PI, 0.9 * Math.PI);
1288
+ ctx.stroke();
1289
+ ctx.beginPath();
1290
+ ctx.arc(14, torsoTopY + 36, 12, 0.1 * Math.PI, 0.9 * Math.PI);
1291
+ ctx.stroke();
1292
+
1293
+ sLine(-22, torsoTopY + 16, -4, torsoTopY + 20, seed + 10, skinShadow, 2);
1294
+ sLine(22, torsoTopY + 16, 4, torsoTopY + 20, seed + 11, skinShadow, 2);
1295
+
1296
+ sLine(0, torsoTopY + 48, 0, torsoBottomY - 10, seed + 12, skinShadow, 1.8);
1297
+ sCircle(0, torsoBottomY - 14, 2, seed + 13, skinShadow, 1, skinShadow);
1298
+ }
1299
+
1300
+ // ── 3. NECK & HEAD ──
1301
+ const headCenterY = torsoTopY - headRadius - 14;
1302
+
1303
+ ctx.fillStyle = skinBase;
1304
+ ctx.fillRect(-11, torsoTopY - 20, 22, 28);
1305
+ ctx.fillStyle = skinShadow;
1306
+ ctx.beginPath();
1307
+ ctx.ellipse(0, torsoTopY - 10, 11, 4, 0, 0, Math.PI * 2);
1308
+ ctx.fill();
1309
+ sLine(-8, torsoTopY - 18, -3, torsoTopY + 6, seed + 14, skinShadow, 1.5);
1310
+ sLine(8, torsoTopY - 18, 3, torsoTopY + 6, seed + 15, skinShadow, 1.5);
1311
+
1312
+ ctx.fillStyle = skinBase;
1313
+ ctx.beginPath();
1314
+ ctx.moveTo(-24, headCenterY - 10);
1315
+ ctx.quadraticCurveTo(-26, headCenterY + 12, -20, headCenterY + 32);
1316
+ ctx.quadraticCurveTo(-10, headCenterY + 44, 0, headCenterY + 44);
1317
+ ctx.quadraticCurveTo(10, headCenterY + 44, 20, headCenterY + 32);
1318
+ ctx.quadraticCurveTo(26, headCenterY + 12, 24, headCenterY - 10);
1319
+ ctx.arc(0, headCenterY - 10, 24, 0, Math.PI, true);
1320
+ ctx.closePath();
1321
+ ctx.fill();
1322
+ ctx.strokeStyle = charStroke;
1323
+ ctx.lineWidth = 2;
1324
+ ctx.stroke();
1325
+
1326
+ ctx.fillStyle = skinBase;
1327
+ ctx.beginPath(); ctx.ellipse(-26, headCenterY + 6, 5, 9, -0.1, 0, Math.PI * 2); ctx.fill();
1328
+ ctx.strokeStyle = charStroke; ctx.lineWidth = 1.8; ctx.stroke();
1329
+ sLine(-26, headCenterY + 3, -25, headCenterY + 9, seed + 16, skinShadow, 1.5);
1330
+
1331
+ ctx.beginPath(); ctx.ellipse(26, headCenterY + 6, 5, 9, 0.1, 0, Math.PI * 2); ctx.fill();
1332
+ ctx.stroke();
1333
+ sLine(26, headCenterY + 3, 25, headCenterY + 9, seed + 17, skinShadow, 1.5);
1334
+
1335
+ // Volumetric 2D Illustrated Hair with organic locks
1336
+ if (hairStyle === 'modern') {
948
1337
  ctx.fillStyle = '#1e293b';
949
1338
  ctx.beginPath();
950
- ctx.arc(0, headCenterY - 12, headRadius * 0.96, Math.PI, Math.PI * 2);
1339
+ ctx.moveTo(-26, headCenterY - 8);
1340
+ ctx.quadraticCurveTo(-28, headCenterY - 32, -14, headCenterY - 42);
1341
+ ctx.quadraticCurveTo(0, headCenterY - 46, 16, headCenterY - 40);
1342
+ ctx.quadraticCurveTo(28, headCenterY - 30, 26, headCenterY - 8);
1343
+ ctx.lineTo(24, headCenterY - 16);
1344
+ ctx.quadraticCurveTo(0, headCenterY - 24, -22, headCenterY - 14);
951
1345
  ctx.closePath();
952
1346
  ctx.fill();
953
- sLine(-headRadius * 0.9, headCenterY - 12, headRadius * 0.9, headCenterY - 12, seed + 20, '#1e293b', 4);
1347
+ sLine(-14, headCenterY - 40, -4, headCenterY - 24, seed + 20, '#334155', 2);
1348
+ sLine(0, headCenterY - 44, 10, headCenterY - 26, seed + 21, '#334155', 2);
1349
+ ctx.fillRect(-26, headCenterY - 8, 3, 12);
1350
+ ctx.fillRect(23, headCenterY - 8, 3, 12);
1351
+
954
1352
  } else if (hairStyle === 'feather') {
955
- sLine(8, headCenterY - headRadius, 28, headCenterY - headRadius - 35, seed + 21, '#38bdf8', 4);
956
- sLine(28, headCenterY - headRadius - 35, 18, headCenterY - headRadius - 20, seed + 22, '#f43f5e', 3);
1353
+ ctx.fillStyle = '#291509';
1354
+ ctx.beginPath();
1355
+ ctx.arc(0, headCenterY - 14, 25, Math.PI * 0.9, Math.PI * 2.1);
1356
+ ctx.fill();
1357
+ sLine(12, headCenterY - 32, 34, headCenterY - 68, seed + 22, '#f8fafc', 3);
1358
+ ctx.fillStyle = '#38bdf8';
1359
+ ctx.beginPath();
1360
+ ctx.moveTo(18, headCenterY - 42);
1361
+ ctx.lineTo(34, headCenterY - 68);
1362
+ ctx.lineTo(24, headCenterY - 56);
1363
+ ctx.closePath();
1364
+ ctx.fill();
1365
+
1366
+ } else {
1367
+ // 'wild': Lush voluminous prehistoric hair mass with textured wavy fringe locks
1368
+ ctx.fillStyle = '#291509';
1369
+ ctx.beginPath();
1370
+ ctx.moveTo(-28, headCenterY + 16);
1371
+ ctx.quadraticCurveTo(-36, headCenterY - 20, -22, headCenterY - 44);
1372
+ ctx.quadraticCurveTo(0, headCenterY - 50, 22, headCenterY - 44);
1373
+ ctx.quadraticCurveTo(36, headCenterY - 20, 28, headCenterY + 16);
1374
+ ctx.lineTo(23, headCenterY - 8);
1375
+ ctx.quadraticCurveTo(0, headCenterY - 18, -23, headCenterY - 8);
1376
+ ctx.closePath();
1377
+ ctx.fill();
1378
+
1379
+ // Natural tapered fringe locks
1380
+ for (let f = 0; f < 5; f++) {
1381
+ const fx = -16 + f * 8;
1382
+ const fy = headCenterY - 8 + (f % 2) * 5;
1383
+ ctx.beginPath();
1384
+ ctx.moveTo(fx - 4, headCenterY - 14);
1385
+ ctx.lineTo(fx + 4, headCenterY - 14);
1386
+ ctx.lineTo(fx, fy);
1387
+ ctx.closePath();
1388
+ ctx.fill();
1389
+ }
1390
+
1391
+ for (let hs = 0; hs < 6; hs++) {
1392
+ const hx = -20 + hs * 8;
1393
+ const hy1 = headCenterY - 38 + (hs % 2) * 6;
1394
+ const hy2 = headCenterY - 12 + (hs % 3) * 4;
1395
+ sLine(hx, hy1, hx + wb(seed + hs, 2), hy2, seed + 24 + hs, '#451a03', 2.5);
1396
+ }
957
1397
  }
958
1398
 
959
- // Eyes
1399
+ // ── 4. EYES & BROWS ──
960
1400
  const eyeY = headCenterY + 4;
961
- const eyeR = 4 * blink;
962
- sCircle(-12, eyeY, Math.max(1, eyeR), seed + 23, '#0f172a', 2, '#0f172a');
963
- sCircle(12, eyeY, Math.max(1, eyeR), seed + 24, '#0f172a', 2, '#0f172a');
1401
+ const eyeSpacing = 13;
964
1402
 
965
- // Specular eye highlights
966
- if (blink > 0.4) {
1403
+ for (let side of [-1, 1]) {
1404
+ const ex = side * eyeSpacing;
967
1405
  ctx.fillStyle = '#ffffff';
968
1406
  ctx.beginPath();
969
- ctx.arc(-14, eyeY - 1.5, 1.2, 0, Math.PI * 2);
970
- ctx.arc(10, eyeY - 1.5, 1.2, 0, Math.PI * 2);
1407
+ ctx.moveTo(ex - 7, eyeY);
1408
+ ctx.quadraticCurveTo(ex, eyeY - 5 * blink, ex + 7, eyeY);
1409
+ ctx.quadraticCurveTo(ex, eyeY + 5 * blink, ex - 7, eyeY);
1410
+ ctx.closePath();
971
1411
  ctx.fill();
1412
+ ctx.strokeStyle = '#0f172a';
1413
+ ctx.lineWidth = 1.5;
1414
+ ctx.stroke();
1415
+
1416
+ if (blink > 0.3) {
1417
+ ctx.fillStyle = isModern ? '#334155' : '#78350f';
1418
+ ctx.beginPath();
1419
+ ctx.arc(ex, eyeY, 3.4 * blink, 0, Math.PI * 2);
1420
+ ctx.fill();
1421
+
1422
+ ctx.fillStyle = '#0f172a';
1423
+ ctx.beginPath();
1424
+ ctx.arc(ex, eyeY, 1.8 * blink, 0, Math.PI * 2);
1425
+ ctx.fill();
1426
+
1427
+ ctx.fillStyle = '#ffffff';
1428
+ ctx.beginPath();
1429
+ ctx.arc(ex - 1.2, eyeY - 1.2, 1.1, 0, Math.PI * 2);
1430
+ ctx.fill();
1431
+ }
1432
+
1433
+ const browLift = (expression === 'surprised' || expression === 'curious') ? -3 : 0;
1434
+ const browFurrow = (expression === 'shivering') ? 2 : 0;
1435
+ sLine(ex - 7 * side, eyeY - 8 + browLift, ex + 7 * side, eyeY - 10 + browLift + browFurrow, seed + 30 + (side > 0 ? 1 : 0), '#0f172a', 2.5);
972
1436
  }
973
1437
 
974
- // Warm cheek blush
975
- ctx.fillStyle = 'rgba(251, 113, 133, 0.28)';
1438
+ // ── 5. NOSE ──
1439
+ const noseY = headCenterY + 16;
1440
+ sLine(-1.5, eyeY - 2, -2.5, noseY, seed + 32, skinShadow, 2);
1441
+ ctx.strokeStyle = skinShadow;
1442
+ ctx.lineWidth = 2;
976
1443
  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();
1444
+ ctx.arc(0, noseY, 3.5, 0.1 * Math.PI, 0.9 * Math.PI);
1445
+ ctx.stroke();
980
1446
 
981
- // Eyebrows
982
- if (expression === 'shivering') {
983
- sLine(-18, eyeY - 12 + wb(seed, 2), -6, eyeY - 7 + wb(seed, 2), seed + 25, '#0f172a', 2.5);
984
- sLine(6, eyeY - 7 + wb(seed, 2), 18, eyeY - 12 + wb(seed, 2), seed + 26, '#0f172a', 2.5);
985
- } else if (expression === 'surprised' || expression === 'curious') {
986
- sLine(-18, eyeY - 15, -6, eyeY - 13, seed + 27, '#0f172a', 2.5);
987
- sLine(6, eyeY - 13, 18, eyeY - 15, seed + 28, '#0f172a', 2.5);
988
- } else {
989
- sLine(-17, eyeY - 9, -7, eyeY - 10, seed + 29, '#0f172a', 2.5);
990
- sLine(7, eyeY - 10, 17, eyeY - 9, seed + 30, '#0f172a', 2.5);
991
- }
992
-
993
- // Mouth
994
- const mouthY = headCenterY + 22;
995
- if (expression === 'shivering') {
996
- const mChatter = Math.sin(t * 32) * 2.5;
997
- sLine(-10, mouthY + mChatter, -4, mouthY - 3 + mChatter, seed + 31, '#0f172a', 2.5);
998
- sLine(-4, mouthY - 3 + mChatter, 3, mouthY + 3 + mChatter, seed + 32, '#0f172a', 2.5);
999
- sLine(3, mouthY + 3 + mChatter, 10, mouthY + mChatter, seed + 33, '#0f172a', 2.5);
1000
- } else if (expression === 'talking') {
1001
- ctx.fillStyle = '#0f172a';
1447
+ // ── 6. MOUTH & AUDIO-DRIVEN LIP SYNC ──
1448
+ const mouthY = headCenterY + 28;
1449
+ const mouthTalk = Math.abs(Math.sin(t * 14)) * 4.5 + Math.abs(Math.sin(t * 22)) * 1.5;
1450
+
1451
+ if (expression === 'talking') {
1452
+ ctx.fillStyle = '#450a0a';
1002
1453
  ctx.beginPath();
1003
- ctx.ellipse(0, mouthY, 7, Math.max(2, mouthTalk), 0, 0, Math.PI * 2);
1454
+ ctx.ellipse(0, mouthY, 7.5, Math.max(2.5, mouthTalk), 0, 0, Math.PI * 2);
1004
1455
  ctx.fill();
1456
+ ctx.strokeStyle = '#0f172a';
1457
+ ctx.lineWidth = 1.8;
1458
+ ctx.stroke();
1459
+
1460
+ if (mouthTalk > 2.5) {
1461
+ ctx.fillStyle = '#ffffff';
1462
+ ctx.fillRect(-5, mouthY - mouthTalk * 0.8, 10, 2.5);
1463
+ }
1464
+ sLine(-6, mouthY + mouthTalk + 1, 6, mouthY + mouthTalk + 1, seed + 33, skinShadow, 1.5);
1465
+
1005
1466
  } else if (expression === 'smiling') {
1467
+ ctx.fillStyle = '#ffffff';
1006
1468
  ctx.beginPath();
1007
- ctx.arc(0, mouthY - 4, 10, 0.2 * Math.PI, 0.8 * Math.PI);
1469
+ ctx.arc(0, mouthY - 3, 9, 0.15 * Math.PI, 0.85 * Math.PI);
1470
+ ctx.closePath();
1471
+ ctx.fill();
1008
1472
  ctx.strokeStyle = '#0f172a';
1009
- ctx.lineWidth = 2.5;
1473
+ ctx.lineWidth = 2;
1010
1474
  ctx.stroke();
1475
+
1476
+ } else if (expression === 'shivering') {
1477
+ const chatter = Math.sin(t * 36) * 2;
1478
+ sLine(-8, mouthY + chatter, 8, mouthY - chatter, seed + 33, '#0f172a', 2.5);
1479
+
1011
1480
  } else {
1012
- sLine(-8, mouthY, 8, mouthY, seed + 34, '#0f172a', 2.5);
1481
+ sLine(-6, mouthY, 6, mouthY, seed + 33, '#0f172a', 2.2);
1482
+ sLine(-3, mouthY + 3, 3, mouthY + 3, seed + 34, skinShadow, 1.5);
1013
1483
  }
1014
1484
 
1015
- // Legs
1016
- const hipY = torsoBottomY;
1017
- 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);
1022
- } else if (armPose === 'hold_spear') {
1023
- const legSwing = Math.sin(t * 5.5);
1024
- const l1KneeX = -15 + legSwing * 22;
1025
- const l1KneeY = hipY + legLength * 0.55;
1026
- const l1FootX = -15 + legSwing * 38;
1027
- const l1FootY = hipY + legLength;
1028
- sLine(-10, hipY, l1KneeX, l1KneeY, seed + 39, '#f8fafc', 4);
1029
- sLine(l1KneeX, l1KneeY, l1FootX, l1FootY, seed + 40, '#f8fafc', 4);
1485
+ if (showBlush || expression === 'blushing' || expression === 'shame') {
1486
+ ctx.fillStyle = 'rgba(251, 113, 133, 0.35)';
1487
+ ctx.beginPath();
1488
+ ctx.ellipse(-14, headCenterY + 14, 7, 4, 0, 0, Math.PI * 2);
1489
+ ctx.ellipse(14, headCenterY + 14, 7, 4, 0, 0, Math.PI * 2);
1490
+ ctx.fill();
1491
+ }
1030
1492
 
1031
- const l2KneeX = 15 - legSwing * 22;
1032
- const l2KneeY = hipY + legLength * 0.55;
1033
- const l2FootX = 15 - legSwing * 38;
1034
- const l2FootY = hipY + legLength;
1035
- sLine(10, hipY, l2KneeX, l2KneeY, seed + 41, '#f8fafc', 4);
1036
- sLine(l2KneeX, l2KneeY, l2FootX, l2FootY, seed + 42, '#f8fafc', 4);
1037
- } 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);
1493
+ // ── 7. ARMS & HANDS WITH ARTICULATED FINGERS ──
1494
+ const shoulderY = torsoTopY + 14;
1495
+
1496
+ function drawArticulatedHand(hx, hy, angle = 0, pose = 'open', handSeed = 200) {
1497
+ ctx.save();
1498
+ ctx.translate(hx, hy);
1499
+ ctx.rotate(angle);
1500
+ ctx.fillStyle = skinBase;
1501
+ ctx.strokeStyle = charStroke;
1502
+ ctx.lineWidth = 1.6;
1503
+
1504
+ ctx.beginPath();
1505
+ ctx.roundRect(-6, -6, 12, 12, 3);
1506
+ ctx.fill();
1507
+ ctx.stroke();
1508
+
1509
+ if (pose === 'fist') {
1510
+ sLine(-5, -3, 5, -3, handSeed, charStroke, 2);
1511
+ ctx.beginPath(); ctx.ellipse(3, 2, 4, 3, 0.4, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
1512
+ } else if (pose === 'pinch') {
1513
+ sLine(-3, -5, 2, -10, handSeed, charStroke, 2);
1514
+ sLine(3, -3, 2, -10, handSeed + 1, charStroke, 2);
1515
+ sLine(-4, -2, -7, -5, handSeed + 2, charStroke, 1.8);
1516
+ } else {
1517
+ sLine(5, 2, 10, -2, handSeed, charStroke, 2);
1518
+ for (let f = 0; f < 4; f++) {
1519
+ const fx = -4 + f * 3;
1520
+ const fLen = 7 + (f === 1 || f === 2 ? 3 : 0);
1521
+ sLine(fx, -5, fx, -5 - fLen, handSeed + 1 + f, charStroke, 1.8);
1522
+ ctx.fillStyle = skinHighlight;
1523
+ ctx.fillRect(fx - 1, -5 - fLen, 2, 2);
1524
+ }
1525
+ }
1526
+ ctx.restore();
1527
+ }
1528
+
1529
+ function drawVolumetricArm(x1, y1, x2, y2, x3, y3, armWidth = 14, color = armFill) {
1530
+ ctx.fillStyle = color;
1531
+ ctx.beginPath(); ctx.arc(x1, y1, armWidth * 0.48, 0, Math.PI * 2); ctx.fill();
1532
+
1533
+ ctx.beginPath();
1534
+ ctx.moveTo(x1 - armWidth * 0.45, y1);
1535
+ ctx.lineTo(x1 + armWidth * 0.45, y1);
1536
+ ctx.lineTo(x2 + armWidth * 0.4, y2);
1537
+ ctx.lineTo(x2 - armWidth * 0.4, y2);
1538
+ ctx.closePath();
1539
+ ctx.fill();
1540
+ sLine(x1 - armWidth * 0.45, y1, x2 - armWidth * 0.4, y2, seed + 50, armStroke, 2);
1541
+ sLine(x1 + armWidth * 0.45, y1, x2 + armWidth * 0.4, y2, seed + 51, armStroke, 2);
1542
+
1543
+ ctx.beginPath(); ctx.arc(x2, y2, armWidth * 0.38, 0, Math.PI * 2); ctx.fill();
1544
+
1545
+ ctx.beginPath();
1546
+ ctx.moveTo(x2 - armWidth * 0.4, y2);
1547
+ ctx.lineTo(x2 + armWidth * 0.4, y2);
1548
+ ctx.lineTo(x3 + armWidth * 0.28, y3);
1549
+ ctx.lineTo(x3 - armWidth * 0.28, y3);
1550
+ ctx.closePath();
1551
+ ctx.fill();
1552
+ sLine(x2 - armWidth * 0.4, y2, x3 - armWidth * 0.28, y3, seed + 52, armStroke, 2);
1553
+ sLine(x2 + armWidth * 0.4, y2, x3 + armWidth * 0.28, y3, seed + 53, armStroke, 2);
1042
1554
  }
1043
1555
 
1044
- // Arms & Hands
1045
- const shoulderY = torsoTopY + 12;
1046
1556
  if (armPose === 'shiver') {
1047
1557
  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);
1558
+ drawVolumetricArm(-shoulderW, shoulderY, -12, shoulderY + 28, 16 + shivA, shoulderY + 26, 13, armFill);
1559
+ drawArticulatedHand(18 + shivA, shoulderY + 26, 0.4, 'open', 310);
1560
+ drawVolumetricArm(shoulderW, shoulderY, 12, shoulderY + 28, -16 + shivA, shoulderY + 30, 13, armFill);
1561
+ drawArticulatedHand(-18 + shivA, shoulderY + 30, -0.4, 'open', 320);
1562
+
1050
1563
  } 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';
1564
+ const spearSway = Math.sin(t * 5.5) * 6;
1565
+ const spearX = 18 + spearSway;
1566
+
1567
+ drawVolumetricArm(-shoulderW, shoulderY, -10, shoulderY + 24, spearX - 4, shoulderY + 12, 14, armFill);
1568
+ drawArticulatedHand(spearX - 4, shoulderY + 12, 0.2, 'fist', 330);
1569
+
1570
+ drawVolumetricArm(shoulderW, shoulderY, 16, shoulderY + 32, spearX + 8, shoulderY + 36, 14, armFill);
1571
+ drawArticulatedHand(spearX + 8, shoulderY + 36, 0.5, 'fist', 340);
1572
+
1573
+ sLine(spearX - 25, cy - 160, spearX + 35, cy + 190, seed + 55, '#78350f', 6);
1574
+ sLine(spearX - 24, cy - 160, spearX + 36, cy + 190, seed + 56, '#92400e', 2.5);
1575
+
1576
+ for (let ln = 0; ln < 4; ln++) {
1577
+ sLine(spearX - 26 + ln * 2, cy - 142 + ln * 4, spearX - 20 + ln * 2, cy - 138 + ln * 4, seed + 57 + ln, '#f59e0b', 2.5);
1578
+ }
1579
+
1580
+ ctx.fillStyle = '#334155';
1056
1581
  ctx.beginPath();
1057
- ctx.moveTo(spearX, -torsoHeight - 165);
1058
- ctx.lineTo(spearX - 10, -torsoHeight - 128);
1059
- ctx.lineTo(spearX + 10, -torsoHeight - 128);
1582
+ ctx.moveTo(spearX - 25, cy - 145);
1583
+ ctx.lineTo(spearX - 35, cy - 195);
1584
+ ctx.lineTo(spearX - 18, cy - 180);
1060
1585
  ctx.closePath();
1061
1586
  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);
1587
+ sLine(spearX - 25, cy - 145, spearX - 35, cy - 195, seed + 60, '#94a3b8', 2);
1588
+ sLine(spearX - 35, cy - 195, spearX - 18, cy - 180, seed + 61, '#64748b', 2);
1589
+
1064
1590
  } 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();
1079
- } 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);
1591
+ const sewPull = Math.sin(t * 4) * 14;
1592
+ drawVolumetricArm(-shoulderW, shoulderY, -36, shoulderY + 28, -16, shoulderY + 36, 13, armFill);
1593
+ drawArticulatedHand(-16, shoulderY + 36, -0.6, 'pinch', 350);
1594
+
1595
+ drawVolumetricArm(shoulderW, shoulderY, 32 + sewPull * 0.4, shoulderY + 22, 10 + sewPull, shoulderY + 32, 13, armFill);
1596
+ drawArticulatedHand(10 + sewPull, shoulderY + 32, 0.4, 'pinch', 360);
1597
+
1598
+ sLine(10 + sewPull, shoulderY + 32, -4, shoulderY + 38, seed + 62, '#fef08a', 3);
1599
+ sLine(10 + sewPull, shoulderY + 32, 28 + sewPull, shoulderY + 44, seed + 63, '#ffffff', 1.5);
1600
+
1082
1601
  } 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);
1088
- } 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);
1602
+ drawVolumetricArm(-shoulderW, shoulderY, -26, shoulderY + 22, -10, shoulderY + 8, 13, armFill);
1603
+ drawArticulatedHand(-10, shoulderY + 8, -0.7, 'pinch', 370);
1604
+
1605
+ drawVolumetricArm(shoulderW, shoulderY, 26, shoulderY + 22, 10, shoulderY + 8, 13, armFill);
1606
+ drawArticulatedHand(10, shoulderY + 8, 0.7, 'pinch', 380);
1607
+
1092
1608
  } 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);
1609
+ drawVolumetricArm(shoulderW, shoulderY, 22, shoulderY + 36, 6, headCenterY + 34, 13, armFill);
1610
+ drawArticulatedHand(6, headCenterY + 34, -0.8, 'pinch', 390);
1611
+
1612
+ drawVolumetricArm(-shoulderW, shoulderY, -28, shoulderY + 36, -14, torsoBottomY + 12, 13, armFill);
1613
+ drawArticulatedHand(-14, torsoBottomY + 12, 0.2, 'open', 400);
1614
+
1615
+ } else if (armPose === 'peace_gesture') {
1616
+ drawVolumetricArm(-shoulderW, shoulderY, -34, shoulderY + 28, -26, shoulderY + 52, 13, armFill);
1617
+ drawArticulatedHand(-26, shoulderY + 52, 0.3, 'open', 410);
1618
+
1619
+ drawVolumetricArm(shoulderW, shoulderY, 36, shoulderY + 10, 48, shoulderY - 26, 13, armFill);
1620
+ drawArticulatedHand(48, shoulderY - 26, -0.2, 'open', 420);
1621
+
1622
+ } else if (armPose === 'cover_body') {
1623
+ drawVolumetricArm(-shoulderW, shoulderY, -14, shoulderY + 22, 8, shoulderY + 26, 13, armFill);
1624
+ drawArticulatedHand(8, shoulderY + 26, 0.5, 'open', 430);
1625
+
1626
+ drawVolumetricArm(shoulderW, shoulderY, 14, shoulderY + 34, -8, shoulderY + 44, 13, armFill);
1627
+ drawArticulatedHand(-8, shoulderY + 44, -0.5, 'open', 440);
1628
+
1096
1629
  } 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);
1630
+ // Natural expressive arms with subtle breathing motion
1631
+ const gArm = Math.sin(t * 3.0) * 4;
1632
+ drawVolumetricArm(-shoulderW, shoulderY, -shoulderW - 6, shoulderY + 34, -shoulderW - 10, shoulderY + 68 + gArm, 13, armFill);
1633
+ drawArticulatedHand(-shoulderW - 10, shoulderY + 68 + gArm, 0.1, 'open', 450);
1634
+
1635
+ drawVolumetricArm(shoulderW, shoulderY, shoulderW + 6, shoulderY + 34, shoulderW + 10, shoulderY + 68 - gArm, 13, armFill);
1636
+ drawArticulatedHand(shoulderW + 10, shoulderY + 68 - gArm, -0.1, 'open', 460);
1102
1637
  }
1103
1638
 
1104
1639
  ctx.restore();
1105
1640
  }
1106
1641
 
1107
- // ── Doodled Character Narrative Themes (3-Beat Progressive Choreography) ─
1642
+ // ── Production-Grade Illustrated Environments & Narrative Themes ──
1108
1643
  if (COMP_THEME === 'doodle_dressing') {
1109
- // Beat 1: Browsing wardrobe -> Beat 2: Trying shirt -> Beat 3: Looking in mirror reflection
1644
+ // Scene 1: Modern Luxury Dressing Room & Walk-in Wardrobe
1110
1645
  const beat2 = spring(t, window.DURATION * 0.35, 180, 16);
1111
1646
  const beat3 = spring(t, window.DURATION * 0.70, 190, 16);
1112
1647
 
1113
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.25)', 2.5);
1114
-
1115
- // Clothes rack on left (sways in beat 1)
1116
- sLine(cx - 360, cy + 220, cx - 360, cy - 140, 11, '#94a3b8', 5);
1117
- sLine(cx - 410, cy + 220, cx - 310, cy + 220, 12, '#94a3b8', 5);
1118
- sLine(cx - 420, cy - 140, cx - 180, cy - 140, 13, '#cbd5e1', 5);
1119
-
1120
- // 3 shirts on hangers
1121
- const shirtColors = ['#38bdf8', '#f43f5e', '#fbbf24'];
1122
- for (let c = 0; c < 3; c++) {
1123
- const hx = cx - 380 + c * 80;
1124
- const sway = Math.sin(t * 2.5 + c * 1.5) * 5;
1125
- sLine(hx, cy - 140, hx + sway, cy - 110, 20 + c, '#e2e8f0', 2.5);
1126
- ctx.fillStyle = shirtColors[c];
1648
+ // 1. Hardwood Herringbone Parquet Floor
1649
+ const floorY = cy + 140;
1650
+ ctx.fillStyle = '#78350f';
1651
+ ctx.fillRect(0, floorY, W, H - floorY);
1652
+ for (let r = 0; r < 6; r++) {
1653
+ const rowY = floorY + r * 38;
1654
+ for (let c = 0; c < 28; c++) {
1655
+ const colX = c * 75 - (r % 2) * 35;
1656
+ ctx.fillStyle = (r + c) % 2 === 0 ? '#92400e' : '#b45309';
1657
+ ctx.fillRect(colX, rowY, 72, 35);
1658
+ ctx.strokeStyle = 'rgba(0,0,0,0.18)';
1659
+ ctx.lineWidth = 1.5;
1660
+ ctx.strokeRect(colX, rowY, 72, 35);
1661
+ }
1662
+ }
1663
+ const floorSheen = ctx.createLinearGradient(0, floorY, 0, H);
1664
+ floorSheen.addColorStop(0, 'rgba(255,255,255,0.12)');
1665
+ floorSheen.addColorStop(1, 'rgba(0,0,0,0.3)');
1666
+ ctx.fillStyle = floorSheen;
1667
+ ctx.fillRect(0, floorY, W, H - floorY);
1668
+
1669
+ // Architectural wainscoting wall trim
1670
+ sLine(0, floorY, W, floorY, 10, '#451a03', 4);
1671
+ sLine(0, floorY - 14, W, floorY - 14, 11, '#cbd5e1', 2);
1672
+ for (let wp = 0; wp < 12; wp++) {
1673
+ const wpx = wp * 165 + 40;
1674
+ ctx.strokeStyle = 'rgba(148, 163, 184, 0.2)';
1675
+ ctx.strokeRect(wpx, cy - 180, 130, floorY - cy + 160);
1676
+ }
1677
+
1678
+ // 2. Luxury Dark Walnut Wardrobe
1679
+ const wX = cx - 360;
1680
+ const wY = cy + 140;
1681
+ ctx.fillStyle = '#1e1b18';
1682
+ ctx.fillRect(wX - 180, wY - 380, 320, 380);
1683
+ ctx.strokeStyle = '#451a03';
1684
+ ctx.lineWidth = 6;
1685
+ ctx.strokeRect(wX - 180, wY - 380, 320, 380);
1686
+
1687
+ const ledGlow = ctx.createLinearGradient(wX - 180, wY - 380, wX - 180, wY);
1688
+ ledGlow.addColorStop(0, 'rgba(251, 191, 36, 0.22)');
1689
+ ledGlow.addColorStop(0.6, 'rgba(251, 191, 36, 0.05)');
1690
+ ledGlow.addColorStop(1, 'rgba(0, 0, 0, 0)');
1691
+ ctx.fillStyle = ledGlow;
1692
+ ctx.fillRect(wX - 176, wY - 376, 312, 376);
1693
+
1694
+ ctx.fillStyle = '#33271e';
1695
+ ctx.fillRect(wX - 176, wY - 320, 312, 10);
1696
+ ctx.fillStyle = '#475569';
1697
+ ctx.fillRect(wX - 140, wY - 365, 80, 45);
1698
+ ctx.fillStyle = '#64748b';
1699
+ ctx.fillRect(wX - 30, wY - 360, 90, 40);
1700
+
1701
+ sLine(wX - 170, wY - 290, wX + 130, wY - 290, 20, '#e2e8f0', 4);
1702
+
1703
+ const hangerItems = [
1704
+ { color: '#0284c7', collar: '#38bdf8', w: 46, h: 140 },
1705
+ { color: '#f8fafc', collar: '#e2e8f0', w: 42, h: 130 },
1706
+ { color: '#ea580c', collar: '#f97316', w: 44, h: 135 },
1707
+ { color: '#ca8a04', collar: '#eab308', w: 40, h: 125 }
1708
+ ];
1709
+ for (let i = 0; i < hangerItems.length; i++) {
1710
+ const it = hangerItems[i];
1711
+ const hx = wX - 130 + i * 70;
1712
+ const sway = Math.sin(t * 2.2 + i * 1.4) * 4;
1713
+ sLine(hx, wY - 290, hx + sway, wY - 268, 30 + i, '#d4af37', 2);
1714
+ sLine(hx + sway - 18, wY - 258, hx + sway + 18, wY - 258, 35 + i, '#b45309', 3);
1715
+ ctx.fillStyle = it.color;
1127
1716
  ctx.beginPath();
1128
- ctx.moveTo(hx + sway - 22, cy - 110);
1129
- ctx.lineTo(hx + sway + 22, cy - 110);
1130
- ctx.lineTo(hx + sway + 28, cy - 10);
1131
- ctx.lineTo(hx + sway - 28, cy - 10);
1717
+ ctx.moveTo(hx + sway - 18, wY - 258);
1718
+ ctx.lineTo(hx + sway + 18, wY - 258);
1719
+ ctx.lineTo(hx + sway + it.w * 0.5, wY - 258 + it.h);
1720
+ ctx.lineTo(hx + sway - it.w * 0.5, wY - 258 + it.h);
1132
1721
  ctx.closePath();
1133
1722
  ctx.fill();
1723
+ sLine(hx + sway - 8, wY - 258, hx + sway, wY - 244, 40 + i, it.collar, 2);
1724
+ sLine(hx + sway + 8, wY - 258, hx + sway, wY - 244, 41 + i, it.collar, 2);
1725
+ sLine(hx + sway, wY - 244, hx + sway, wY - 258 + it.h, 42 + i, 'rgba(0,0,0,0.2)', 1.5);
1134
1726
  }
1135
1727
 
1136
- // Mirror on right - emerges and glows in Beat 2 & 3
1137
- const mirrorX = cx + 320;
1138
- const mirrorY = cy + 20;
1139
- ctx.fillStyle = 'rgba(56, 189, 248, ' + (0.05 + beat2 * 0.08) + ')';
1728
+ // 3. Arched Full-Length Brass Mirror
1729
+ const mX = cx + 330;
1730
+ const mY = cy + 20;
1731
+ ctx.fillStyle = '#b45309';
1140
1732
  ctx.beginPath();
1141
- ctx.roundRect(mirrorX - 85, mirrorY - 210, 170, 390, 24);
1733
+ ctx.roundRect(mX - 100, mY - 230, 200, 420, [100, 100, 10, 10]);
1142
1734
  ctx.fill();
1143
- ctx.strokeStyle = beat3 > 0.5 ? '#f59e0b' : '#94a3b8';
1144
- ctx.lineWidth = 4;
1735
+ ctx.strokeStyle = '#d4af37';
1736
+ ctx.lineWidth = 6;
1145
1737
  ctx.stroke();
1146
1738
 
1147
- // Reflected character in mirror (smiling and thumbs up in beat 3)
1148
- if (beat2 > 0.1) {
1739
+ const glassGrad = ctx.createLinearGradient(mX - 90, mY - 220, mX + 90, mY + 180);
1740
+ glassGrad.addColorStop(0, 'rgba(56, 189, 248, 0.12)');
1741
+ glassGrad.addColorStop(0.5, 'rgba(255, 255, 255, 0.25)');
1742
+ glassGrad.addColorStop(1, 'rgba(14, 165, 233, 0.08)');
1743
+ ctx.fillStyle = glassGrad;
1744
+ ctx.beginPath();
1745
+ ctx.roundRect(mX - 90, mY - 220, 180, 400, [90, 90, 6, 6]);
1746
+ ctx.fill();
1747
+
1748
+ sLine(mX - 70, mY + 140, mX + 50, mY - 180, 50, 'rgba(255,255,255,0.25)', 2);
1749
+ sLine(mX - 40, mY + 160, mX + 70, mY - 140, 51, 'rgba(255,255,255,0.18)', 2);
1750
+
1751
+ if (beat2 > 0.05) {
1149
1752
  ctx.save();
1150
- ctx.globalAlpha = Math.min(0.65, beat2 * 0.65);
1753
+ ctx.globalAlpha = Math.min(0.85, beat2 * 0.85);
1151
1754
  drawDoodleCharacter({
1152
- x: mirrorX,
1755
+ x: mX,
1153
1756
  y: cy + 120,
1154
1757
  scale: 1.05,
1155
1758
  hairStyle: 'modern',
1156
- expression: beat3 > 0.5 ? 'smiling' : 'curious',
1759
+ expression: beat3 > 0.5 ? 'smiling' : 'talking',
1157
1760
  clothing: 'shirt',
1158
1761
  armPose: beat3 > 0.5 ? 'peace_gesture' : 'dress_shirt',
1159
1762
  seed: 88,
@@ -1161,8 +1764,8 @@ function drawMainScene(t) {
1161
1764
  ctx.restore();
1162
1765
  }
1163
1766
 
1164
- // Main character moves progressively from rack -> center -> mirror
1165
- const charX = cx - 220 + beat2 * 170 + beat3 * 60;
1767
+ // 4. Main Character
1768
+ const charX = cx - 180 + beat2 * 140 + beat3 * 50;
1166
1769
  drawDoodleCharacter({
1167
1770
  x: charX,
1168
1771
  y: cy + 140,
@@ -1171,96 +1774,275 @@ function drawMainScene(t) {
1171
1774
  expression: beat3 > 0.5 ? 'smiling' : 'talking',
1172
1775
  clothing: 'shirt',
1173
1776
  armPose: beat2 > 0.5 ? 'dress_shirt' : 'natural',
1777
+ showShadow: true,
1174
1778
  seed: 101,
1175
1779
  });
1176
1780
 
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);
1781
+ } else if (COMP_THEME === 'doodle_modern_vs_prehistoric') {
1782
+ // Scene 2: Split-Screen Comparison
1783
+ const dur = window.DURATION || 3;
1784
+ const bridgeSpring = spring(t, dur * 0.35, 170, 15);
1785
+
1786
+ // Modern half (Left 0 to cx)
1787
+ ctx.save();
1788
+ ctx.beginPath();
1789
+ ctx.rect(0, 0, cx, H);
1790
+ ctx.clip();
1791
+
1792
+ const skyM = ctx.createLinearGradient(0, 0, 0, cy + 120);
1793
+ skyM.addColorStop(0, '#020617');
1794
+ skyM.addColorStop(0.6, '#0f172a');
1795
+ skyM.addColorStop(1, '#1e293b');
1796
+ ctx.fillStyle = skyM;
1797
+ ctx.fillRect(0, 0, cx, cy + 120);
1798
+
1799
+ const towers = [
1800
+ { x: 30, w: 90, h: 320, windows: 5 },
1801
+ { x: 135, w: 120, h: 420, windows: 7 },
1802
+ { x: 270, w: 85, h: 360, windows: 4 },
1803
+ { x: 370, w: 140, h: 460, windows: 8 },
1804
+ { x: 530, w: 110, h: 380, windows: 6 },
1805
+ { x: 660, w: 130, h: 480, windows: 8 },
1806
+ { x: 810, w: 120, h: 410, windows: 7 }
1807
+ ];
1808
+ for (let tw of towers) {
1809
+ ctx.fillStyle = '#090d16';
1810
+ ctx.fillRect(tw.x, cy + 120 - tw.h, tw.w, tw.h);
1811
+ sLine(tw.x + tw.w * 0.5, cy + 120 - tw.h, tw.x + tw.w * 0.5, cy + 120 - tw.h - 45, 100 + tw.x, '#64748b', 2);
1812
+ const beaconOn = (Math.sin(t * 4 + tw.x) > 0);
1813
+ if (beaconOn) {
1814
+ ctx.fillStyle = '#ef4444';
1815
+ ctx.beginPath(); ctx.arc(tw.x + tw.w * 0.5, cy + 120 - tw.h - 45, 3, 0, Math.PI * 2); ctx.fill();
1816
+ }
1817
+ for (let wy = 0; wy < tw.windows; wy++) {
1818
+ for (let wx = 0; wx < 3; wx++) {
1819
+ const winLight = (Math.sin(tw.x * 3 + wy * 5 + wx) > -0.2);
1820
+ ctx.fillStyle = winLight ? '#fef08a' : 'rgba(255,255,255,0.08)';
1821
+ ctx.fillRect(tw.x + 12 + wx * 28, cy + 120 - tw.h + 25 + wy * 38, 16, 20);
1822
+ }
1823
+ }
1824
+ }
1825
+
1826
+ ctx.fillStyle = '#0f172a';
1827
+ ctx.fillRect(0, cy + 120, cx, H - (cy + 120));
1828
+ sLine(0, cy + 120, cx, cy + 120, 110, '#38bdf8', 2);
1829
+ ctx.restore();
1830
+
1831
+ // Savannah half (Right cx to W)
1832
+ ctx.save();
1833
+ ctx.beginPath();
1834
+ ctx.rect(cx, 0, W - cx, H);
1835
+ ctx.clip();
1836
+
1837
+ const skyS = ctx.createLinearGradient(cx, 0, cx, cy + 120);
1838
+ skyS.addColorStop(0, '#7f1d1d');
1839
+ skyS.addColorStop(0.4, '#c2410c');
1840
+ skyS.addColorStop(0.75, '#ea580c');
1841
+ skyS.addColorStop(1, '#fde047');
1842
+ ctx.fillStyle = skyS;
1843
+ ctx.fillRect(cx, 0, W - cx, cy + 120);
1844
+
1845
+ ctx.fillStyle = '#fef08a';
1846
+ ctx.beginPath();
1847
+ ctx.arc(cx + 340, cy + 80, 80, 0, Math.PI * 2);
1848
+ ctx.fill();
1849
+
1850
+ ctx.fillStyle = '#4c1d95';
1851
+ ctx.beginPath();
1852
+ ctx.moveTo(cx, cy + 120);
1853
+ ctx.lineTo(cx + 180, cy + 30);
1854
+ ctx.lineTo(cx + 360, cy + 60);
1855
+ ctx.lineTo(cx + 520, cy + 15);
1856
+ ctx.lineTo(W, cy + 80);
1857
+ ctx.lineTo(W, cy + 120);
1858
+ ctx.closePath();
1859
+ ctx.fill();
1860
+
1861
+ function drawSavannahAcacia(ax, ay, sc = 1.0) {
1862
+ ctx.save();
1863
+ ctx.translate(ax, ay);
1864
+ ctx.scale(sc, sc);
1865
+ ctx.fillStyle = '#1c1917';
1866
+ ctx.beginPath();
1867
+ ctx.moveTo(-6, 0);
1868
+ ctx.quadraticCurveTo(-14, -60, -28, -90);
1869
+ ctx.lineTo(-20, -90);
1870
+ ctx.quadraticCurveTo(-8, -60, 4, 0);
1871
+ ctx.closePath();
1872
+ ctx.fill();
1873
+ sLine(-24, -85, 20, -110, 120, '#1c1917', 5);
1874
+ sLine(-24, -85, -60, -105, 121, '#1c1917', 5);
1875
+ ctx.beginPath(); ctx.ellipse(-24, -98, 55, 14, 0, 0, Math.PI * 2); ctx.fill();
1876
+ ctx.beginPath(); ctx.ellipse(20, -114, 45, 12, 0.05, 0, Math.PI * 2); ctx.fill();
1877
+ ctx.beginPath(); ctx.ellipse(-60, -108, 40, 11, -0.05, 0, Math.PI * 2); ctx.fill();
1878
+ ctx.restore();
1879
+ }
1880
+ drawSavannahAcacia(cx + 200, cy + 120, 0.75);
1881
+ drawSavannahAcacia(cx + 580, cy + 120, 1.1);
1882
+
1883
+ ctx.fillStyle = '#78350f';
1884
+ ctx.fillRect(cx, cy + 120, W - cx, H - (cy + 120));
1885
+ sLine(cx, cy + 120, W, cy + 120, 130, '#f59e0b', 3);
1886
+ ctx.restore();
1887
+
1888
+ // Modern Human on Left
1889
+ drawDoodleCharacter({
1890
+ x: cx - 260,
1891
+ y: cy + 140,
1892
+ scale: 1.35,
1893
+ hairStyle: 'modern',
1894
+ expression: 'talking',
1895
+ clothing: 'shirt',
1896
+ armPose: 'dress_shirt',
1897
+ showShadow: true,
1898
+ seed: 120,
1899
+ });
1900
+
1901
+ // Prehistoric Human on Right
1902
+ drawDoodleCharacter({
1903
+ x: cx + 260,
1904
+ y: cy + 140,
1905
+ scale: 1.35,
1906
+ hairStyle: 'wild',
1907
+ expression: 'curious',
1908
+ clothing: 'none',
1909
+ armPose: 'natural',
1910
+ showShadow: true,
1911
+ seed: 121,
1912
+ });
1181
1913
 
1182
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
1914
+ // Central Luminous Timeline Rift & Evolution Badge
1915
+ ctx.save();
1916
+ const riftGlow = ctx.createLinearGradient(cx - 30, 0, cx + 30, 0);
1917
+ riftGlow.addColorStop(0, 'rgba(217, 119, 6, 0)');
1918
+ riftGlow.addColorStop(0.5, 'rgba(251, 191, 36, 0.6)');
1919
+ riftGlow.addColorStop(1, 'rgba(217, 119, 6, 0)');
1920
+ ctx.fillStyle = riftGlow;
1921
+ ctx.fillRect(cx - 30, 0, 60, H);
1922
+ sLine(cx, 0, cx, H, 140, '#fef08a', 2.5);
1923
+
1924
+ for (let rp = 0; rp < 18; rp++) {
1925
+ const rpx = cx + Math.sin(t * 3 + rp * 2) * 22;
1926
+ const rpy = (rp * 55 + t * 90) % H;
1927
+ sCircle(rpx, rpy, 2.5, 150 + rp, '#fef08a', 1, '#ffffff');
1928
+ }
1929
+
1930
+ const badgeY = cy - 200;
1931
+ ctx.fillStyle = 'rgba(15, 23, 42, 0.88)';
1932
+ ctx.beginPath();
1933
+ ctx.roundRect(cx - 160, badgeY - 22, 320, 44, 22);
1934
+ ctx.fill();
1935
+ ctx.strokeStyle = '#d4af37';
1936
+ ctx.lineWidth = 2.5;
1937
+ ctx.stroke();
1183
1938
 
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);
1939
+ ctx.fillStyle = '#fef08a';
1940
+ ctx.font = '700 16px ' + FONT_DISPLAY;
1941
+ ctx.textAlign = 'center';
1942
+ ctx.textBaseline = 'middle';
1943
+ ctx.fillText('300,000 BCE ───▶ PRESENT', cx, badgeY);
1944
+ ctx.restore();
1945
+
1946
+ } else if (COMP_THEME === 'doodle_shiver_cold') {
1947
+ // Scene 3: Ice Age Cold / Glacial Wilderness / Volumetric Sunbeams
1948
+ const dur = window.DURATION || 3;
1949
+ const beat2 = spring(t, dur * 0.45, 160, 15);
1950
+
1951
+ const skyG = ctx.createLinearGradient(0, 0, 0, cy + 140);
1952
+ skyG.addColorStop(0, '#082f49');
1953
+ skyG.addColorStop(0.5, '#0c4a6e');
1954
+ skyG.addColorStop(1, '#0284c7');
1955
+ ctx.fillStyle = skyG;
1956
+ ctx.fillRect(0, 0, W, cy + 140);
1957
+
1958
+ ctx.fillStyle = '#e0f2fe';
1959
+ ctx.beginPath();
1960
+ ctx.moveTo(0, cy + 140);
1961
+ ctx.lineTo(180, cy - 80);
1962
+ ctx.lineTo(340, cy + 20);
1963
+ ctx.lineTo(540, cy - 140);
1964
+ ctx.lineTo(760, cy - 20);
1965
+ ctx.lineTo(980, cy - 180);
1966
+ ctx.lineTo(1240, cy - 40);
1967
+ ctx.lineTo(1520, cy - 150);
1968
+ ctx.lineTo(1760, cy - 10);
1969
+ ctx.lineTo(W, cy - 90);
1970
+ ctx.lineTo(W, cy + 140);
1971
+ ctx.closePath();
1972
+ ctx.fill();
1973
+
1974
+ ctx.fillStyle = '#f0f9ff';
1975
+ ctx.fillRect(0, cy + 140, W, H - (cy + 140));
1976
+ sLine(0, cy + 140, W, cy + 140, 10, '#38bdf8', 3);
1977
+
1978
+ const windAlpha = Math.max(0, 1 - beat2 * 0.85);
1979
+ if (windAlpha > 0.05) {
1980
+ for (let w = 0; w < 16; w++) {
1981
+ const windX = ((t * 750 + w * 180) % (W + 600)) - 300;
1982
+ const windY = cy - 250 + (w * 36) + Math.sin(t * 3.5 + w) * 15;
1983
+ const windColor = 'rgba(224, 242, 254, ' + (0.45 * windAlpha) + ')';
1984
+ sLine(windX, windY, windX + 180, windY + 4, 200 + w, windColor, 2.5);
1985
+ }
1189
1986
  }
1190
1987
 
1191
- // 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;
1988
+ for (let sp = 0; sp < 28; sp++) {
1989
+ const px = ((sp * 75 + t * 450) % W);
1990
+ const py = (sp * 38 + t * 90) % H;
1195
1991
  sCircle(px, py, 2.5, 300 + sp, '#e0f2fe', 1, '#ffffff');
1196
1992
  }
1197
1993
 
1198
- // Shivering breath vapor cloud (calms in Beat 3)
1199
- if (stamp2 < 0.8) {
1994
+ if (beat2 < 0.7) {
1200
1995
  const breathPhase = (t * 2.2) % 1;
1201
1996
  const bCloudX = cx - 50 + breathPhase * 60;
1202
1997
  const bCloudY = cy - 90 - breathPhase * 25;
1203
- ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.5) + ')';
1998
+ ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.45) + ')';
1204
1999
  ctx.beginPath();
1205
2000
  ctx.arc(bCloudX, bCloudY, 15 + breathPhase * 25, 0, Math.PI * 2);
1206
2001
  ctx.fill();
1207
2002
  }
1208
2003
 
1209
- // Main character: shivers in Beat 1 & 2, then realizes / curious in Beat 3
1210
- const isShivering = stamp2 < 0.7;
2004
+ if (beat2 > 0.05) {
2005
+ ctx.save();
2006
+ ctx.globalAlpha = Math.min(0.65, beat2 * 0.65);
2007
+ const sunBeamGrad = ctx.createRadialGradient(cx + 220, cy - 240, 20, cx + 220, cy - 100, 380);
2008
+ sunBeamGrad.addColorStop(0, 'rgba(251, 191, 36, 0.45)');
2009
+ sunBeamGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
2010
+ ctx.fillStyle = sunBeamGrad;
2011
+ ctx.beginPath();
2012
+ ctx.arc(cx + 220, cy - 180, 320, 0, Math.PI * 2);
2013
+ ctx.fill();
2014
+ ctx.restore();
2015
+ }
2016
+
2017
+ const isShivering = beat2 < 0.6;
1211
2018
  const vibX = isShivering ? Math.sin(t * 38) * 4.5 : 0;
1212
2019
  const vibY = isShivering ? Math.cos(t * 32) * 2 : 0;
1213
2020
  drawDoodleCharacter({
1214
- x: cx - 120 + vibX,
2021
+ x: cx - 80 + beat2 * 80 + vibX,
1215
2022
  y: cy + 140 + vibY,
1216
2023
  scale: 1.35,
1217
2024
  hairStyle: 'wild',
1218
2025
  expression: isShivering ? 'shivering' : 'curious',
1219
2026
  clothing: 'none',
1220
2027
  armPose: isShivering ? 'shiver' : 'thinker',
2028
+ showShadow: true,
1221
2029
  seed: 102,
1222
2030
  });
1223
2031
 
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
2032
  } else if (COMP_THEME === 'doodle_hunter_savannah') {
1257
- // Beat 1: Walking with spear -> Beat 2: Swatting mosquitoes -> Beat 3: Pausing alert
2033
+ // Scene 5: African Savannah Plains / Hunter with Carved Spear
1258
2034
  const walkProg = Math.min(1, Math.max(0, t / window.DURATION));
1259
2035
  const swatting = (t % 2.4 > 1.2);
1260
2036
 
1261
- // Blazing midday sun with undulating rays
2037
+ const skyH = ctx.createLinearGradient(0, 0, 0, cy + 140);
2038
+ skyH.addColorStop(0, '#ea580c');
2039
+ skyH.addColorStop(0.5, '#f59e0b');
2040
+ skyH.addColorStop(1, '#fef08a');
2041
+ ctx.fillStyle = skyH;
2042
+ ctx.fillRect(0, 0, W, cy + 140);
2043
+
1262
2044
  const sunR = 75 + Math.sin(t * 2) * 6;
1263
- ctx.fillStyle = '#f59e0b';
2045
+ ctx.fillStyle = '#fef08a';
1264
2046
  ctx.beginPath();
1265
2047
  ctx.arc(cx - 380, cy - 180, sunR, 0, Math.PI * 2);
1266
2048
  ctx.fill();
@@ -1273,23 +2055,35 @@ function drawMainScene(t) {
1273
2055
  sLine(rx1, ry1, rx2, ry2, 450 + r, '#fbbf24', 3);
1274
2056
  }
1275
2057
 
1276
- // Ground and tall swaying savannah grass
1277
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, '#78350f', 3);
1278
- for (let g = 0; g < 32; g++) {
1279
- const gx = cx - 550 + g * 36;
2058
+ ctx.fillStyle = '#78350f';
2059
+ ctx.beginPath();
2060
+ ctx.moveTo(0, cy + 140);
2061
+ ctx.lineTo(240, cy + 40);
2062
+ ctx.lineTo(580, cy + 80);
2063
+ ctx.lineTo(920, cy + 20);
2064
+ ctx.lineTo(1340, cy + 70);
2065
+ ctx.lineTo(W, cy + 30);
2066
+ ctx.lineTo(W, cy + 140);
2067
+ ctx.closePath();
2068
+ ctx.fill();
2069
+
2070
+ ctx.fillStyle = '#451a03';
2071
+ ctx.fillRect(0, cy + 140, W, H - (cy + 140));
2072
+ sLine(0, cy + 140, W, cy + 140, 10, '#d97706', 3.5);
2073
+
2074
+ for (let g = 0; g < 42; g++) {
2075
+ const gx = g * 48;
1280
2076
  const sway = Math.sin(t * 3.2 + g * 0.4) * 16;
1281
- sLine(gx, cy + 220, gx + sway, cy + 140, 500 + g, '#d97706', 2.5);
2077
+ sLine(gx, cy + 140, gx + sway, cy + 50, 500 + g, '#f59e0b', 2.5);
1282
2078
  }
1283
2079
 
1284
- // Buzzing mosquitoes orbiting hunter
1285
- const charX = cx - 180 + walkProg * 240;
2080
+ const charX = cx - 220 + walkProg * 320;
1286
2081
  for (let m = 0; m < 4; m++) {
1287
2082
  const mx = charX + 50 + Math.sin(t * 8 + m * 2) * 65;
1288
2083
  const my = cy - 60 + Math.cos(t * 7 + m * 2) * 45;
1289
2084
  sCircle(mx, my, 2.5, 550 + m, '#0f172a', 1.5, '#ef4444');
1290
2085
  }
1291
2086
 
1292
- // Walking early hunter with spear
1293
2087
  drawDoodleCharacter({
1294
2088
  x: charX,
1295
2089
  y: cy + 140,
@@ -1298,32 +2092,44 @@ function drawMainScene(t) {
1298
2092
  expression: swatting ? 'shivering' : 'talking',
1299
2093
  clothing: 'none',
1300
2094
  armPose: 'hold_spear',
2095
+ showShadow: true,
1301
2096
  seed: 103,
1302
2097
  });
1303
2098
 
1304
2099
  } else if (COMP_THEME === 'doodle_sewing_needle') {
1305
- // Beat 1: Seated by campfire -> Beat 2: Zoom loupe with bone needle -> Beat 3: Timeline badges
2100
+ // Scene 4: Cave Hearth & Eyed Bone Needle Loupe Zoom
1306
2101
  const loupeSpring = spring(t, window.DURATION * 0.32, 180, 15);
1307
2102
  const badgeSpring = spring(t, window.DURATION * 0.68, 200, 15);
1308
2103
 
1309
- // Campfire with animated flames and sparks
2104
+ ctx.fillStyle = '#1c1917';
2105
+ ctx.fillRect(0, 0, W, H);
2106
+
2107
+ ctx.fillStyle = 'rgba(180, 83, 9, 0.22)';
2108
+ ctx.beginPath();
2109
+ ctx.ellipse(cx - 320, cy - 140, 80, 48, -0.15, 0, Math.PI * 2);
2110
+ ctx.fill();
2111
+ ctx.fillRect(cx - 370, cy - 160, 40, 40);
2112
+
1310
2113
  const fireX = cx - 380;
1311
- const fireY = cy + 180;
1312
- for (let f = 0; f < 5; f++) {
1313
- const flH = 50 + Math.sin(t * 12 + f) * 22;
2114
+ const fireY = cy + 170;
2115
+ for (let st = 0; st < 7; st++) {
2116
+ const stX = fireX - 45 + st * 15;
2117
+ sCircle(stX, fireY + 12, 10, 600 + st, '#44403c', 2, '#292524');
2118
+ }
2119
+ for (let f = 0; f < 6; f++) {
2120
+ const flH = 55 + Math.sin(t * 12 + f) * 24;
1314
2121
  ctx.fillStyle = f % 2 === 0 ? '#ef4444' : '#f59e0b';
1315
2122
  ctx.beginPath();
1316
- ctx.moveTo(fireX - 30 + f * 12, fireY);
1317
- ctx.quadraticCurveTo(fireX + (f - 2) * 8, fireY - flH, fireX - 10 + f * 12, fireY);
2123
+ ctx.moveTo(fireX - 35 + f * 12, fireY + 8);
2124
+ ctx.quadraticCurveTo(fireX + (f - 2.5) * 8, fireY - flH, fireX - 12 + f * 12, fireY + 8);
1318
2125
  ctx.fill();
1319
2126
  }
1320
2127
  for (let sp = 0; sp < 8; sp++) {
1321
2128
  const sx = fireX + Math.sin(t * 4 + sp) * 35;
1322
- const sy = fireY - 30 - ((t * 80 + sp * 35) % 140);
1323
- sCircle(sx, sy, 2, 600 + sp, '#fbbf24', 1, '#fbbf24');
2129
+ const sy = fireY - 30 - ((t * 80 + sp * 35) % 150);
2130
+ sCircle(sx, sy, 2.2, 610 + sp, '#fbbf24', 1, '#fbbf24');
1324
2131
  }
1325
2132
 
1326
- // Seated human stitching hide with bone needle
1327
2133
  drawDoodleCharacter({
1328
2134
  x: cx - 120,
1329
2135
  y: cy + 140,
@@ -1332,85 +2138,110 @@ function drawMainScene(t) {
1332
2138
  expression: 'talking',
1333
2139
  clothing: 'pelt',
1334
2140
  armPose: 'sewing',
2141
+ showShadow: true,
1335
2142
  seed: 104,
1336
2143
  });
1337
2144
 
1338
- // Zoomed loupe expands open in Beat 2
1339
2145
  if (loupeSpring > 0.05) {
1340
2146
  ctx.save();
1341
- const lensX = cx + 270;
1342
- const lensY = cy - 20;
2147
+ const lensX = cx + 280;
2148
+ const lensY = cy - 10;
1343
2149
  ctx.translate(lensX, lensY);
1344
2150
  ctx.scale(loupeSpring, loupeSpring);
1345
2151
 
1346
- const lensR = 140;
1347
- ctx.fillStyle = 'rgba(15, 23, 42, 0.92)';
2152
+ const lensR = 150;
2153
+ ctx.fillStyle = '#090d16';
1348
2154
  ctx.beginPath();
1349
2155
  ctx.arc(0, 0, lensR, 0, Math.PI * 2);
1350
2156
  ctx.fill();
2157
+
1351
2158
  ctx.strokeStyle = '#d4af37';
1352
- ctx.lineWidth = 5;
2159
+ ctx.lineWidth = 8;
2160
+ ctx.stroke();
2161
+
2162
+ ctx.fillStyle = '#78350f';
2163
+ ctx.fillRect(-120, -75, 240, 60);
2164
+ ctx.fillStyle = '#92400e';
2165
+ ctx.fillRect(-120, 15, 240, 60);
2166
+
2167
+ for (let h = 0; h < 5; h++) {
2168
+ const hx = -80 + h * 40;
2169
+ sCircle(hx, -15, 5, 630 + h, '#0f172a', 2, '#1c1917');
2170
+ sCircle(hx, 15, 5, 640 + h, '#0f172a', 2, '#1c1917');
2171
+ }
2172
+
2173
+ const nWiggle = Math.sin(t * 4.5) * 16;
2174
+ ctx.fillStyle = '#fef08a';
2175
+ ctx.beginPath();
2176
+ ctx.moveTo(-50 + nWiggle, -70);
2177
+ ctx.lineTo(25 + nWiggle, 65);
2178
+ ctx.lineTo(32 + nWiggle, 60);
2179
+ ctx.lineTo(-44 + nWiggle, -72);
2180
+ ctx.closePath();
2181
+ ctx.fill();
2182
+ ctx.strokeStyle = '#ca8a04';
2183
+ ctx.lineWidth = 1.5;
1353
2184
  ctx.stroke();
1354
2185
 
1355
- // Two thick leather pelt pieces being stitched
1356
- sLine(-85, -20, 85, -20, 650, '#92400e', 8);
1357
- sLine(-85, 20, 85, 20, 651, '#78350f', 8);
2186
+ sCircle(-42 + nWiggle, -62, 4, 650, '#0f172a', 1.5, '#0f172a');
1358
2187
 
1359
- // Drilled bone needle piercing through with animated wiggle
1360
- const nWiggle = Math.sin(t * 4.5) * 18;
1361
- sLine(-45 + nWiggle, -65, 25 + nWiggle, 55, 652, '#fef08a', 6);
1362
- sCircle(-40 + nWiggle, -58, 4, 653, '#0f172a', 1.5, '#0f172a');
2188
+ ctx.strokeStyle = '#ffffff';
2189
+ ctx.lineWidth = 3.5;
2190
+ ctx.beginPath();
2191
+ ctx.moveTo(-42 + nWiggle, -62);
2192
+ ctx.quadraticCurveTo(-20, -30, 0, 0);
2193
+ ctx.stroke();
1363
2194
 
1364
- // Stitches across pelts
1365
- for (let st = 0; st < 6; st++) {
1366
- const stX = -70 + st * 28;
1367
- sLine(stX, -22, stX + 8, 22, 660 + st, '#ffffff', 3.5);
2195
+ for (let st = 0; st < 4; st++) {
2196
+ const stX = -80 + st * 40;
2197
+ sLine(stX, -15, stX + 8, 15, 660 + st, '#ffffff', 4);
1368
2198
  }
1369
2199
  ctx.restore();
1370
2200
  }
1371
2201
 
1372
- // Timeline badge in Beat 3
1373
2202
  if (badgeSpring > 0.05) {
1374
2203
  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);
2204
+ ctx.globalAlpha = Math.min(0.85, badgeSpring * 0.85);
2205
+ sLine(cx + 140, cy + 160, cx + 420, cy + 160, 670, '#d4af37', 2);
1381
2206
  ctx.restore();
1382
2207
  }
1383
2208
 
1384
2209
  } else if (COMP_THEME === 'doodle_shame_reflection') {
1385
- // Beat 1: Looking down -> Beat 2: Water reflection & blushing -> Beat 3: Theory of Mind halo
2210
+ // Scene 9: Pristine Woodland Pool / Water Reflection / Theory of Mind Halo
1386
2211
  const refSpring = spring(t, window.DURATION * 0.30, 180, 15);
1387
2212
  const haloSpring = spring(t, window.DURATION * 0.65, 190, 15);
1388
2213
 
1389
2214
  const poolX = cx;
1390
- const poolY = cy + 180;
1391
- ctx.fillStyle = 'rgba(14, 165, 233, 0.15)';
2215
+ const poolY = cy + 170;
2216
+
2217
+ ctx.fillStyle = 'rgba(14, 165, 233, 0.18)';
1392
2218
  ctx.beginPath();
1393
- ctx.ellipse(poolX, poolY, 260, 60, 0, 0, Math.PI * 2);
2219
+ ctx.ellipse(poolX, poolY, 280, 68, 0, 0, Math.PI * 2);
1394
2220
  ctx.fill();
1395
- ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)';
1396
- ctx.lineWidth = 2.5;
2221
+ ctx.strokeStyle = 'rgba(56, 189, 248, 0.45)';
2222
+ ctx.lineWidth = 3;
1397
2223
  ctx.stroke();
1398
2224
 
1399
- // Ripples in pool
2225
+ for (let rk = 0; rk < 8; rk++) {
2226
+ const ra = (rk / 8) * Math.PI * 2;
2227
+ const rx = poolX + Math.cos(ra) * 280;
2228
+ const ry = poolY + Math.sin(ra) * 68;
2229
+ sCircle(rx, ry, 16, 700 + rk, '#065f46', 2, '#047857');
2230
+ }
2231
+
1400
2232
  for (let rip = 1; rip <= 3; rip++) {
1401
- const ripR = (rip * 65 + t * 45) % 250;
1402
- const ripAlpha = (1 - ripR / 250) * 0.4;
2233
+ const ripR = (rip * 70 + t * 45) % 270;
2234
+ const ripAlpha = (1 - ripR / 270) * 0.45;
1403
2235
  ctx.strokeStyle = 'rgba(56, 189, 248, ' + ripAlpha + ')';
1404
2236
  ctx.beginPath();
1405
- ctx.ellipse(poolX, poolY, ripR, ripR * 0.23, 0, 0, Math.PI * 2);
2237
+ ctx.ellipse(poolX, poolY, ripR, ripR * 0.24, 0, 0, Math.PI * 2);
1406
2238
  ctx.stroke();
1407
2239
  }
1408
2240
 
1409
- // Inverted rippling reflection of character in water (clarifies in Beat 2)
1410
2241
  if (refSpring > 0.1) {
1411
2242
  ctx.save();
1412
- ctx.globalAlpha = Math.min(0.45, refSpring * 0.45);
1413
- ctx.translate(cx, poolY + 20);
2243
+ ctx.globalAlpha = Math.min(0.48, refSpring * 0.48);
2244
+ ctx.translate(cx, poolY + 22);
1414
2245
  ctx.scale(1.2, -0.75);
1415
2246
  drawDoodleCharacter({
1416
2247
  x: 0,
@@ -1425,7 +2256,6 @@ function drawMainScene(t) {
1425
2256
  ctx.restore();
1426
2257
  }
1427
2258
 
1428
- // Standing character looking down and covering body
1429
2259
  drawDoodleCharacter({
1430
2260
  x: cx,
1431
2261
  y: cy + 120,
@@ -1434,42 +2264,35 @@ function drawMainScene(t) {
1434
2264
  expression: refSpring > 0.4 ? 'surprised' : 'talking',
1435
2265
  clothing: 'none',
1436
2266
  armPose: refSpring > 0.4 ? 'cover_body' : 'natural',
2267
+ showShadow: true,
2268
+ showBlush: refSpring > 0.3,
1437
2269
  seed: 105,
1438
2270
  });
1439
2271
 
1440
- // Blushing cheeks appear in Beat 2
1441
- if (refSpring > 0.3) {
1442
- const blushA = Math.min(0.7, refSpring * 0.7);
1443
- sCircle(cx - 18, cy - 8, 7, 750, 'rgba(244,63,94,' + blushA + ')', 1, 'rgba(244,63,94,' + blushA + ')');
1444
- sCircle(cx + 18, cy - 8, 7, 751, 'rgba(244,63,94,' + blushA + ')', 1, 'rgba(244,63,94,' + blushA + ')');
1445
- }
1446
-
1447
- // Theory of Mind Thought Halo in Beat 3
1448
2272
  if (haloSpring > 0.05) {
1449
2273
  ctx.save();
1450
2274
  const haloX = cx + 180;
1451
2275
  const haloY = cy - 120;
1452
2276
  ctx.translate(haloX, haloY);
1453
2277
  ctx.scale(haloSpring, haloSpring);
1454
- sCircle(0, 0, 48, 760, '#38bdf8', 3);
1455
- sCircle(0, 0, 18, 761, '#ffffff', 2.5);
1456
- 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);
2278
+ sCircle(0, 0, 52, 760, '#38bdf8', 3);
2279
+ sCircle(0, 0, 26, 761, STROKE_ALT, 2);
2280
+ sCircle(0, 0, 9, 762, '#38bdf8', 1, '#38bdf8');
1461
2281
  ctx.restore();
1462
2282
  }
1463
2283
 
1464
2284
  } else if (COMP_THEME === 'doodle_tribe_meeting') {
1465
- // Beat 1: Walking toward center -> Beat 2: Greet & check patterns -> Beat 3: Social language arc
2285
+ // Scene 10: Misty Ancient Valley / Megaliths / Tribal Meeting & Social Language Arc
1466
2286
  const approach = spring(t, 0.05, 140, 16);
1467
2287
  const greet = spring(t, window.DURATION * 0.38, 190, 15);
1468
2288
  const arcSpring = spring(t, window.DURATION * 0.68, 200, 15);
1469
2289
 
1470
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
2290
+ ctx.fillStyle = '#334155';
2291
+ ctx.fillRect(cx - 380, cy - 140, 48, 280);
2292
+ ctx.fillRect(cx + 340, cy - 160, 52, 300);
2293
+
2294
+ sLine(0, cy + 140, W, cy + 140, 10, AMBIENT_LINE, 2);
1471
2295
 
1472
- // Left Clan Member walks toward center
1473
2296
  const leftX = cx - 380 + Math.min(1, approach) * 160;
1474
2297
  drawDoodleCharacter({
1475
2298
  x: leftX,
@@ -1479,10 +2302,10 @@ function drawMainScene(t) {
1479
2302
  expression: 'talking',
1480
2303
  clothing: 'pelt',
1481
2304
  armPose: greet > 0.5 ? 'peace_gesture' : 'natural',
2305
+ showShadow: true,
1482
2306
  seed: 106,
1483
2307
  });
1484
2308
 
1485
- // Right Clan Member walks toward center
1486
2309
  const rightX = cx + 380 - Math.min(1, approach) * 160;
1487
2310
  drawDoodleCharacter({
1488
2311
  x: rightX,
@@ -1492,13 +2315,13 @@ function drawMainScene(t) {
1492
2315
  expression: 'curious',
1493
2316
  clothing: 'cloak',
1494
2317
  armPose: greet > 0.5 ? 'peace_gesture' : 'natural',
2318
+ showShadow: true,
1495
2319
  seed: 107,
1496
2320
  });
1497
2321
 
1498
- // Glowing connection arc and symbols in Beat 3
1499
2322
  if (arcSpring > 0.05) {
1500
2323
  ctx.save();
1501
- ctx.globalAlpha = Math.min(1, arcSpring);
2324
+ ctx.globalAlpha = Math.min(0.85, arcSpring * 0.85);
1502
2325
  ctx.strokeStyle = ACCENT;
1503
2326
  ctx.lineWidth = 3;
1504
2327
  ctx.setLineDash([8, 8]);
@@ -1508,22 +2331,16 @@ function drawMainScene(t) {
1508
2331
  ctx.stroke();
1509
2332
  ctx.setLineDash([]);
1510
2333
 
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);
2334
+ sCircle(cx, cy - 65, 28, 800, ACCENT, 3, IS_LIGHT ? '#ffffff' : 'rgba(15, 23, 42, 0.85)');
2335
+ sCircle(cx, cy - 65, 12, 801, STROKE_COLOR, 2, STROKE_COLOR);
1518
2336
  ctx.restore();
1519
2337
  }
1520
2338
 
1521
2339
  } else if (COMP_THEME === 'doodle_thinking_future') {
1522
- // Beat 1: Seated thinker by fire -> Beat 2: Thought cloud floats up -> Beat 3: Tomorrow's rack & tools
2340
+ // Scene 8: Primeval Starlit Night / Contemplative Thinker on Granite Boulder
1523
2341
  const cloudSpring = spring(t, window.DURATION * 0.32, 170, 14);
1524
2342
  const ideaSpring = spring(t, window.DURATION * 0.65, 190, 15);
1525
2343
 
1526
- // Starlit night and crescent moon
1527
2344
  ctx.fillStyle = '#fef08a';
1528
2345
  ctx.beginPath();
1529
2346
  ctx.arc(cx - 360, cy - 190, 50, 0, Math.PI * 2);
@@ -1533,13 +2350,11 @@ function drawMainScene(t) {
1533
2350
  ctx.arc(cx - 345, cy - 200, 48, 0, Math.PI * 2);
1534
2351
  ctx.fill();
1535
2352
 
1536
- // Large rock boulder for thinker
1537
- ctx.fillStyle = '#334155';
2353
+ ctx.fillStyle = IS_LIGHT ? '#78716c' : '#334155';
1538
2354
  ctx.beginPath();
1539
2355
  ctx.ellipse(cx - 140, cy + 175, 85, 55, 0, 0, Math.PI * 2);
1540
2356
  ctx.fill();
1541
2357
 
1542
- // Thinker seated on rock
1543
2358
  drawDoodleCharacter({
1544
2359
  x: cx - 140,
1545
2360
  y: cy + 130,
@@ -1551,7 +2366,6 @@ function drawMainScene(t) {
1551
2366
  seed: 108,
1552
2367
  });
1553
2368
 
1554
- // Expanding thought cloud overhead in Beat 2
1555
2369
  if (cloudSpring > 0.05) {
1556
2370
  const cloudX = cx + 180;
1557
2371
  const cloudY = cy - 80;
@@ -1559,9 +2373,9 @@ function drawMainScene(t) {
1559
2373
  ctx.translate(cloudX, cloudY);
1560
2374
  ctx.scale(cloudSpring, cloudSpring);
1561
2375
 
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');
2376
+ sCircle(-180, 160, 12, 850, STROKE_COLOR, 2, '#ffffff');
2377
+ sCircle(-140, 120, 18, 851, STROKE_COLOR, 2, '#ffffff');
2378
+ sCircle(-90, 70, 24, 852, STROKE_COLOR, 2, '#ffffff');
1565
2379
 
1566
2380
  ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
1567
2381
  ctx.beginPath();
@@ -1570,8 +2384,10 @@ function drawMainScene(t) {
1570
2384
  ctx.arc(70, 0, 55, 0, Math.PI * 2);
1571
2385
  ctx.arc(0, 30, 60, 0, Math.PI * 2);
1572
2386
  ctx.fill();
2387
+ ctx.strokeStyle = STROKE_COLOR;
2388
+ ctx.lineWidth = 2.5;
2389
+ ctx.stroke();
1573
2390
 
1574
- // Inside thought cloud: Tomorrow's sunrise & stretched hide on rack in Beat 3
1575
2391
  if (ideaSpring > 0.1) {
1576
2392
  ctx.fillStyle = '#f59e0b';
1577
2393
  ctx.beginPath();
@@ -1584,23 +2400,17 @@ function drawMainScene(t) {
1584
2400
 
1585
2401
  ctx.fillStyle = '#b45309';
1586
2402
  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
2403
  }
1593
2404
  ctx.restore();
1594
2405
  }
1595
2406
 
1596
2407
  } else if (COMP_THEME === 'doodle_microscope_lice') {
1597
- // Beat 1: Scratching pelt -> Beat 2: Giant microscope slides in -> Beat 3: Divergence tree
2408
+ // Scene 6: Scratching Early Human & Optical Loupe Divergence Tree
1598
2409
  const scopeSpring = spring(t, window.DURATION * 0.30, 180, 15);
1599
2410
  const treeSpring = spring(t, window.DURATION * 0.65, 200, 15);
1600
2411
 
1601
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
2412
+ sLine(0, cy + 140, W, cy + 140, 10, AMBIENT_LINE, 2);
1602
2413
 
1603
- // Early human scratching pelt with itch lines
1604
2414
  drawDoodleCharacter({
1605
2415
  x: cx - 240,
1606
2416
  y: cy + 140,
@@ -1609,6 +2419,7 @@ function drawMainScene(t) {
1609
2419
  expression: 'shivering',
1610
2420
  clothing: 'pelt',
1611
2421
  armPose: 'shiver',
2422
+ showShadow: true,
1612
2423
  seed: 109,
1613
2424
  });
1614
2425
 
@@ -1618,7 +2429,6 @@ function drawMainScene(t) {
1618
2429
  sLine(itX, itY, itX + 18, itY - 14, 900 + it, '#f59e0b', 2.5);
1619
2430
  }
1620
2431
 
1621
- // Microscope slides in in Beat 2
1622
2432
  if (scopeSpring > 0.05) {
1623
2433
  const lensX = cx + 180;
1624
2434
  const lensY = cy;
@@ -1636,39 +2446,27 @@ function drawMainScene(t) {
1636
2446
  ctx.lineWidth = 6;
1637
2447
  ctx.stroke();
1638
2448
 
1639
- // Fabric weave grid lines inside lens
1640
2449
  for (let g = -4; g <= 4; g++) {
1641
2450
  sLine(g * 24, -lensR * 0.8, g * 24, lensR * 0.8, 920 + g, 'rgba(56, 189, 248, 0.15)', 2);
1642
2451
  sLine(-lensR * 0.8, g * 24, lensR * 0.8, g * 24, 930 + g, 'rgba(56, 189, 248, 0.15)', 2);
1643
2452
  }
1644
2453
 
1645
- // Divergence tree in Beat 3
1646
2454
  if (treeSpring > 0.1) {
1647
2455
  sLine(-100, 0, -20, 0, 940, '#f8fafc', 4);
1648
2456
  sCircle(-20, 0, 7, 941, '#d4af37', 2, '#d4af37');
1649
2457
 
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);
2458
+ sLine(-20, 0, 75, -45, 942, '#38bdf8', 4);
2459
+ sCircle(82, -45, 8, 943, '#38bdf8', 2, '#38bdf8');
2460
+
2461
+ sLine(-20, 0, 75, 45, 944, '#f43f5e', 4);
2462
+ sCircle(82, 45, 8, 945, '#f43f5e', 2, '#f43f5e');
1665
2463
  }
1666
2464
  ctx.restore();
1667
2465
  }
1668
2466
 
1669
2467
  } else if (COMP_THEME === 'doodle_general_human') {
1670
- // 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);
2468
+ // General Expressive Illustrated Human Narrator
2469
+ sLine(0, cy + 140, W, cy + 140, 10, AMBIENT_LINE, 2);
1672
2470
  drawDoodleCharacter({
1673
2471
  x: cx,
1674
2472
  y: cy + 140,
@@ -1677,9 +2475,9 @@ function drawMainScene(t) {
1677
2475
  expression: 'talking',
1678
2476
  clothing: 'pelt',
1679
2477
  armPose: 'natural',
2478
+ showShadow: true,
1680
2479
  seed: 110,
1681
2480
  });
1682
-
1683
2481
  } else if (COMP_THEME === 'celestial_orbit') {
1684
2482
  // 3D Celestial Body (Earth) with orbiting satellites & signal pulses
1685
2483
  const earthR = Math.min(W, H) * 0.22;