@amg555/frameforge-mcp 2.4.5 → 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.
@@ -924,6 +924,7 @@ function drawMainScene(t) {
924
924
  ctx.restore();
925
925
  }
926
926
 
927
+ // ── High-Quality Illustrated 2D Character Renderer (Volumetric Anatomy, Pure-t) ──
927
928
  function drawDoodleCharacter({
928
929
  x = cx,
929
930
  y = cy,
@@ -944,320 +945,818 @@ function drawMainScene(t) {
944
945
  ctx.translate(x, y);
945
946
  ctx.scale(scale, scale);
946
947
 
947
- // Ground contact shadow: strictly conditional (default false)
948
+ // Ground contact drop shadow
948
949
  if (showShadow) {
949
- ctx.fillStyle = 'rgba(0, 0, 0, 0.28)';
950
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.32)';
950
951
  ctx.beginPath();
951
- ctx.ellipse(0, legLength, 34, 7, 0, 0, Math.PI * 2);
952
+ ctx.ellipse(0, legLength + 6, 44, 9, 0, 0, Math.PI * 2);
952
953
  ctx.fill();
953
954
  }
954
955
 
955
956
  const blink = (t % 3.2 < 0.14) ? 0.15 : 1.0;
956
- const mouthTalk = Math.abs(Math.sin(t * 14)) * 4;
957
957
  const torsoTopY = -torsoHeight;
958
958
  const torsoBottomY = 0;
959
- const shoulderW = 28;
959
+ const shoulderW = 34;
960
960
 
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';
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;
1039
+
1040
+ ctx.beginPath();
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);
1047
+ ctx.closePath();
1048
+ ctx.fill();
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';
965
1178
  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);
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);
970
1192
  ctx.closePath();
971
1193
  ctx.fill();
972
1194
 
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
1195
  ctx.fillStyle = ACCENT;
981
1196
  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);
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);
1202
+ ctx.closePath();
1203
+ ctx.fill();
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);
986
1246
  ctx.closePath();
987
1247
  ctx.fill();
988
1248
 
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);
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
+
993
1260
  } else if (clothing === 'cloak') {
994
- // Flowing draped mantle
995
1261
  ctx.fillStyle = '#0f766e';
996
1262
  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);
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);
1001
1268
  ctx.closePath();
1002
1269
  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);
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
+
1005
1273
  } 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)';
1274
+ ctx.fillStyle = skinBase;
1008
1275
  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);
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);
1014
1281
  ctx.closePath();
1015
1282
  ctx.fill();
1016
1283
 
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') {
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') {
1041
1337
  ctx.fillStyle = '#1e293b';
1042
1338
  ctx.beginPath();
1043
- 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);
1044
1345
  ctx.closePath();
1045
1346
  ctx.fill();
1046
- 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
+
1047
1352
  } 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);
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
+ }
1050
1397
  }
1051
1398
 
1052
- // Eyes
1399
+ // ── 4. EYES & BROWS ──
1053
1400
  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');
1401
+ const eyeSpacing = 13;
1057
1402
 
1058
- // Specular eye highlights: only when explicitly enabled
1059
- if (showSpecular && blink > 0.4) {
1403
+ for (let side of [-1, 1]) {
1404
+ const ex = side * eyeSpacing;
1060
1405
  ctx.fillStyle = '#ffffff';
1061
1406
  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);
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();
1064
1411
  ctx.fill();
1065
- }
1412
+ ctx.strokeStyle = '#0f172a';
1413
+ ctx.lineWidth = 1.5;
1414
+ ctx.stroke();
1066
1415
 
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();
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);
1074
1436
  }
1075
1437
 
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';
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;
1443
+ ctx.beginPath();
1444
+ ctx.arc(0, noseY, 3.5, 0.1 * Math.PI, 0.9 * Math.PI);
1445
+ ctx.stroke();
1446
+
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';
1097
1453
  ctx.beginPath();
