@amg555/frameforge-mcp 2.4.5 → 2.4.7

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