@amg555/frameforge-mcp 2.4.3 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/package.json +1 -1
- package/scripts/deep-validate.js +2 -2
- package/src/cli/make.js +10 -0
- package/src/mcp-server/server.js +6 -2
- package/src/video-renderer/renderer.js +27 -8
- package/src/video-renderer/scene-animator.js +402 -203
- package/src/workflows/assets.js +2 -1
- package/src/workflows/ingest.js +4 -1
- package/src/workflows/render.js +2 -0
- package/src/workflows/script.js +4 -4
|
@@ -45,6 +45,19 @@ function getScriptFontStack(text = '', language = '') {
|
|
|
45
45
|
|
|
46
46
|
// ── Aesthetic Definitions ──────────────────────────────────────────────────────
|
|
47
47
|
const AESTHETICS = {
|
|
48
|
+
hand_drawn_doodle: {
|
|
49
|
+
name: 'Hand-Drawn Doodle',
|
|
50
|
+
palettes: [
|
|
51
|
+
{ bg0: '#ffffff', bg1: '#faf7f2', bg2: '#f6f1e8', stroke: '#1c1917', strokeAlt: '#44403c', accent: '#c2410c', accent2: '#d97706', cardBg: 'rgba(255, 255, 255, 0.95)', cardBorder: 'rgba(28, 25, 23, 0.25)', tag: 'DOODLE', isLight: true },
|
|
52
|
+
{ bg0: '#fdfbf7', bg1: '#f8f4eb', bg2: '#f3ece0', stroke: '#18181b', strokeAlt: '#3f3f46', accent: '#15803d', accent2: '#b45309', cardBg: 'rgba(255, 255, 255, 0.95)', cardBorder: 'rgba(24, 24, 27, 0.25)', tag: 'SKETCH', isLight: true },
|
|
53
|
+
{ bg0: '#fcfaf5', bg1: '#f7f2e7', bg2: '#f0e8d8', stroke: '#0f172a', strokeAlt: '#334155', accent: '#0369a1', accent2: '#c2410c', cardBg: 'rgba(255, 255, 255, 0.95)', cardBorder: 'rgba(15, 23, 42, 0.25)', tag: 'INK', isLight: true },
|
|
54
|
+
],
|
|
55
|
+
fontFamily: (script) => script ? `${script}, 'Caveat', 'Patrick Hand', 'Comic Sans MS', cursive, sans-serif` : `'Caveat', 'Patrick Hand', 'Segoe UI', cursive, sans-serif`,
|
|
56
|
+
fontDisplayWeight: '700',
|
|
57
|
+
backgroundType: 'paper_texture',
|
|
58
|
+
springTension: 160,
|
|
59
|
+
springFriction: 14,
|
|
60
|
+
},
|
|
48
61
|
cyber_tech: {
|
|
49
62
|
name: 'Cyber Tech',
|
|
50
63
|
palettes: [
|
|
@@ -191,6 +204,7 @@ function detectAesthetic({
|
|
|
191
204
|
// Explicit override wins
|
|
192
205
|
const explicit = (theme || style || '').toLowerCase().trim();
|
|
193
206
|
if (AESTHETICS[explicit]) return explicit;
|
|
207
|
+
if (/doodle|sketch|hand[-\s]?drawn|drawing|white background|cream background|cartoon|illustration/i.test(explicit)) return 'hand_drawn_doodle';
|
|
194
208
|
if (/cyber|tech|code|ai|crypto|hacker|futur/i.test(explicit)) return 'cyber_tech';
|
|
195
209
|
if (/doc|history|ancient|story|cinema|kerala|temple|culture/i.test(explicit)) return 'cinematic_documentary';
|
|
196
210
|
if (/finan|market|stock|money|invest|crypto|business/i.test(explicit)) return 'finance_executive';
|
|
@@ -204,6 +218,12 @@ function detectAesthetic({
|
|
|
204
218
|
// Keyword scan across all textual signals (English + Multilingual)
|
|
205
219
|
const combined = `${topic} ${title} ${visualDescription} ${narration} ${caption} ${musicMood}`.toLowerCase();
|
|
206
220
|
|
|
221
|
+
// Hand-Drawn Doodle / Sketch / Cartoon
|
|
222
|
+
if (/(?:^|[^\w])(doodle|sketch|hand[-\s]?drawn|drawing|white background|cream background|paper|cartoon)(?:[^\w]|$)/.test(combined)
|
|
223
|
+
|| /ഡൂഡിൽ|വര|വരച്ച|ചിത്രരചന|रेखाचित्र|कार्टून/.test(combined)) {
|
|
224
|
+
return 'hand_drawn_doodle';
|
|
225
|
+
}
|
|
226
|
+
|
|
207
227
|
// Cosmic Space / Quantum / Orbital
|
|
208
228
|
if (/(?:^|[^\w])(quantum|astronomy|galaxy|universe|space|mars|satellite|telescope|cosmos|interstellar|orbit|gps|satellites)(?:[^\w]|$)/.test(combined)
|
|
209
229
|
|| /പ്രപഞ്ച|ബഹിരാകാശ|നക്ഷത്ര|ഗ്രഹം|ഉപഗ്രഹ|ജ്യോതിശാസ്ത്ര|अंतरिक्ष|ब्रह्मांड|तारा|விண்வெளி|பிரபஞ்சம்/.test(combined)) {
|
|
@@ -263,9 +283,10 @@ function detectAesthetic({
|
|
|
263
283
|
/**
|
|
264
284
|
* Detect the graphic entity to render on the full canvas for a given scene.
|
|
265
285
|
*/
|
|
266
|
-
function detectComponentTheme({ visualDescription = '', caption = '', narration = '', topic = '', imagePath = null, aesthetic = 'cyber_tech' } = {}) {
|
|
286
|
+
function detectComponentTheme({ visualDescription = '', visual_description = '', prompt = '', caption = '', narration = '', topic = '', imagePath = null, aesthetic = 'cyber_tech' } = {}) {
|
|
267
287
|
if (imagePath) return 'image';
|
|
268
|
-
const
|
|
288
|
+
const visDesc = visualDescription || visual_description || prompt || '';
|
|
289
|
+
const text = `${visDesc} ${caption} ${narration} ${topic}`.toLowerCase();
|
|
269
290
|
|
|
270
291
|
// ── Doodled Character Actions (Human Stories, History, Evolution, Daily Life) ──
|
|
271
292
|
// 1. Microscope / Lice Theory / Parasite / Genetics (Scene 7)
|
|
@@ -310,13 +331,19 @@ function detectComponentTheme({ visualDescription = '', caption = '', narration
|
|
|
310
331
|
return 'doodle_shame_reflection';
|
|
311
332
|
}
|
|
312
333
|
|
|
313
|
-
// 8.
|
|
334
|
+
// 8. Modern vs Prehistoric Human Comparison (Scene 2)
|
|
335
|
+
if (/(?:^|[^\w])(modern vs ancient|modern human|fashionable|modern vs prehistoric|split-screen|split screen|transformation|then and now|evolution of human)(?:[^\w]|$)/i.test(text)
|
|
336
|
+
|| /ഫാഷൻ|ആധുനിക മനുഷ്യൻ|പുരാതന മനുഷ്യൻ|കടകളോ|ആധുനികവും പുരാതനവും/.test(text)) {
|
|
337
|
+
return 'doodle_modern_vs_prehistoric';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 9. Dressing / Wardrobe / Wearing Clothes / Outfit / Morning Routine (Scene 1, 11)
|
|
314
341
|
if (/(?:^|[^\w])(dress|dressing|wearing clothes|shirt|wardrobe|closet|hanger|outfit|choose clothes|wear|garment|wedding dress|interview|daily habit|why we wear clothes)(?:[^\w]|$)/i.test(text)
|
|
315
|
-
||
|
|
342
|
+
|| /വസ്ത്രധാരണം|ധരിച്ചിട്ടുണ്ടാ|ധരിച്ച|വസ്ത്രം ധരിക്കാൻ|വസ്ത്രത്തിന്|വസ്ത്രം തിരഞ്ഞെടുക്കു|ഷർട്ട്|തിരഞ്ഞെടുത്തത്|തുണി എടുത്തിടുകയല്ല|കല്യാണത്തിന്|ഇന്റർവ്യൂ|ആമുഖം: വസ്ത്രധാരണം|വസ്ത്രം തിരഞ്ഞെടുക്കുമ്പോൾ|വസ്ത്രം തിരഞ്ഞെടുക്കും/.test(text)) {
|
|
316
343
|
return 'doodle_dressing';
|
|
317
344
|
}
|
|
318
345
|
|
|
319
|
-
//
|
|
346
|
+
// 10. General Human Character
|
|
320
347
|
if (/(?:^|[^\w])(human|person|man|woman|people|child|guy|doodle|character|actor)(?:[^\w]|$)/i.test(text)
|
|
321
348
|
|| /മനുഷ്യൻ|ആളുകൾ|വ്യക്തി/.test(text)) {
|
|
322
349
|
return 'doodle_general_human';
|
|
@@ -497,6 +524,13 @@ window.seek = async (t) => {
|
|
|
497
524
|
</html>`;
|
|
498
525
|
}
|
|
499
526
|
|
|
527
|
+
// ── Captions Requested Detector ───────────────────────────────────────────────
|
|
528
|
+
/** Returns true ONLY if caller explicitly asks for on-screen captions or subtitles in prompt. */
|
|
529
|
+
function detectCaptionsRequested(text = '') {
|
|
530
|
+
if (!text || typeof text !== 'string') return false;
|
|
531
|
+
return /(?:with|show|display|include|enable|add)\s+(?:captions?|subtitles?|sub-titles?|subs?)|(?:captions?|subtitles?)\s*[:=]\s*(?:true|yes|1|on|always)/i.test(text);
|
|
532
|
+
}
|
|
533
|
+
|
|
500
534
|
// ── HTML Scene Code Generator ──────────────────────────────────────────────────
|
|
501
535
|
/**
|
|
502
536
|
* Generate a complete, self-contained HTML scene conforming to window.seek(t).
|
|
@@ -513,11 +547,19 @@ function generateSceneHtml({
|
|
|
513
547
|
title = '',
|
|
514
548
|
language = '',
|
|
515
549
|
musicMood = 'neutral',
|
|
550
|
+
camera = 'auto',
|
|
551
|
+
cameraHint = 'auto',
|
|
516
552
|
duration = 3,
|
|
517
553
|
width = 1920,
|
|
518
554
|
height = 1080,
|
|
519
555
|
sceneIndex = 0,
|
|
556
|
+
showCaptions = false,
|
|
520
557
|
} = {}) {
|
|
558
|
+
const captionsWanted = Boolean(
|
|
559
|
+
showCaptions ||
|
|
560
|
+
detectCaptionsRequested(`${caption} ${narration} ${topic} ${visualDescription} ${style} ${title}`) ||
|
|
561
|
+
process.env.FRAMEFORGE_SHOW_CAPTIONS === '1'
|
|
562
|
+
);
|
|
521
563
|
const aestheticKey = detectAesthetic({
|
|
522
564
|
topic,
|
|
523
565
|
style,
|
|
@@ -585,6 +627,7 @@ function generateSceneHtml({
|
|
|
585
627
|
<canvas id="stage" width="${width}" height="${height}"></canvas>
|
|
586
628
|
<script>
|
|
587
629
|
window.DURATION = ${clampedDur};
|
|
630
|
+
window.CAMERA_HINT = ${JSON.stringify(camera || cameraHint || 'auto')};
|
|
588
631
|
const canvas = document.getElementById('stage');
|
|
589
632
|
const ctx = canvas.getContext('2d');
|
|
590
633
|
const W = ${width};
|
|
@@ -627,6 +670,7 @@ function spring(t, delay = 0, tension = ${aesthetic.springTension}, friction = $
|
|
|
627
670
|
return 1 - Math.exp(-omega * dt);
|
|
628
671
|
}
|
|
629
672
|
|
|
673
|
+
const SHOW_CAPTIONS = ${JSON.stringify(captionsWanted)};
|
|
630
674
|
const AESTHETIC = ${JSON.stringify(aestheticKey)};
|
|
631
675
|
const BG_TYPE = ${JSON.stringify(aesthetic.backgroundType)};
|
|
632
676
|
const COMP_THEME = ${JSON.stringify(compTheme)};
|
|
@@ -644,6 +688,10 @@ const FONT_DISPLAY = ${JSON.stringify(fontDisplay)};
|
|
|
644
688
|
const FONT_BODY = ${JSON.stringify(fontBody)};
|
|
645
689
|
const FONT_WEIGHT = ${JSON.stringify(aesthetic.fontDisplayWeight)};
|
|
646
690
|
const IMAGE_SRC = ${JSON.stringify(imageDataUri)};
|
|
691
|
+
const IS_LIGHT = ${Boolean(pal.isLight)};
|
|
692
|
+
const STROKE_COLOR = ${JSON.stringify(pal.isLight ? (pal.stroke || '#1c1917') : '#f8fafc')};
|
|
693
|
+
const STROKE_ALT = ${JSON.stringify(pal.isLight ? (pal.strokeAlt || '#44403c') : 'rgba(255, 255, 255, 0.7)')};
|
|
694
|
+
const AMBIENT_LINE = ${JSON.stringify(pal.isLight ? 'rgba(28, 25, 23, 0.22)' : 'rgba(255, 255, 255, 0.25)')};
|
|
647
695
|
|
|
648
696
|
let cachedImage = null;
|
|
649
697
|
if (IMAGE_SRC) {
|
|
@@ -663,7 +711,24 @@ function drawBackground(t) {
|
|
|
663
711
|
ctx.fillRect(0, 0, W, H);
|
|
664
712
|
|
|
665
713
|
ctx.save();
|
|
666
|
-
if (BG_TYPE === '
|
|
714
|
+
if (BG_TYPE === 'paper_texture' || IS_LIGHT) {
|
|
715
|
+
// Subtle paper grain / parchment texture
|
|
716
|
+
ctx.strokeStyle = 'rgba(120, 80, 40, 0.04)';
|
|
717
|
+
ctx.lineWidth = 1;
|
|
718
|
+
for (let y = 40; y < H; y += 40) {
|
|
719
|
+
const wobble = Math.sin(y * 0.1) * 3;
|
|
720
|
+
ctx.beginPath();
|
|
721
|
+
ctx.moveTo(0, y + wobble);
|
|
722
|
+
ctx.lineTo(W, y - wobble);
|
|
723
|
+
ctx.stroke();
|
|
724
|
+
}
|
|
725
|
+
// Subtle organic vignette
|
|
726
|
+
const vig = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.45, W / 2, H / 2, Math.max(W, H) * 0.75);
|
|
727
|
+
vig.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
|
728
|
+
vig.addColorStop(1, 'rgba(120, 90, 60, 0.08)');
|
|
729
|
+
ctx.fillStyle = vig;
|
|
730
|
+
ctx.fillRect(0, 0, W, H);
|
|
731
|
+
} else if (BG_TYPE === 'grid_perspective') {
|
|
667
732
|
// 3D Perspective Grid
|
|
668
733
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
|
|
669
734
|
ctx.lineWidth = 1;
|
|
@@ -740,13 +805,61 @@ function drawBackground(t) {
|
|
|
740
805
|
// ── Full-Canvas Cinematic Visual Directors ─────────────────────────────────────
|
|
741
806
|
function drawMainScene(t) {
|
|
742
807
|
ctx.save();
|
|
743
|
-
|
|
808
|
+
// Continuous audio-driven cinematic camera movement (no fixed interval timer cuts)
|
|
809
|
+
const dur = window.DURATION || 3;
|
|
810
|
+
const p = Math.min(1, Math.max(0, t / dur));
|
|
744
811
|
const enterSpring = spring(t, 0.05, 170, 13);
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
812
|
+
const camHint = (window.CAMERA_HINT || 'auto').toLowerCase();
|
|
813
|
+
|
|
814
|
+
let targetZoom = 1.0;
|
|
815
|
+
let targetPanX = 0;
|
|
816
|
+
let targetPanY = 0;
|
|
817
|
+
|
|
818
|
+
if (camHint === 'slow_zoom_in' || camHint === 'zoom_in') {
|
|
819
|
+
targetZoom = 1.0 + p * 0.08;
|
|
820
|
+
targetPanX = Math.sin(t * 0.4) * 6;
|
|
821
|
+
targetPanY = Math.cos(t * 0.3) * 4;
|
|
822
|
+
} else if (camHint === 'slow_zoom_out' || camHint === 'zoom_out') {
|
|
823
|
+
targetZoom = 1.08 - p * 0.08;
|
|
824
|
+
targetPanX = Math.sin(t * 0.4) * 6;
|
|
825
|
+
targetPanY = Math.cos(t * 0.3) * 4;
|
|
826
|
+
} else if (camHint === 'pan_left') {
|
|
827
|
+
targetZoom = 1.02;
|
|
828
|
+
targetPanX = 25 - p * 50;
|
|
829
|
+
targetPanY = Math.cos(t * 0.3) * 3;
|
|
830
|
+
} else if (camHint === 'pan_right') {
|
|
831
|
+
targetZoom = 1.02;
|
|
832
|
+
targetPanX = -25 + p * 50;
|
|
833
|
+
targetPanY = Math.cos(t * 0.3) * 3;
|
|
834
|
+
} else if (camHint === 'tilt_up') {
|
|
835
|
+
targetZoom = 1.03;
|
|
836
|
+
targetPanX = Math.sin(t * 0.3) * 4;
|
|
837
|
+
targetPanY = 20 - p * 40;
|
|
838
|
+
} else if (camHint === 'tilt_down') {
|
|
839
|
+
targetZoom = 1.03;
|
|
840
|
+
targetPanX = Math.sin(t * 0.3) * 4;
|
|
841
|
+
targetPanY = -20 + p * 40;
|
|
842
|
+
} else if (camHint === 'ken_burns') {
|
|
843
|
+
targetZoom = 1.0 + p * 0.06;
|
|
844
|
+
targetPanX = -18 + p * 36;
|
|
845
|
+
targetPanY = 12 - p * 24;
|
|
846
|
+
} else if (camHint === 'static') {
|
|
847
|
+
targetZoom = 1.0;
|
|
848
|
+
targetPanX = 0;
|
|
849
|
+
targetPanY = 0;
|
|
850
|
+
} else {
|
|
851
|
+
// 'auto' or narrative default: subtle organic breathing and smooth forward progression
|
|
852
|
+
targetZoom = 1.0 + Math.sin(p * Math.PI * 0.5) * 0.04;
|
|
853
|
+
targetPanX = Math.sin(t * 0.35) * 8;
|
|
854
|
+
targetPanY = Math.cos(t * 0.28) * 4;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const camZoom = targetZoom;
|
|
858
|
+
const camPanX = targetPanX;
|
|
859
|
+
const camPanY = targetPanY;
|
|
860
|
+
|
|
748
861
|
ctx.translate(W / 2 + camPanX, H * 0.50 + camPanY);
|
|
749
|
-
ctx.scale(Math.min(1.
|
|
862
|
+
ctx.scale(Math.min(1.02, Math.max(0.92, enterSpring)) * camZoom, Math.min(1.02, Math.max(0.92, enterSpring)) * camZoom);
|
|
750
863
|
ctx.translate(-W / 2, -H * 0.50);
|
|
751
864
|
|
|
752
865
|
const cx = W / 2;
|
|
@@ -758,7 +871,7 @@ function drawMainScene(t) {
|
|
|
758
871
|
return Math.sin(seed * 19.37 + step * 2.41) * mag;
|
|
759
872
|
}
|
|
760
873
|
|
|
761
|
-
function sLine(x1, y1, x2, y2, seed, color =
|
|
874
|
+
function sLine(x1, y1, x2, y2, seed, color = STROKE_COLOR, width = 3.5) {
|
|
762
875
|
ctx.save();
|
|
763
876
|
ctx.strokeStyle = color;
|
|
764
877
|
ctx.lineWidth = width;
|
|
@@ -773,7 +886,7 @@ function drawMainScene(t) {
|
|
|
773
886
|
ctx.restore();
|
|
774
887
|
}
|
|
775
888
|
|
|
776
|
-
function sCircle(x, y, r, seed, color =
|
|
889
|
+
function sCircle(x, y, r, seed, color = STROKE_COLOR, width = 3, fillColor = null) {
|
|
777
890
|
ctx.save();
|
|
778
891
|
if (fillColor) {
|
|
779
892
|
ctx.fillStyle = fillColor;
|
|
@@ -798,7 +911,7 @@ function drawMainScene(t) {
|
|
|
798
911
|
ctx.restore();
|
|
799
912
|
}
|
|
800
913
|
|
|
801
|
-
function sRect(x, y, w, h, seed, color =
|
|
914
|
+
function sRect(x, y, w, h, seed, color = STROKE_COLOR, width = 3, fillColor = null) {
|
|
802
915
|
ctx.save();
|
|
803
916
|
if (fillColor) {
|
|
804
917
|
ctx.fillStyle = fillColor;
|
|
@@ -822,69 +935,107 @@ function drawMainScene(t) {
|
|
|
822
935
|
expression = 'talking',
|
|
823
936
|
clothing = 'none',
|
|
824
937
|
armPose = 'natural',
|
|
938
|
+
showShadow = false,
|
|
939
|
+
showBlush = false,
|
|
940
|
+
showSpecular = false,
|
|
825
941
|
seed = 100,
|
|
826
942
|
} = {}) {
|
|
827
943
|
ctx.save();
|
|
828
944
|
ctx.translate(x, y);
|
|
829
945
|
ctx.scale(scale, scale);
|
|
830
946
|
|
|
947
|
+
// Ground contact shadow: strictly conditional (default false)
|
|
948
|
+
if (showShadow) {
|
|
949
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.28)';
|
|
950
|
+
ctx.beginPath();
|
|
951
|
+
ctx.ellipse(0, legLength, 34, 7, 0, 0, Math.PI * 2);
|
|
952
|
+
ctx.fill();
|
|
953
|
+
}
|
|
954
|
+
|
|
831
955
|
const blink = (t % 3.2 < 0.14) ? 0.15 : 1.0;
|
|
832
956
|
const mouthTalk = Math.abs(Math.sin(t * 14)) * 4;
|
|
833
957
|
const torsoTopY = -torsoHeight;
|
|
834
958
|
const torsoBottomY = 0;
|
|
959
|
+
const shoulderW = 28;
|
|
835
960
|
|
|
836
|
-
//
|
|
961
|
+
// High quality contoured torso silhouette (organic human form, never stick figure)
|
|
837
962
|
if (clothing === 'pelt') {
|
|
963
|
+
// Prehistoric animal pelt tunic with textured ragged hem
|
|
838
964
|
ctx.fillStyle = '#b45309';
|
|
839
965
|
ctx.beginPath();
|
|
840
|
-
ctx.moveTo(-
|
|
841
|
-
ctx.lineTo(
|
|
842
|
-
ctx.lineTo(
|
|
843
|
-
ctx.lineTo(-
|
|
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);
|
|
844
970
|
ctx.closePath();
|
|
845
971
|
ctx.fill();
|
|
846
|
-
|
|
972
|
+
|
|
973
|
+
// Pelt border stitching & waist wrap
|
|
974
|
+
sLine(-shoulderW - 6, torsoBottomY + 22, shoulderW + 6, torsoBottomY + 22, seed + 4, '#78350f', 4);
|
|
975
|
+
sLine(-shoulderW, torsoBottomY - 8, shoulderW, torsoBottomY - 8, seed + 5, '#451a03', 3);
|
|
976
|
+
// Asymmetric fur shoulder wrap
|
|
977
|
+
sLine(-shoulderW, torsoTopY, shoulderW - 10, torsoTopY + 24, seed + 6, '#78350f', 3.5);
|
|
847
978
|
} else if (clothing === 'shirt') {
|
|
979
|
+
// Modern tailored shirt/jacket with collar and button placket
|
|
848
980
|
ctx.fillStyle = ACCENT;
|
|
849
981
|
ctx.beginPath();
|
|
850
|
-
ctx.moveTo(-
|
|
851
|
-
ctx.lineTo(
|
|
852
|
-
ctx.lineTo(
|
|
853
|
-
ctx.lineTo(-
|
|
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);
|
|
854
986
|
ctx.closePath();
|
|
855
987
|
ctx.fill();
|
|
988
|
+
|
|
989
|
+
// Collar lines & placket
|
|
856
990
|
sLine(-14, torsoTopY, 0, torsoTopY + 18, seed + 4, '#ffffff', 3);
|
|
857
991
|
sLine(14, torsoTopY, 0, torsoTopY + 18, seed + 5, '#ffffff', 3);
|
|
858
992
|
sLine(0, torsoTopY + 18, 0, torsoBottomY + 10, seed + 6, '#ffffff', 2.5);
|
|
859
993
|
} else if (clothing === 'cloak') {
|
|
994
|
+
// Flowing draped mantle
|
|
860
995
|
ctx.fillStyle = '#0f766e';
|
|
861
996
|
ctx.beginPath();
|
|
862
|
-
ctx.moveTo(-
|
|
863
|
-
ctx.lineTo(
|
|
864
|
-
ctx.lineTo(
|
|
865
|
-
ctx.lineTo(-
|
|
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);
|
|
866
1001
|
ctx.closePath();
|
|
867
1002
|
ctx.fill();
|
|
1003
|
+
sLine(-shoulderW - 8, torsoTopY - 6, -shoulderW - 18, torsoBottomY + 32, seed + 7, '#134e4a', 3.5);
|
|
1004
|
+
sLine(shoulderW + 8, torsoTopY - 6, shoulderW + 18, torsoBottomY + 32, seed + 8, '#134e4a', 3.5);
|
|
868
1005
|
} else {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1006
|
+
// Natural contoured human torso (shoulders, ribcage, waist, hips)
|
|
1007
|
+
ctx.fillStyle = IS_LIGHT ? 'rgba(217, 119, 6, 0.12)' : 'rgba(254, 243, 199, 0.22)';
|
|
1008
|
+
ctx.beginPath();
|
|
1009
|
+
ctx.moveTo(-shoulderW + wb(seed, 1.5), torsoTopY + 5);
|
|
1010
|
+
ctx.lineTo(shoulderW + wb(seed + 1, 1.5), torsoTopY + 5);
|
|
1011
|
+
ctx.quadraticCurveTo(shoulderW - 6 + wb(seed + 2, 1.5), torsoTopY + torsoHeight * 0.5, shoulderW - 4, torsoBottomY);
|
|
1012
|
+
ctx.lineTo(-shoulderW + 4, torsoBottomY);
|
|
1013
|
+
ctx.quadraticCurveTo(-shoulderW + 6 + wb(seed + 3, 1.5), torsoTopY + torsoHeight * 0.5, -shoulderW + wb(seed, 1.5), torsoTopY + 5);
|
|
1014
|
+
ctx.closePath();
|
|
1015
|
+
ctx.fill();
|
|
1016
|
+
|
|
1017
|
+
// Contoured torso outline strokes
|
|
1018
|
+
sLine(-shoulderW, torsoTopY + 5, -shoulderW + 4, torsoBottomY, seed + 7, STROKE_COLOR, 3.5);
|
|
1019
|
+
sLine(shoulderW, torsoTopY + 5, shoulderW - 4, torsoBottomY, seed + 8, STROKE_COLOR, 3.5);
|
|
1020
|
+
sLine(-shoulderW + 4, torsoBottomY, shoulderW - 4, torsoBottomY, seed + 9, STROKE_COLOR, 3);
|
|
1021
|
+
// Subtle collarbone / rib accents
|
|
1022
|
+
sLine(-16, torsoTopY + 16, -2, torsoTopY + 22, seed + 10, STROKE_ALT, 2);
|
|
1023
|
+
sLine(16, torsoTopY + 16, 2, torsoTopY + 22, seed + 11, STROKE_ALT, 2);
|
|
872
1024
|
}
|
|
873
|
-
sLine(0, torsoTopY, 0, torsoBottomY, seed + 10, '#f8fafc', 4);
|
|
874
1025
|
|
|
875
|
-
// Head
|
|
876
|
-
const headCenterY = torsoTopY - headRadius - 6 + wb(seed +
|
|
877
|
-
sCircle(0, headCenterY, headRadius, seed +
|
|
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');
|
|
878
1029
|
|
|
879
1030
|
// Hair
|
|
880
1031
|
if (hairStyle === 'wild') {
|
|
881
|
-
for (let h = 0; h <
|
|
882
|
-
const hairAng = -Math.PI * 0.
|
|
1032
|
+
for (let h = 0; h < 7; h++) {
|
|
1033
|
+
const hairAng = -Math.PI * 0.88 + (h / 6) * Math.PI * 0.76;
|
|
883
1034
|
const hx1 = Math.cos(hairAng) * headRadius;
|
|
884
1035
|
const hy1 = headCenterY + Math.sin(hairAng) * headRadius;
|
|
885
|
-
const hx2 = Math.cos(hairAng) * (headRadius +
|
|
886
|
-
const hy2 = headCenterY + Math.sin(hairAng) * (headRadius +
|
|
887
|
-
sLine(hx1, hy1, hx2, hy2, seed +
|
|
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);
|
|
888
1039
|
}
|
|
889
1040
|
} else if (hairStyle === 'modern') {
|
|
890
1041
|
ctx.fillStyle = '#1e293b';
|
|
@@ -900,10 +1051,28 @@ function drawMainScene(t) {
|
|
|
900
1051
|
|
|
901
1052
|
// Eyes
|
|
902
1053
|
const eyeY = headCenterY + 4;
|
|
903
|
-
const eyeR = 4 * blink;
|
|
1054
|
+
const eyeR = 4.2 * blink;
|
|
904
1055
|
sCircle(-12, eyeY, Math.max(1, eyeR), seed + 23, '#0f172a', 2, '#0f172a');
|
|
905
1056
|
sCircle(12, eyeY, Math.max(1, eyeR), seed + 24, '#0f172a', 2, '#0f172a');
|
|
906
1057
|
|
|
1058
|
+
// Specular eye highlights: only when explicitly enabled
|
|
1059
|
+
if (showSpecular && blink > 0.4) {
|
|
1060
|
+
ctx.fillStyle = '#ffffff';
|
|
1061
|
+
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);
|
|
1064
|
+
ctx.fill();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Rosy cheek blush: strictly conditional (default false)
|
|
1068
|
+
if (showBlush || expression === 'blushing' || expression === 'shame') {
|
|
1069
|
+
ctx.fillStyle = 'rgba(251, 113, 133, 0.28)';
|
|
1070
|
+
ctx.beginPath();
|
|
1071
|
+
ctx.ellipse(-14, headCenterY + 12, 6, 3.5, 0, 0, Math.PI * 2);
|
|
1072
|
+
ctx.ellipse(14, headCenterY + 12, 6, 3.5, 0, 0, Math.PI * 2);
|
|
1073
|
+
ctx.fill();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
907
1076
|
// Eyebrows
|
|
908
1077
|
if (expression === 'shivering') {
|
|
909
1078
|
sLine(-18, eyeY - 12 + wb(seed, 2), -6, eyeY - 7 + wb(seed, 2), seed + 25, '#0f172a', 2.5);
|
|
@@ -938,93 +1107,102 @@ function drawMainScene(t) {
|
|
|
938
1107
|
sLine(-8, mouthY, 8, mouthY, seed + 34, '#0f172a', 2.5);
|
|
939
1108
|
}
|
|
940
1109
|
|
|
941
|
-
// Legs
|
|
1110
|
+
// Legs with articulated knees and planted feet
|
|
942
1111
|
const hipY = torsoBottomY;
|
|
943
1112
|
if (armPose === 'sewing' || armPose === 'thinker') {
|
|
944
|
-
|
|
945
|
-
sLine(-
|
|
946
|
-
sLine(
|
|
947
|
-
sLine(
|
|
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);
|
|
948
1118
|
} else if (armPose === 'hold_spear') {
|
|
1119
|
+
// Dynamic walking gait
|
|
949
1120
|
const legSwing = Math.sin(t * 5.5);
|
|
950
1121
|
const l1KneeX = -15 + legSwing * 22;
|
|
951
1122
|
const l1KneeY = hipY + legLength * 0.55;
|
|
952
1123
|
const l1FootX = -15 + legSwing * 38;
|
|
953
1124
|
const l1FootY = hipY + legLength;
|
|
954
|
-
sLine(-
|
|
955
|
-
sLine(l1KneeX, l1KneeY, l1FootX, l1FootY, seed + 40,
|
|
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);
|
|
956
1128
|
|
|
957
1129
|
const l2KneeX = 15 - legSwing * 22;
|
|
958
1130
|
const l2KneeY = hipY + legLength * 0.55;
|
|
959
1131
|
const l2FootX = 15 - legSwing * 38;
|
|
960
1132
|
const l2FootY = hipY + legLength;
|
|
961
|
-
sLine(
|
|
962
|
-
sLine(l2KneeX, l2KneeY, l2FootX, l2FootY, seed +
|
|
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);
|
|
963
1136
|
} else {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1137
|
+
// Standing legs with natural knee contour and planted feet
|
|
1138
|
+
const lKneeX = -15 + wb(seed + 39, 1);
|
|
1139
|
+
const rKneeX = 15 + wb(seed + 40, 1);
|
|
1140
|
+
const kneeY = hipY + legLength * 0.52;
|
|
1141
|
+
const footY = hipY + legLength;
|
|
1142
|
+
sLine(-16, hipY, lKneeX, kneeY, seed + 41, STROKE_COLOR, 4);
|
|
1143
|
+
sLine(lKneeX, kneeY, -16, footY, seed + 42, STROKE_COLOR, 4);
|
|
1144
|
+
sLine(-16, footY, -28, footY, seed + 43, STROKE_COLOR, 3.5);
|
|
1145
|
+
|
|
1146
|
+
sLine(16, hipY, rKneeX, kneeY, seed + 44, STROKE_COLOR, 4);
|
|
1147
|
+
sLine(rKneeX, kneeY, 16, footY, seed + 45, STROKE_COLOR, 4);
|
|
1148
|
+
sLine(16, footY, 28, footY, seed + 46, STROKE_COLOR, 3.5);
|
|
968
1149
|
}
|
|
969
1150
|
|
|
970
|
-
// Arms & Hands
|
|
1151
|
+
// Arms & Hands with natural articulated elbows and gestures
|
|
971
1152
|
const shoulderY = torsoTopY + 12;
|
|
972
1153
|
if (armPose === 'shiver') {
|
|
973
1154
|
const shivA = Math.sin(t * 34) * 3;
|
|
974
|
-
sLine(-
|
|
975
|
-
sLine(
|
|
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);
|
|
976
1157
|
} else if (armPose === 'hold_spear') {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
sLine(-
|
|
980
|
-
sLine(
|
|
981
|
-
|
|
1158
|
+
// Holding spear with grip
|
|
1159
|
+
const spearSway = Math.sin(t * 5.5) * 6;
|
|
1160
|
+
sLine(-shoulderW, shoulderY, -10, shoulderY + 28, seed + 49, STROKE_COLOR, 4);
|
|
1161
|
+
sLine(-10, shoulderY + 28, 12, shoulderY + 14, seed + 50, STROKE_COLOR, 4);
|
|
1162
|
+
// Spear pole passing through hands
|
|
1163
|
+
const spearX = 18 + spearSway;
|
|
1164
|
+
sLine(spearX - 25, cy - 140, spearX + 35, cy + 180, seed + 51, '#78350f', 5.5);
|
|
1165
|
+
// Flint spearhead bound with sinew
|
|
1166
|
+
ctx.fillStyle = '#94a3b8';
|
|
982
1167
|
ctx.beginPath();
|
|
983
|
-
ctx.moveTo(spearX
|
|
984
|
-
ctx.lineTo(spearX -
|
|
985
|
-
ctx.lineTo(spearX
|
|
1168
|
+
ctx.moveTo(spearX - 25, cy - 140);
|
|
1169
|
+
ctx.lineTo(spearX - 32, cy - 180);
|
|
1170
|
+
ctx.lineTo(spearX - 18, cy - 170);
|
|
986
1171
|
ctx.closePath();
|
|
987
1172
|
ctx.fill();
|
|
988
|
-
sLine(
|
|
989
|
-
sLine(35, shoulderY + 25 + Math.sin(t * 5.5) * 20, 48, shoulderY + 45, seed + 53, '#f8fafc', 4);
|
|
1173
|
+
sLine(spearX - 28, cy - 140, spearX - 20, cy - 150, seed + 52, '#f59e0b', 3);
|
|
990
1174
|
} else if (armPose === 'sewing') {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
sLine(
|
|
997
|
-
sLine(38, shoulderY + 25, handX, handY, seed + 57, '#f8fafc', 4);
|
|
998
|
-
sLine(handX - 5, handY - 10, handX + 18, handY + 12, seed + 58, '#fef08a', 3.5);
|
|
999
|
-
ctx.strokeStyle = '#ffffff';
|
|
1000
|
-
ctx.lineWidth = 1.5;
|
|
1001
|
-
ctx.beginPath();
|
|
1002
|
-
ctx.moveTo(handX - 5, handY - 10);
|
|
1003
|
-
ctx.quadraticCurveTo(handX - 25, handY - 25, 0, hipY + 10);
|
|
1004
|
-
ctx.stroke();
|
|
1175
|
+
// Left hand holds stretched pelt, right hand drives bone needle with sinew
|
|
1176
|
+
const sewPull = Math.sin(t * 4) * 14;
|
|
1177
|
+
sLine(-shoulderW, shoulderY, -40, shoulderY + 38, seed + 53, STROKE_COLOR, 4);
|
|
1178
|
+
sLine(-40, shoulderY + 38, -15, shoulderY + 45, seed + 54, STROKE_COLOR, 4);
|
|
1179
|
+
sLine(shoulderW, shoulderY, 35 + sewPull * 0.5, shoulderY + 28, seed + 55, STROKE_COLOR, 4);
|
|
1180
|
+
sLine(35 + sewPull * 0.5, shoulderY + 28, 10 + sewPull, shoulderY + 42, seed + 56, STROKE_COLOR, 4);
|
|
1005
1181
|
} else if (armPose === 'cover_body') {
|
|
1006
|
-
|
|
1007
|
-
sLine(
|
|
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);
|
|
1008
1190
|
} else if (armPose === 'dress_shirt') {
|
|
1009
|
-
|
|
1010
|
-
sLine(-
|
|
1011
|
-
|
|
1012
|
-
sLine(18, shoulderY, 35, shoulderY + 15, seed + 63, '#f8fafc', 4);
|
|
1013
|
-
sLine(35, shoulderY + 15, 10, adjustY, seed + 64, '#f8fafc', 4);
|
|
1191
|
+
// Examining clothes, touching button / adjusting collar
|
|
1192
|
+
sLine(-shoulderW, shoulderY, -14, shoulderY + 22, seed + 62, STROKE_COLOR, 4);
|
|
1193
|
+
sLine(shoulderW, shoulderY, 14, shoulderY + 22, seed + 63, STROKE_COLOR, 4);
|
|
1014
1194
|
} else if (armPose === 'peace_gesture') {
|
|
1015
|
-
|
|
1016
|
-
sLine(
|
|
1017
|
-
sLine(
|
|
1018
|
-
|
|
1019
|
-
sLine(18, shoulderY, 36, hipY + 15, seed + 68, '#f8fafc', 4);
|
|
1020
|
-
sLine(36, hipY + 15, 8, headCenterY + 18, seed + 69, '#f8fafc', 4);
|
|
1021
|
-
sLine(-18, shoulderY, -30, hipY + 25, seed + 70, '#f8fafc', 4);
|
|
1195
|
+
// Open welcoming hand
|
|
1196
|
+
sLine(-shoulderW, shoulderY, -35, shoulderY + 30, seed + 64, STROKE_COLOR, 4);
|
|
1197
|
+
sLine(shoulderW, shoulderY, 35, shoulderY + 15, seed + 65, STROKE_COLOR, 4);
|
|
1198
|
+
sLine(35, shoulderY + 15, 52, shoulderY + 2, seed + 66, STROKE_COLOR, 3.5);
|
|
1022
1199
|
} else {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
sLine(-
|
|
1026
|
-
sLine(
|
|
1027
|
-
sLine(
|
|
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);
|
|
1028
1206
|
}
|
|
1029
1207
|
|
|
1030
1208
|
ctx.restore();
|
|
@@ -1036,7 +1214,7 @@ function drawMainScene(t) {
|
|
|
1036
1214
|
const beat2 = spring(t, window.DURATION * 0.35, 180, 16);
|
|
1037
1215
|
const beat3 = spring(t, window.DURATION * 0.70, 190, 16);
|
|
1038
1216
|
|
|
1039
|
-
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10,
|
|
1217
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2.5);
|
|
1040
1218
|
|
|
1041
1219
|
// Clothes rack on left (sways in beat 1)
|
|
1042
1220
|
sLine(cx - 360, cy + 220, cx - 360, cy - 140, 11, '#94a3b8', 5);
|
|
@@ -1100,44 +1278,120 @@ function drawMainScene(t) {
|
|
|
1100
1278
|
seed: 101,
|
|
1101
1279
|
});
|
|
1102
1280
|
|
|
1103
|
-
} else if (COMP_THEME === '
|
|
1104
|
-
//
|
|
1105
|
-
const
|
|
1106
|
-
const
|
|
1281
|
+
} else if (COMP_THEME === 'doodle_modern_vs_prehistoric') {
|
|
1282
|
+
// Split-screen comparison: Modern stylish human on left vs Prehistoric human on right
|
|
1283
|
+
const dur = window.DURATION || 3;
|
|
1284
|
+
const bridgeSpring = spring(t, dur * 0.35, 170, 15);
|
|
1285
|
+
|
|
1286
|
+
// Natural ground line
|
|
1287
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2.5);
|
|
1288
|
+
|
|
1289
|
+
// Subtle split-screen center divider
|
|
1290
|
+
ctx.save();
|
|
1291
|
+
ctx.setLineDash([10, 10]);
|
|
1292
|
+
sLine(cx, cy - 220, cx, cy + 200, 20, AMBIENT_LINE, 2);
|
|
1293
|
+
ctx.restore();
|
|
1294
|
+
|
|
1295
|
+
// Modern human on left
|
|
1296
|
+
drawDoodleCharacter({
|
|
1297
|
+
x: cx - 260,
|
|
1298
|
+
y: cy + 140,
|
|
1299
|
+
scale: 1.3,
|
|
1300
|
+
hairStyle: 'modern',
|
|
1301
|
+
expression: 'talking',
|
|
1302
|
+
clothing: 'shirt',
|
|
1303
|
+
armPose: 'dress_shirt',
|
|
1304
|
+
seed: 120,
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
// Prehistoric human on right
|
|
1308
|
+
drawDoodleCharacter({
|
|
1309
|
+
x: cx + 260,
|
|
1310
|
+
y: cy + 140,
|
|
1311
|
+
scale: 1.3,
|
|
1312
|
+
hairStyle: 'wild',
|
|
1313
|
+
expression: 'curious',
|
|
1314
|
+
clothing: 'none',
|
|
1315
|
+
armPose: 'natural',
|
|
1316
|
+
seed: 121,
|
|
1317
|
+
});
|
|
1107
1318
|
|
|
1108
|
-
|
|
1319
|
+
// Dynamic timeline bridge / evolution arc connecting them
|
|
1320
|
+
if (bridgeSpring > 0.05) {
|
|
1321
|
+
ctx.save();
|
|
1322
|
+
ctx.globalAlpha = Math.min(0.85, bridgeSpring * 0.85);
|
|
1323
|
+
ctx.strokeStyle = ACCENT;
|
|
1324
|
+
ctx.lineWidth = 3;
|
|
1325
|
+
ctx.setLineDash([8, 8]);
|
|
1326
|
+
ctx.beginPath();
|
|
1327
|
+
ctx.moveTo(cx - 200, cy - 30);
|
|
1328
|
+
ctx.quadraticCurveTo(cx, cy - 140, cx + 200, cy - 30);
|
|
1329
|
+
ctx.stroke();
|
|
1330
|
+
ctx.setLineDash([]);
|
|
1331
|
+
|
|
1332
|
+
// Center compass / timeline knot
|
|
1333
|
+
sCircle(cx, cy - 85, 24, 820, ACCENT, 3, IS_LIGHT ? '#ffffff' : 'rgba(15, 23, 42, 0.9)');
|
|
1334
|
+
sCircle(cx, cy - 85, 10, 821, STROKE_COLOR, 2, STROKE_COLOR);
|
|
1335
|
+
ctx.restore();
|
|
1336
|
+
}
|
|
1109
1337
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1338
|
+
} else if (COMP_THEME === 'doodle_shiver_cold') {
|
|
1339
|
+
// Narrative beat progression based strictly on audio duration (window.DURATION)
|
|
1340
|
+
const dur = window.DURATION || 3;
|
|
1341
|
+
const beat2 = spring(t, dur * 0.45, 160, 15);
|
|
1342
|
+
|
|
1343
|
+
// Natural ground line
|
|
1344
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
|
|
1345
|
+
|
|
1346
|
+
// Freezing wind streaks (rush during cold beat, calm as sun breaks)
|
|
1347
|
+
const windAlpha = Math.max(0, 1 - beat2 * 0.85);
|
|
1348
|
+
if (windAlpha > 0.05) {
|
|
1349
|
+
for (let w = 0; w < 14; w++) {
|
|
1350
|
+
const windX = ((t * 750 + w * 210) % (W + 600)) - 300;
|
|
1351
|
+
const windY = cy - 250 + (w * 42) + Math.sin(t * 3.5 + w) * 15;
|
|
1352
|
+
const windColor = (IS_LIGHT ? 'rgba(100,116,139,' : 'rgba(255,255,255,') + (0.35 * windAlpha) + ')';
|
|
1353
|
+
sLine(windX, windY, windX + 170, windY + 5, 200 + w, windColor, 2);
|
|
1354
|
+
}
|
|
1115
1355
|
}
|
|
1116
1356
|
|
|
1117
1357
|
// Drifting snow particles
|
|
1118
|
-
for (let sp = 0; sp <
|
|
1119
|
-
const px = ((sp * 85 + t *
|
|
1120
|
-
const py = (sp * 45 + t *
|
|
1121
|
-
sCircle(px, py, 2.
|
|
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');
|
|
1122
1362
|
}
|
|
1123
1363
|
|
|
1124
|
-
// Shivering breath vapor cloud (calms in Beat
|
|
1125
|
-
if (
|
|
1364
|
+
// Shivering breath vapor cloud (calms in Beat 2)
|
|
1365
|
+
if (beat2 < 0.7) {
|
|
1126
1366
|
const breathPhase = (t * 2.2) % 1;
|
|
1127
1367
|
const bCloudX = cx - 50 + breathPhase * 60;
|
|
1128
1368
|
const bCloudY = cy - 90 - breathPhase * 25;
|
|
1129
|
-
ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.
|
|
1369
|
+
ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.45) + ')';
|
|
1130
1370
|
ctx.beginPath();
|
|
1131
1371
|
ctx.arc(bCloudX, bCloudY, 15 + breathPhase * 25, 0, Math.PI * 2);
|
|
1132
1372
|
ctx.fill();
|
|
1133
1373
|
}
|
|
1134
1374
|
|
|
1135
|
-
//
|
|
1136
|
-
|
|
1375
|
+
// Warm golden sunlight breaks in (visual myth-busting revelation)
|
|
1376
|
+
if (beat2 > 0.05) {
|
|
1377
|
+
ctx.save();
|
|
1378
|
+
ctx.globalAlpha = Math.min(0.6, beat2 * 0.6);
|
|
1379
|
+
const sunBeamGrad = ctx.createRadialGradient(cx + 180, cy - 220, 20, cx + 180, cy - 100, 320);
|
|
1380
|
+
sunBeamGrad.addColorStop(0, 'rgba(251, 191, 36, 0.4)');
|
|
1381
|
+
sunBeamGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
|
|
1382
|
+
ctx.fillStyle = sunBeamGrad;
|
|
1383
|
+
ctx.beginPath();
|
|
1384
|
+
ctx.arc(cx + 180, cy - 180, 260, 0, Math.PI * 2);
|
|
1385
|
+
ctx.fill();
|
|
1386
|
+
ctx.restore();
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Main character: shivers in cold beat, then pauses and looks at arms in wonder as sun illuminates
|
|
1390
|
+
const isShivering = beat2 < 0.6;
|
|
1137
1391
|
const vibX = isShivering ? Math.sin(t * 38) * 4.5 : 0;
|
|
1138
1392
|
const vibY = isShivering ? Math.cos(t * 32) * 2 : 0;
|
|
1139
1393
|
drawDoodleCharacter({
|
|
1140
|
-
x: cx -
|
|
1394
|
+
x: cx - 100 + beat2 * 80 + vibX,
|
|
1141
1395
|
y: cy + 140 + vibY,
|
|
1142
1396
|
scale: 1.35,
|
|
1143
1397
|
hairStyle: 'wild',
|
|
@@ -1147,38 +1401,6 @@ function drawMainScene(t) {
|
|
|
1147
1401
|
seed: 102,
|
|
1148
1402
|
});
|
|
1149
1403
|
|
|
1150
|
-
// Stamp 1: NOT FOR COLD (slams down in Beat 2)
|
|
1151
|
-
if (stamp1 > 0.05) {
|
|
1152
|
-
ctx.save();
|
|
1153
|
-
const s1X = cx + 240;
|
|
1154
|
-
const s1Y = cy - 100;
|
|
1155
|
-
ctx.translate(s1X, s1Y);
|
|
1156
|
-
ctx.scale(stamp1, stamp1);
|
|
1157
|
-
sCircle(0, 0, 50, 400, 'rgba(239,68,68,0.9)', 4);
|
|
1158
|
-
sLine(-36, -36, 36, 36, 401, 'rgba(239,68,68,0.95)', 4.5);
|
|
1159
|
-
ctx.font = 'bold 20px monospace';
|
|
1160
|
-
ctx.fillStyle = '#ef4444';
|
|
1161
|
-
ctx.textAlign = 'center';
|
|
1162
|
-
ctx.fillText('NOT FOR COLD', 0, 75);
|
|
1163
|
-
ctx.restore();
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
// Stamp 2: NOT FOR PROTECTION (slams down in Beat 3)
|
|
1167
|
-
if (stamp2 > 0.05) {
|
|
1168
|
-
ctx.save();
|
|
1169
|
-
const s2X = cx + 240;
|
|
1170
|
-
const s2Y = cy + 90;
|
|
1171
|
-
ctx.translate(s2X, s2Y);
|
|
1172
|
-
ctx.scale(stamp2, stamp2);
|
|
1173
|
-
sCircle(0, 0, 50, 410, 'rgba(239,68,68,0.9)', 4);
|
|
1174
|
-
sLine(-36, -36, 36, 36, 411, 'rgba(239,68,68,0.95)', 4.5);
|
|
1175
|
-
ctx.font = 'bold 20px monospace';
|
|
1176
|
-
ctx.fillStyle = '#ef4444';
|
|
1177
|
-
ctx.textAlign = 'center';
|
|
1178
|
-
ctx.fillText('NOT FOR PROTECTION', 0, 75);
|
|
1179
|
-
ctx.restore();
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
1404
|
} else if (COMP_THEME === 'doodle_hunter_savannah') {
|
|
1183
1405
|
// Beat 1: Walking with spear -> Beat 2: Swatting mosquitoes -> Beat 3: Pausing alert
|
|
1184
1406
|
const walkProg = Math.min(1, Math.max(0, t / window.DURATION));
|
|
@@ -1295,15 +1517,12 @@ function drawMainScene(t) {
|
|
|
1295
1517
|
ctx.restore();
|
|
1296
1518
|
}
|
|
1297
1519
|
|
|
1298
|
-
//
|
|
1520
|
+
// Ancient stone strata and bone needle engraving in Beat 3
|
|
1299
1521
|
if (badgeSpring > 0.05) {
|
|
1300
1522
|
ctx.save();
|
|
1301
|
-
ctx.globalAlpha = Math.min(
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
ctx.fillStyle = '#fef08a';
|
|
1305
|
-
ctx.textAlign = 'center';
|
|
1306
|
-
ctx.fillText('BONE NEEDLE • 120,000 YEARS', cx + 270, cy + 167);
|
|
1523
|
+
ctx.globalAlpha = Math.min(0.65, badgeSpring * 0.65);
|
|
1524
|
+
sLine(cx + 140, cy + 150, cx + 420, cy + 150, 670, '#d4af37', 2);
|
|
1525
|
+
sLine(cx + 180, cy + 168, cx + 380, cy + 168, 671, '#d4af37', 1.5);
|
|
1307
1526
|
ctx.restore();
|
|
1308
1527
|
}
|
|
1309
1528
|
|
|
@@ -1378,12 +1597,8 @@ function drawMainScene(t) {
|
|
|
1378
1597
|
ctx.translate(haloX, haloY);
|
|
1379
1598
|
ctx.scale(haloSpring, haloSpring);
|
|
1380
1599
|
sCircle(0, 0, 48, 760, '#38bdf8', 3);
|
|
1381
|
-
sCircle(0, 0,
|
|
1600
|
+
sCircle(0, 0, 24, 761, STROKE_ALT, 2);
|
|
1382
1601
|
sCircle(0, 0, 8, 762, '#38bdf8', 1, '#38bdf8');
|
|
1383
|
-
ctx.font = 'bold 16px monospace';
|
|
1384
|
-
ctx.fillStyle = '#38bdf8';
|
|
1385
|
-
ctx.textAlign = 'center';
|
|
1386
|
-
ctx.fillText('THEORY OF MIND', 0, 72);
|
|
1387
1602
|
ctx.restore();
|
|
1388
1603
|
}
|
|
1389
1604
|
|
|
@@ -1393,7 +1608,7 @@ function drawMainScene(t) {
|
|
|
1393
1608
|
const greet = spring(t, window.DURATION * 0.38, 190, 15);
|
|
1394
1609
|
const arcSpring = spring(t, window.DURATION * 0.68, 200, 15);
|
|
1395
1610
|
|
|
1396
|
-
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10,
|
|
1611
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
|
|
1397
1612
|
|
|
1398
1613
|
// Left Clan Member walks toward center
|
|
1399
1614
|
const leftX = cx - 380 + Math.min(1, approach) * 160;
|
|
@@ -1421,10 +1636,10 @@ function drawMainScene(t) {
|
|
|
1421
1636
|
seed: 107,
|
|
1422
1637
|
});
|
|
1423
1638
|
|
|
1424
|
-
// Glowing connection arc
|
|
1639
|
+
// Glowing connection arc in Beat 3
|
|
1425
1640
|
if (arcSpring > 0.05) {
|
|
1426
1641
|
ctx.save();
|
|
1427
|
-
ctx.globalAlpha = Math.min(
|
|
1642
|
+
ctx.globalAlpha = Math.min(0.85, arcSpring * 0.85);
|
|
1428
1643
|
ctx.strokeStyle = ACCENT;
|
|
1429
1644
|
ctx.lineWidth = 3;
|
|
1430
1645
|
ctx.setLineDash([8, 8]);
|
|
@@ -1434,13 +1649,8 @@ function drawMainScene(t) {
|
|
|
1434
1649
|
ctx.stroke();
|
|
1435
1650
|
ctx.setLineDash([]);
|
|
1436
1651
|
|
|
1437
|
-
sCircle(cx, cy - 65,
|
|
1438
|
-
|
|
1439
|
-
ctx.fillStyle = ACCENT;
|
|
1440
|
-
ctx.textAlign = 'center';
|
|
1441
|
-
ctx.fillText('🤝', cx, cy - 56);
|
|
1442
|
-
ctx.font = 'bold 16px monospace';
|
|
1443
|
-
ctx.fillText('CLOTHING AS SOCIAL LANGUAGE', cx, cy + 15);
|
|
1652
|
+
sCircle(cx, cy - 65, 28, 800, ACCENT, 3, IS_LIGHT ? '#ffffff' : 'rgba(15, 23, 42, 0.85)');
|
|
1653
|
+
sCircle(cx, cy - 65, 12, 801, STROKE_COLOR, 2, STROKE_COLOR);
|
|
1444
1654
|
ctx.restore();
|
|
1445
1655
|
}
|
|
1446
1656
|
|
|
@@ -1460,7 +1670,7 @@ function drawMainScene(t) {
|
|
|
1460
1670
|
ctx.fill();
|
|
1461
1671
|
|
|
1462
1672
|
// Large rock boulder for thinker
|
|
1463
|
-
ctx.fillStyle = '#334155';
|
|
1673
|
+
ctx.fillStyle = IS_LIGHT ? '#78716c' : '#334155';
|
|
1464
1674
|
ctx.beginPath();
|
|
1465
1675
|
ctx.ellipse(cx - 140, cy + 175, 85, 55, 0, 0, Math.PI * 2);
|
|
1466
1676
|
ctx.fill();
|
|
@@ -1485,9 +1695,9 @@ function drawMainScene(t) {
|
|
|
1485
1695
|
ctx.translate(cloudX, cloudY);
|
|
1486
1696
|
ctx.scale(cloudSpring, cloudSpring);
|
|
1487
1697
|
|
|
1488
|
-
sCircle(-180, 160, 12, 850,
|
|
1489
|
-
sCircle(-140, 120, 18, 851,
|
|
1490
|
-
sCircle(-90, 70, 24, 852,
|
|
1698
|
+
sCircle(-180, 160, 12, 850, STROKE_COLOR, 2, '#ffffff');
|
|
1699
|
+
sCircle(-140, 120, 18, 851, STROKE_COLOR, 2, '#ffffff');
|
|
1700
|
+
sCircle(-90, 70, 24, 852, STROKE_COLOR, 2, '#ffffff');
|
|
1491
1701
|
|
|
1492
1702
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
|
1493
1703
|
ctx.beginPath();
|
|
@@ -1496,6 +1706,9 @@ function drawMainScene(t) {
|
|
|
1496
1706
|
ctx.arc(70, 0, 55, 0, Math.PI * 2);
|
|
1497
1707
|
ctx.arc(0, 30, 60, 0, Math.PI * 2);
|
|
1498
1708
|
ctx.fill();
|
|
1709
|
+
ctx.strokeStyle = STROKE_COLOR;
|
|
1710
|
+
ctx.lineWidth = 2.5;
|
|
1711
|
+
ctx.stroke();
|
|
1499
1712
|
|
|
1500
1713
|
// Inside thought cloud: Tomorrow's sunrise & stretched hide on rack in Beat 3
|
|
1501
1714
|
if (ideaSpring > 0.1) {
|
|
@@ -1510,11 +1723,6 @@ function drawMainScene(t) {
|
|
|
1510
1723
|
|
|
1511
1724
|
ctx.fillStyle = '#b45309';
|
|
1512
1725
|
ctx.fillRect(-28, -20, 56, 32);
|
|
1513
|
-
|
|
1514
|
-
ctx.font = 'bold 15px monospace';
|
|
1515
|
-
ctx.fillStyle = '#d97706';
|
|
1516
|
-
ctx.textAlign = 'center';
|
|
1517
|
-
ctx.fillText('💡 PLANNING FOR TOMORROW', 0, 52);
|
|
1518
1726
|
}
|
|
1519
1727
|
ctx.restore();
|
|
1520
1728
|
}
|
|
@@ -1524,7 +1732,7 @@ function drawMainScene(t) {
|
|
|
1524
1732
|
const scopeSpring = spring(t, window.DURATION * 0.30, 180, 15);
|
|
1525
1733
|
const treeSpring = spring(t, window.DURATION * 0.65, 200, 15);
|
|
1526
1734
|
|
|
1527
|
-
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10,
|
|
1735
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
|
|
1528
1736
|
|
|
1529
1737
|
// Early human scratching pelt with itch lines
|
|
1530
1738
|
drawDoodleCharacter({
|
|
@@ -1573,28 +1781,18 @@ function drawMainScene(t) {
|
|
|
1573
1781
|
sLine(-100, 0, -20, 0, 940, '#f8fafc', 4);
|
|
1574
1782
|
sCircle(-20, 0, 7, 941, '#d4af37', 2, '#d4af37');
|
|
1575
1783
|
|
|
1576
|
-
sLine(-20, 0,
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
sLine(-20, 0, 60, 45, 943, '#f43f5e', 4);
|
|
1583
|
-
ctx.fillStyle = '#f43f5e';
|
|
1584
|
-
ctx.fillText('BODY LICE (CLOTHES)', 70, 48);
|
|
1585
|
-
|
|
1586
|
-
sRect(-90, 80, 180, 32, 950, '#d4af37', 2, 'rgba(212, 175, 55, 0.25)');
|
|
1587
|
-
ctx.font = 'bold 14px monospace';
|
|
1588
|
-
ctx.fillStyle = '#fef08a';
|
|
1589
|
-
ctx.textAlign = 'center';
|
|
1590
|
-
ctx.fillText('170,000 YEARS AGO', 0, 101);
|
|
1784
|
+
sLine(-20, 0, 75, -45, 942, '#38bdf8', 4);
|
|
1785
|
+
sCircle(82, -45, 8, 943, '#38bdf8', 2, '#38bdf8');
|
|
1786
|
+
|
|
1787
|
+
sLine(-20, 0, 75, 45, 944, '#f43f5e', 4);
|
|
1788
|
+
sCircle(82, 45, 8, 945, '#f43f5e', 2, '#f43f5e');
|
|
1591
1789
|
}
|
|
1592
1790
|
ctx.restore();
|
|
1593
1791
|
}
|
|
1594
1792
|
|
|
1595
1793
|
} else if (COMP_THEME === 'doodle_general_human') {
|
|
1596
1794
|
// General Expressive Doodled Human Narrator
|
|
1597
|
-
sLine(cx - W * 0.4, cy + 220, cx + W * 0.4, cy + 220, 10,
|
|
1795
|
+
sLine(cx - W * 0.4, cy + 220, cx + W * 0.4, cy + 220, 10, AMBIENT_LINE, 2);
|
|
1598
1796
|
drawDoodleCharacter({
|
|
1599
1797
|
x: cx,
|
|
1600
1798
|
y: cy + 140,
|
|
@@ -2084,8 +2282,8 @@ function drawMainScene(t) {
|
|
|
2084
2282
|
function drawCinematicOverlay(t) {
|
|
2085
2283
|
ctx.save();
|
|
2086
2284
|
|
|
2087
|
-
// Subtitles / Spoken Narration -
|
|
2088
|
-
if (NARRATION && NARRATION.trim().length > 0) {
|
|
2285
|
+
// Subtitles / Spoken Narration - ONLY rendered when explicitly asked in prompt or options
|
|
2286
|
+
if (SHOW_CAPTIONS && NARRATION && NARRATION.trim().length > 0) {
|
|
2089
2287
|
// Split narration into natural spoken sentence chunks
|
|
2090
2288
|
const sentences = NARRATION.split(/[.!?;|]+/).map(s => s.trim()).filter(Boolean);
|
|
2091
2289
|
|
|
@@ -2234,6 +2432,7 @@ module.exports = {
|
|
|
2234
2432
|
generateSceneCodeWithLLM,
|
|
2235
2433
|
detectAesthetic,
|
|
2236
2434
|
detectComponentTheme,
|
|
2435
|
+
detectCaptionsRequested,
|
|
2237
2436
|
getScriptFontStack,
|
|
2238
2437
|
AESTHETICS,
|
|
2239
2438
|
REGIONAL_FONTS,
|