1098
- 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);
1099
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
+
1100
1466
  } else if (expression === 'smiling') {
1467
+ ctx.fillStyle = '#ffffff';
1101
1468
  ctx.beginPath();
1102
- 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();
1103
1472
  ctx.strokeStyle = '#0f172a';
1104
- ctx.lineWidth = 2.5;
1473
+ ctx.lineWidth = 2;
1105
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
+
1106
1480
  } else {
1107
- 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);
1108
1483
  }
1109
1484
 
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);
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
+ }
1128
1492
 
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);
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;
1145
1503
 
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);
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);
1149
1554
  }
1150
1555
 
1151
- // Arms & Hands with natural articulated elbows and gestures
1152
- const shoulderY = torsoTopY + 12;
1153
1556
  if (armPose === 'shiver') {
1154
1557
  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);
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
+
1157
1563
  } else if (armPose === 'hold_spear') {
1158
- // Holding spear with grip
1159
1564
  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
1565
  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';
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';
1167
1581
  ctx.beginPath();
1168
- ctx.moveTo(spearX - 25, cy - 140);
1169
- ctx.lineTo(spearX - 32, cy - 180);
1170
- ctx.lineTo(spearX - 18, cy - 170);
1582
+ ctx.moveTo(spearX - 25, cy - 145);
1583
+ ctx.lineTo(spearX - 35, cy - 195);
1584
+ ctx.lineTo(spearX - 18, cy - 180);
1171
1585
  ctx.closePath();
1172
1586
  ctx.fill();
1173
- sLine(spearX - 28, cy - 140, spearX - 20, cy - 150, seed + 52, '#f59e0b', 3);
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
+
1174
1590
  } else if (armPose === 'sewing') {
1175
- // Left hand holds stretched pelt, right hand drives bone needle with sinew
1176
1591
  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);
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
+
1190
1601
  } 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);
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
+
1608
+ } else if (armPose === 'thinker') {
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
+
1194
1615
  } 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);
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
+
1199
1629
  } 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);
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);
1206
1637
  }
1207
1638
 
1208
1639
  ctx.restore();
1209
1640
  }
1210
1641
 
1211
- // ── Doodled Character Narrative Themes (3-Beat Progressive Choreography) ─
1642
+ // ── Production-Grade Illustrated Environments & Narrative Themes ──
1212
1643
  if (COMP_THEME === 'doodle_dressing') {
1213
- // Beat 1: Browsing wardrobe -> Beat 2: Trying shirt -> Beat 3: Looking in mirror reflection
1644
+ // Scene 1: Modern Luxury Dressing Room & Walk-in Wardrobe
1214
1645
  const beat2 = spring(t, window.DURATION * 0.35, 180, 16);
1215
1646
  const beat3 = spring(t, window.DURATION * 0.70, 190, 16);
1216
1647
 
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];
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;
1231
1716
  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);
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);
1236
1721
  ctx.closePath();
1237
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);
1238
1726
  }
1239
1727
 
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) + ')';
1728
+ // 3. Arched Full-Length Brass Mirror
1729
+ const mX = cx + 330;
1730
+ const mY = cy + 20;
1731
+ ctx.fillStyle = '#b45309';
1244
1732
  ctx.beginPath();
1245
- ctx.roundRect(mirrorX - 85, mirrorY - 210, 170, 390, 24);
1733
+ ctx.roundRect(mX - 100, mY - 230, 200, 420, [100, 100, 10, 10]);
1246
1734
  ctx.fill();
1247
- ctx.strokeStyle = beat3 > 0.5 ? '#f59e0b' : '#94a3b8';
1248
- ctx.lineWidth = 4;
1735
+ ctx.strokeStyle = '#d4af37';
1736
+ ctx.lineWidth = 6;
1249
1737
  ctx.stroke();
1250
1738
 
1251
- // Reflected character in mirror (smiling and thumbs up in beat 3)
1252
- 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) {
1253
1752
  ctx.save();
1254
- ctx.globalAlpha = Math.min(0.65, beat2 * 0.65);
1753
+ ctx.globalAlpha = Math.min(0.85, beat2 * 0.85);
1255
1754
  drawDoodleCharacter({
1256
- x: mirrorX,
1755
+ x: mX,
1257
1756
  y: cy + 120,
1258
1757
  scale: 1.05,
1259
1758
  hairStyle: 'modern',
1260
- expression: beat3 > 0.5 ? 'smiling' : 'curious',
1759
+ expression: beat3 > 0.5 ? 'smiling' : 'talking',
1261
1760
  clothing: 'shirt',
1262
1761
  armPose: beat3 > 0.5 ? 'peace_gesture' : 'dress_shirt',
1263
1762
  seed: 88,
@@ -1265,8 +1764,8 @@ function drawMainScene(t) {
1265
1764
  ctx.restore();
1266
1765
  }
1267
1766
 
1268
- // Main character moves progressively from rack -> center -> mirror
1269
- const charX = cx - 220 + beat2 * 170 + beat3 * 60;
1767
+ // 4. Main Character
1768
+ const charX = cx - 180 + beat2 * 140 + beat3 * 50;
1270
1769
  drawDoodleCharacter({
1271
1770
  x: charX,
1272
1771
  y: cy + 140,
@@ -1275,93 +1774,223 @@ function drawMainScene(t) {
1275
1774
  expression: beat3 > 0.5 ? 'smiling' : 'talking',
1276
1775
  clothing: 'shirt',
1277
1776
  armPose: beat2 > 0.5 ? 'dress_shirt' : 'natural',
1777
+ showShadow: true,
1278
1778
  seed: 101,
1279
1779
  });
1280
1780
 
1281
1781
  } else if (COMP_THEME === 'doodle_modern_vs_prehistoric') {
1282
- // Split-screen comparison: Modern stylish human on left vs Prehistoric human on right
1782
+ // Scene 2: Split-Screen Comparison
1283
1783
  const dur = window.DURATION || 3;
1284
1784
  const bridgeSpring = spring(t, dur * 0.35, 170, 15);
1285
1785
 
1286
- // Natural ground line
1287
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2.5);
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();
1288
1830
 
1289
- // Subtle split-screen center divider
1831
+ // Savannah half (Right cx to W)
1290
1832
  ctx.save();
1291
- ctx.setLineDash([10, 10]);
1292
- sLine(cx, cy - 220, cx, cy + 200, 20, AMBIENT_LINE, 2);
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);
1293
1886
  ctx.restore();
1294
1887
 
1295
- // Modern human on left
1888
+ // Modern Human on Left
1296
1889
  drawDoodleCharacter({
1297
1890
  x: cx - 260,
1298
1891
  y: cy + 140,
1299
- scale: 1.3,
1892
+ scale: 1.35,
1300
1893
  hairStyle: 'modern',
1301
1894
  expression: 'talking',
1302
1895
  clothing: 'shirt',
1303
1896
  armPose: 'dress_shirt',
1897
+ showShadow: true,
1304
1898
  seed: 120,
1305
1899
  });
1306
1900
 
1307
- // Prehistoric human on right
1901
+ // Prehistoric Human on Right
1308
1902
  drawDoodleCharacter({
1309
1903
  x: cx + 260,
1310
1904
  y: cy + 140,
1311
- scale: 1.3,
1905
+ scale: 1.35,
1312
1906
  hairStyle: 'wild',
1313
1907
  expression: 'curious',
1314
1908
  clothing: 'none',
1315
1909
  armPose: 'natural',
1910
+ showShadow: true,
1316
1911
  seed: 121,
1317
1912
  });
1318
1913
 
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([]);
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();
1331
1938
 
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
- }
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();
1337
1945
 
1338
1946
  } else if (COMP_THEME === 'doodle_shiver_cold') {
1339
- // Narrative beat progression based strictly on audio duration (window.DURATION)
1947
+ // Scene 3: Ice Age Cold / Glacial Wilderness / Volumetric Sunbeams
1340
1948
  const dur = window.DURATION || 3;
1341
1949
  const beat2 = spring(t, dur * 0.45, 160, 15);
1342
1950
 
1343
- // Natural ground line
1344
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
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);
1345
1977
 
1346
- // Freezing wind streaks (rush during cold beat, calm as sun breaks)
1347
1978
  const windAlpha = Math.max(0, 1 - beat2 * 0.85);
1348
1979
  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);
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);
1354
1985
  }
1355
1986
  }
1356
1987
 
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');
1988
+ for (let sp = 0; sp < 28; sp++) {
1989
+ const px = ((sp * 75 + t * 450) % W);
1990
+ const py = (sp * 38 + t * 90) % H;
1991
+ sCircle(px, py, 2.5, 300 + sp, '#e0f2fe', 1, '#ffffff');
1362
1992
  }
1363
1993
 
1364
- // Shivering breath vapor cloud (calms in Beat 2)
1365
1994
  if (beat2 < 0.7) {
1366
1995
  const breathPhase = (t * 2.2) % 1;
1367
1996
  const bCloudX = cx - 50 + breathPhase * 60;
@@ -1372,43 +2001,48 @@ function drawMainScene(t) {
1372
2001
  ctx.fill();
1373
2002
  }
1374
2003
 
1375
- // Warm golden sunlight breaks in (visual myth-busting revelation)
1376
2004
  if (beat2 > 0.05) {
1377
2005
  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)');
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)');
1381
2009
  sunBeamGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
1382
2010
  ctx.fillStyle = sunBeamGrad;
1383
2011
  ctx.beginPath();
1384
- ctx.arc(cx + 180, cy - 180, 260, 0, Math.PI * 2);
2012
+ ctx.arc(cx + 220, cy - 180, 320, 0, Math.PI * 2);
1385
2013
  ctx.fill();
1386
2014
  ctx.restore();
1387
2015
  }
1388
2016
 
1389
- // Main character: shivers in cold beat, then pauses and looks at arms in wonder as sun illuminates
1390
2017
  const isShivering = beat2 < 0.6;
1391
2018
  const vibX = isShivering ? Math.sin(t * 38) * 4.5 : 0;
1392
2019
  const vibY = isShivering ? Math.cos(t * 32) * 2 : 0;
1393
2020
  drawDoodleCharacter({
1394
- x: cx - 100 + beat2 * 80 + vibX,
2021
+ x: cx - 80 + beat2 * 80 + vibX,
1395
2022
  y: cy + 140 + vibY,
1396
2023
  scale: 1.35,
1397
2024
  hairStyle: 'wild',
1398
2025
  expression: isShivering ? 'shivering' : 'curious',
1399
2026
  clothing: 'none',
1400
2027
  armPose: isShivering ? 'shiver' : 'thinker',
2028
+ showShadow: true,
1401
2029
  seed: 102,
1402
2030
  });
1403
2031
 
1404
2032
  } else if (COMP_THEME === 'doodle_hunter_savannah') {
1405
- // Beat 1: Walking with spear -> Beat 2: Swatting mosquitoes -> Beat 3: Pausing alert
2033
+ // Scene 5: African Savannah Plains / Hunter with Carved Spear
1406
2034
  const walkProg = Math.min(1, Math.max(0, t / window.DURATION));
1407
2035
  const swatting = (t % 2.4 > 1.2);
1408
2036
 
1409
- // 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
+
1410
2044
  const sunR = 75 + Math.sin(t * 2) * 6;
1411
- ctx.fillStyle = '#f59e0b';
2045
+ ctx.fillStyle = '#fef08a';
1412
2046
  ctx.beginPath();
1413
2047
  ctx.arc(cx - 380, cy - 180, sunR, 0, Math.PI * 2);
1414
2048
  ctx.fill();
@@ -1421,23 +2055,35 @@ function drawMainScene(t) {
1421
2055
  sLine(rx1, ry1, rx2, ry2, 450 + r, '#fbbf24', 3);
1422
2056
  }
1423
2057
 
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;
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;
1428
2076
  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);
2077
+ sLine(gx, cy + 140, gx + sway, cy + 50, 500 + g, '#f59e0b', 2.5);
1430
2078
  }
1431
2079
 
1432
- // Buzzing mosquitoes orbiting hunter
1433
- const charX = cx - 180 + walkProg * 240;
2080
+ const charX = cx - 220 + walkProg * 320;
1434
2081
  for (let m = 0; m < 4; m++) {
1435
2082
  const mx = charX + 50 + Math.sin(t * 8 + m * 2) * 65;
1436
2083
  const my = cy - 60 + Math.cos(t * 7 + m * 2) * 45;
1437
2084
  sCircle(mx, my, 2.5, 550 + m, '#0f172a', 1.5, '#ef4444');
1438
2085
  }
1439
2086
 
1440
- // Walking early hunter with spear
1441
2087
  drawDoodleCharacter({
1442
2088
  x: charX,
1443
2089
  y: cy + 140,
@@ -1446,32 +2092,44 @@ function drawMainScene(t) {
1446
2092
  expression: swatting ? 'shivering' : 'talking',
1447
2093
  clothing: 'none',
1448
2094
  armPose: 'hold_spear',
2095
+ showShadow: true,
1449
2096
  seed: 103,
1450
2097
  });
1451
2098
 
1452
2099
  } else if (COMP_THEME === 'doodle_sewing_needle') {
1453
- // 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
1454
2101
  const loupeSpring = spring(t, window.DURATION * 0.32, 180, 15);
1455
2102
  const badgeSpring = spring(t, window.DURATION * 0.68, 200, 15);
1456
2103
 
1457
- // 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
+
1458
2113
  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;
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;
1462
2121
  ctx.fillStyle = f % 2 === 0 ? '#ef4444' : '#f59e0b';
1463
2122
  ctx.beginPath();
1464
- ctx.moveTo(fireX - 30 + f * 12, fireY);
1465
- 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);
1466
2125
  ctx.fill();
1467
2126
  }
1468
2127
  for (let sp = 0; sp < 8; sp++) {
1469
2128
  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');
2129
+ const sy = fireY - 30 - ((t * 80 + sp * 35) % 150);
2130
+ sCircle(sx, sy, 2.2, 610 + sp, '#fbbf24', 1, '#fbbf24');
1472
2131
  }
1473
2132
 
1474
- // Seated human stitching hide with bone needle
1475
2133
  drawDoodleCharacter({
1476
2134
  x: cx - 120,
1477
2135
  y: cy + 140,
@@ -1480,82 +2138,110 @@ function drawMainScene(t) {
1480
2138
  expression: 'talking',
1481
2139
  clothing: 'pelt',
1482
2140
  armPose: 'sewing',
2141
+ showShadow: true,
1483
2142
  seed: 104,
1484
2143
  });
1485
2144
 
1486
- // Zoomed loupe expands open in Beat 2
1487
2145
  if (loupeSpring > 0.05) {
1488
2146
  ctx.save();
1489
- const lensX = cx + 270;
1490
- const lensY = cy - 20;
2147
+ const lensX = cx + 280;
2148
+ const lensY = cy - 10;
1491
2149
  ctx.translate(lensX, lensY);
1492
2150
  ctx.scale(loupeSpring, loupeSpring);
1493
2151
 
1494
- const lensR = 140;
1495
- ctx.fillStyle = 'rgba(15, 23, 42, 0.92)';
2152
+ const lensR = 150;
2153
+ ctx.fillStyle = '#090d16';
1496
2154
  ctx.beginPath();
1497
2155
  ctx.arc(0, 0, lensR, 0, Math.PI * 2);
1498
2156
  ctx.fill();
2157
+
1499
2158
  ctx.strokeStyle = '#d4af37';
1500
- ctx.lineWidth = 5;
2159
+ ctx.lineWidth = 8;
1501
2160
  ctx.stroke();
1502
2161
 
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);
2162
+ ctx.fillStyle = '#78350f';
2163
+ ctx.fillRect(-120, -75, 240, 60);
2164
+ ctx.fillStyle = '#92400e';
2165
+ ctx.fillRect(-120, 15, 240, 60);
1506
2166
 
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');
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
+ }
1511
2172
 
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);
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;
2184
+ ctx.stroke();
2185
+
2186
+ sCircle(-42 + nWiggle, -62, 4, 650, '#0f172a', 1.5, '#0f172a');
2187
+
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();
2194
+
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);
1516
2198
  }
1517
2199
  ctx.restore();
1518
2200
  }
1519
2201
 
1520
- // Ancient stone strata and bone needle engraving in Beat 3
1521
2202
  if (badgeSpring > 0.05) {
1522
2203
  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);
2204
+ ctx.globalAlpha = Math.min(0.85, badgeSpring * 0.85);
2205
+ sLine(cx + 140, cy + 160, cx + 420, cy + 160, 670, '#d4af37', 2);
1526
2206
  ctx.restore();
1527
2207
  }
1528
2208
 
1529
2209
  } else if (COMP_THEME === 'doodle_shame_reflection') {
1530
- // 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
1531
2211
  const refSpring = spring(t, window.DURATION * 0.30, 180, 15);
1532
2212
  const haloSpring = spring(t, window.DURATION * 0.65, 190, 15);
1533
2213
 
1534
2214
  const poolX = cx;
1535
- const poolY = cy + 180;
1536
- ctx.fillStyle = 'rgba(14, 165, 233, 0.15)';
2215
+ const poolY = cy + 170;
2216
+
2217
+ ctx.fillStyle = 'rgba(14, 165, 233, 0.18)';
1537
2218
  ctx.beginPath();
1538
- ctx.ellipse(poolX, poolY, 260, 60, 0, 0, Math.PI * 2);
2219
+ ctx.ellipse(poolX, poolY, 280, 68, 0, 0, Math.PI * 2);
1539
2220
  ctx.fill();
1540
- ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)';
1541
- ctx.lineWidth = 2.5;
2221
+ ctx.strokeStyle = 'rgba(56, 189, 248, 0.45)';
2222
+ ctx.lineWidth = 3;
1542
2223
  ctx.stroke();
1543
2224
 
1544
- // 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
+
1545
2232
  for (let rip = 1; rip <= 3; rip++) {
1546
- const ripR = (rip * 65 + t * 45) % 250;
1547
- const ripAlpha = (1 - ripR / 250) * 0.4;
2233
+ const ripR = (rip * 70 + t * 45) % 270;
2234
+ const ripAlpha = (1 - ripR / 270) * 0.45;
1548
2235
  ctx.strokeStyle = 'rgba(56, 189, 248, ' + ripAlpha + ')';
1549
2236
  ctx.beginPath();
1550
- 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);
1551
2238
  ctx.stroke();
1552
2239
  }
1553
2240
 
1554
- // Inverted rippling reflection of character in water (clarifies in Beat 2)
1555
2241
  if (refSpring > 0.1) {
1556
2242
  ctx.save();
1557
- ctx.globalAlpha = Math.min(0.45, refSpring * 0.45);
1558
- ctx.translate(cx, poolY + 20);
2243
+ ctx.globalAlpha = Math.min(0.48, refSpring * 0.48);
2244
+ ctx.translate(cx, poolY + 22);
1559
2245
  ctx.scale(1.2, -0.75);
1560
2246
  drawDoodleCharacter({
1561
2247
  x: 0,
@@ -1570,7 +2256,6 @@ function drawMainScene(t) {
1570
2256
  ctx.restore();
1571
2257
  }
1572
2258
 
1573
- // Standing character looking down and covering body
1574
2259
  drawDoodleCharacter({
1575
2260
  x: cx,
1576
2261
  y: cy + 120,
@@ -1579,38 +2264,35 @@ function drawMainScene(t) {
1579
2264
  expression: refSpring > 0.4 ? 'surprised' : 'talking',
1580
2265
  clothing: 'none',
1581
2266
  armPose: refSpring > 0.4 ? 'cover_body' : 'natural',
2267
+ showShadow: true,
2268
+ showBlush: refSpring > 0.3,
1582
2269
  seed: 105,
1583
2270
  });
1584
2271
 
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
2272
  if (haloSpring > 0.05) {
1594
2273
  ctx.save();
1595
2274
  const haloX = cx + 180;
1596
2275
  const haloY = cy - 120;
1597
2276
  ctx.translate(haloX, haloY);
1598
2277
  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');
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');
1602
2281
  ctx.restore();
1603
2282
  }
1604
2283
 
1605
2284
  } else if (COMP_THEME === 'doodle_tribe_meeting') {
1606
- // 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
1607
2286
  const approach = spring(t, 0.05, 140, 16);
1608
2287
  const greet = spring(t, window.DURATION * 0.38, 190, 15);
1609
2288
  const arcSpring = spring(t, window.DURATION * 0.68, 200, 15);
1610
2289
 
1611
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 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);
1612
2295
 
1613
- // Left Clan Member walks toward center
1614
2296
  const leftX = cx - 380 + Math.min(1, approach) * 160;
1615
2297
  drawDoodleCharacter({
1616
2298
  x: leftX,
@@ -1620,10 +2302,10 @@ function drawMainScene(t) {
1620
2302
  expression: 'talking',
1621
2303
  clothing: 'pelt',
1622
2304
  armPose: greet > 0.5 ? 'peace_gesture' : 'natural',
2305
+ showShadow: true,
1623
2306
  seed: 106,
1624
2307
  });
1625
2308
 
1626
- // Right Clan Member walks toward center
1627
2309
  const rightX = cx + 380 - Math.min(1, approach) * 160;
1628
2310
  drawDoodleCharacter({
1629
2311
  x: rightX,
@@ -1633,10 +2315,10 @@ function drawMainScene(t) {
1633
2315
  expression: 'curious',
1634
2316
  clothing: 'cloak',
1635
2317
  armPose: greet > 0.5 ? 'peace_gesture' : 'natural',
2318
+ showShadow: true,
1636
2319
  seed: 107,
1637
2320
  });
1638
2321
 
1639
- // Glowing connection arc in Beat 3
1640
2322
  if (arcSpring > 0.05) {
1641
2323
  ctx.save();
1642
2324
  ctx.globalAlpha = Math.min(0.85, arcSpring * 0.85);
@@ -1655,11 +2337,10 @@ function drawMainScene(t) {
1655
2337
  }
1656
2338
 
1657
2339
  } 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
2340
+ // Scene 8: Primeval Starlit Night / Contemplative Thinker on Granite Boulder
1659
2341
  const cloudSpring = spring(t, window.DURATION * 0.32, 170, 14);
1660
2342
  const ideaSpring = spring(t, window.DURATION * 0.65, 190, 15);
1661
2343
 
1662
- // Starlit night and crescent moon
1663
2344
  ctx.fillStyle = '#fef08a';
1664
2345
  ctx.beginPath();
1665
2346
  ctx.arc(cx - 360, cy - 190, 50, 0, Math.PI * 2);
@@ -1669,13 +2350,11 @@ function drawMainScene(t) {
1669
2350
  ctx.arc(cx - 345, cy - 200, 48, 0, Math.PI * 2);
1670
2351
  ctx.fill();
1671
2352
 
1672
- // Large rock boulder for thinker
1673
2353
  ctx.fillStyle = IS_LIGHT ? '#78716c' : '#334155';
1674
2354
  ctx.beginPath();
1675
2355
  ctx.ellipse(cx - 140, cy + 175, 85, 55, 0, 0, Math.PI * 2);
1676
2356
  ctx.fill();
1677
2357
 
1678
- // Thinker seated on rock
1679
2358
  drawDoodleCharacter({
1680
2359
  x: cx - 140,
1681
2360
  y: cy + 130,
@@ -1687,7 +2366,6 @@ function drawMainScene(t) {
1687
2366
  seed: 108,
1688
2367
  });
1689
2368
 
1690
- // Expanding thought cloud overhead in Beat 2
1691
2369
  if (cloudSpring > 0.05) {
1692
2370
  const cloudX = cx + 180;
1693
2371
  const cloudY = cy - 80;
@@ -1710,7 +2388,6 @@ function drawMainScene(t) {
1710
2388
  ctx.lineWidth = 2.5;
1711
2389
  ctx.stroke();
1712
2390
 
1713
- // Inside thought cloud: Tomorrow's sunrise & stretched hide on rack in Beat 3
1714
2391
  if (ideaSpring > 0.1) {
1715
2392
  ctx.fillStyle = '#f59e0b';
1716
2393
  ctx.beginPath();
@@ -1728,13 +2405,12 @@ function drawMainScene(t) {
1728
2405
  }
1729
2406
 
1730
2407
  } else if (COMP_THEME === 'doodle_microscope_lice') {
1731
- // Beat 1: Scratching pelt -> Beat 2: Giant microscope slides in -> Beat 3: Divergence tree
2408
+ // Scene 6: Scratching Early Human & Optical Loupe Divergence Tree
1732
2409
  const scopeSpring = spring(t, window.DURATION * 0.30, 180, 15);
1733
2410
  const treeSpring = spring(t, window.DURATION * 0.65, 200, 15);
1734
2411
 
1735
- sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
2412
+ sLine(0, cy + 140, W, cy + 140, 10, AMBIENT_LINE, 2);
1736
2413
 
1737
- // Early human scratching pelt with itch lines
1738
2414
  drawDoodleCharacter({
1739
2415
  x: cx - 240,
1740
2416
  y: cy + 140,
@@ -1743,6 +2419,7 @@ function drawMainScene(t) {
1743
2419
  expression: 'shivering',
1744
2420
  clothing: 'pelt',
1745
2421
  armPose: 'shiver',
2422
+ showShadow: true,
1746
2423
  seed: 109,
1747
2424
  });
1748
2425
 
@@ -1752,7 +2429,6 @@ function drawMainScene(t) {
1752
2429
  sLine(itX, itY, itX + 18, itY - 14, 900 + it, '#f59e0b', 2.5);
1753
2430
  }
1754
2431
 
1755
- // Microscope slides in in Beat 2
1756
2432
  if (scopeSpring > 0.05) {
1757
2433
  const lensX = cx + 180;
1758
2434
  const lensY = cy;
@@ -1770,13 +2446,11 @@ function drawMainScene(t) {
1770
2446
  ctx.lineWidth = 6;
1771
2447
  ctx.stroke();
1772
2448
 
1773
- // Fabric weave grid lines inside lens
1774
2449
  for (let g = -4; g <= 4; g++) {
1775
2450
  sLine(g * 24, -lensR * 0.8, g * 24, lensR * 0.8, 920 + g, 'rgba(56, 189, 248, 0.15)', 2);
1776
2451
  sLine(-lensR * 0.8, g * 24, lensR * 0.8, g * 24, 930 + g, 'rgba(56, 189, 248, 0.15)', 2);
1777
2452
  }
1778
2453
 
1779
- // Divergence tree in Beat 3
1780
2454
  if (treeSpring > 0.1) {
1781
2455
  sLine(-100, 0, -20, 0, 940, '#f8fafc', 4);
1782
2456
  sCircle(-20, 0, 7, 941, '#d4af37', 2, '#d4af37');
@@ -1791,8 +2465,8 @@ function drawMainScene(t) {
1791
2465
  }
1792
2466
 
1793
2467
  } 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);
2468
+ // General Expressive Illustrated Human Narrator
2469
+ sLine(0, cy + 140, W, cy + 140, 10, AMBIENT_LINE, 2);
1796
2470
  drawDoodleCharacter({
1797
2471
  x: cx,
1798
2472
  y: cy + 140,
@@ -1801,9 +2475,9 @@ function drawMainScene(t) {
1801
2475
  expression: 'talking',
1802
2476
  clothing: 'pelt',
1803
2477
  armPose: 'natural',
2478
+ showShadow: true,
1804
2479
  seed: 110,
1805
2480
  });
1806
-
1807
2481
  } else if (COMP_THEME === 'celestial_orbit') {
1808
2482
  // 3D Celestial Body (Earth) with orbiting satellites & signal pulses
1809
2483
  const earthR = Math.min(W, H) * 0.22;