@amg555/frameforge-mcp 2.4.2 → 2.4.4
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 +60 -0
- package/out/frames/f000048.jpg +0 -0
- package/out/frames/f000049.jpg +0 -0
- package/out/frames/f000050.jpg +0 -0
- package/out/frames/f000051.jpg +0 -0
- package/out/frames/f000052.jpg +0 -0
- package/out/frames/f000053.jpg +0 -0
- package/out/frames/f000054.jpg +0 -0
- package/out/frames/f000055.jpg +0 -0
- package/out/frames/f000056.jpg +0 -0
- package/out/frames/f000057.jpg +0 -0
- package/out/frames/f000058.jpg +0 -0
- package/out/frames/f000059.jpg +0 -0
- package/package.json +1 -1
- package/scripts/deep-validate.js +2 -2
- package/src/cli/make.js +5 -0
- package/src/mcp-server/server.js +6 -2
- package/src/video-renderer/renderer.js +18 -8
- package/src/video-renderer/scene-animator.js +486 -286
- package/src/workflows/render.js +2 -0
- package/src/workflows/script.js +4 -4
|
@@ -268,54 +268,54 @@ function detectComponentTheme({ visualDescription = '', caption = '', narration
|
|
|
268
268
|
const text = `${visualDescription} ${caption} ${narration} ${topic}`.toLowerCase();
|
|
269
269
|
|
|
270
270
|
// ── Doodled Character Actions (Human Stories, History, Evolution, Daily Life) ──
|
|
271
|
-
// 1. Microscope / Lice Theory / Parasite / Genetics (
|
|
271
|
+
// 1. Microscope / Lice Theory / Parasite / Genetics (Scene 7)
|
|
272
272
|
if (/(?:^|[^\w])(lice|louse|microscope|fiber|weave|parasite|lice evolution|genetic biology proof)(?:[^\w]|$)/i.test(text)
|
|
273
273
|
|| /പേൻ|പേൻ തിയറി|തലയിലെ പേൻ|ശരീരത്തിലെ പേൻ|ലൈസ് തിയറി|മൈക്രോസ്കോപ്പ്/.test(text)) {
|
|
274
274
|
return 'doodle_microscope_lice';
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
// 2.
|
|
278
|
-
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)
|
|
279
|
-
|| /വസ്ത്രധാരണം|ധരിച്ചിട്ടുണ്ടാ|ഷർട്ട്|തിരഞ്ഞെടുത്തത്|തുണി എടുത്തിടുകയല്ല|കല്യാണത്തിന്|ഇന്റർവ്യൂ|ആമുഖം: വസ്ത്രധാരണം|വസ്ത്രം തിരഞ്ഞെടുക്കുമ്പോൾ|വസ്ത്രം തിരഞ്ഞെടുക്കും/.test(text)) {
|
|
280
|
-
return 'doodle_dressing';
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// 3. Shivering / Cold / Protection Flaw / Winter / Freezing (e.g. myth busting cold defense)
|
|
277
|
+
// 2. Shivering / Cold / Protection Flaw / Winter / Freezing (Scene 2, 6)
|
|
284
278
|
if (/(?:^|[^\w])(shiver|shivering|cold|freeze|freezing|ice|winter|teeth|chatter|protection flaw)(?:[^\w]|$)/i.test(text)
|
|
285
|
-
||
|
|
279
|
+
|| /സാധാരണ ധാരണകൾ|തണുപ്പും സംരക്ഷണവും|തള്ളിക്കളയുന്നു|ശരീരത്തെ സംരക്ഷിക്കാൻ|ശരീര സംരക്ഷണം|വിറയ്ക്കുക|തണുപ്പിൽ നിന്ന് രക്ഷ നേടാൻ|തണുപ്പ് എന്ന കാരണം|തണുപ്പുള്ള സ്ഥലങ്ങളിലേക്ക്/.test(text)) {
|
|
286
280
|
return 'doodle_shiver_cold';
|
|
287
281
|
}
|
|
288
282
|
|
|
289
|
-
//
|
|
283
|
+
// 3. Hunter in Savannah / Wild / Blazing Sun / Spear / Mosquitoes (Scene 5)
|
|
284
|
+
if (/(?:^|[^\w])(savannah|savanna|spear|hunter|hunting|africa savannah|foraging|wild jungle|mosquito|mosquitoes|thorns)(?:[^\w]|$)/i.test(text)
|
|
285
|
+
|| /കാട്ടിൽ നഗ്നരായി|നഗ്നരായിട്ടാണ്|കൊടുംചൂട്|കൊതുകുകടി|മുള്ളുകൾ|കുന്തം|കാട്ടിലെ ലക്ഷക്കണക്കിന്/.test(text)) {
|
|
286
|
+
return 'doodle_hunter_savannah';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 4. Meeting Tribes / Social Language / Status / Community / Strangers (Scene 10)
|
|
290
290
|
if (/(?:^|[^\w])(tribe|tribal|clan|stranger|meeting strangers|social language|status in society|hierarchy|friend or enemy)(?:[^\w]|$)/i.test(text)
|
|
291
291
|
|| /കൂട്ടമായി|സമൂഹം|അപരിചിത|ഗോത്രം|ശത്രു|സുഹൃത്ത്|സാമൂഹിക ഭാഷ|സാമൂഹിക|സ്ഥാനം തിരിച്ചറിഞ്ഞിരുന്നത്/.test(text)) {
|
|
292
292
|
return 'doodle_tribe_meeting';
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
// 5. Future Planning / Cognitive Revolution / Tomorrow / Foresight / Campfire thinking
|
|
295
|
+
// 5. Future Planning / Cognitive Revolution / Tomorrow / Foresight / Campfire thinking (Scene 8)
|
|
296
296
|
if (/(?:^|[^\w])(planning for tomorrow|future planning|cognitive revolution|thinking future|contemplate|foresight|ian gilligan)(?:[^\w]|$)/i.test(text)
|
|
297
297
|
|| /നാളത്തേക്ക്|പ്ലാൻ|ഭാവി|ഭാവിയെ കുറിച്ച്|ബുദ്ധി ഉപയോഗിച്ച്|ചിന്തയിലെ മാറ്റം|ഇയാൻ ഗില്ലിഗൻ|ചിന്തിക്കാൻ തുടങ്ങി/.test(text)) {
|
|
298
298
|
return 'doodle_thinking_future';
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
// 6.
|
|
302
|
-
if (/(?:^|[^\w])(savannah|savanna|spear|hunter|hunting|africa savannah|foraging|wild jungle|naked|clothless|mosquito|mosquitoes|thorns)(?:[^\w]|$)/i.test(text)
|
|
303
|
-
|| /വേട്ട|വേട്ടയാടി|ആഫ്രിക്കൻ കാടുകളിൽ|കാട്ടിൽ നഗ്നരായി|നഗ്നരായിട്ടാണ്|കൊടുംചൂട്|കൊതുകുകടി|മുള്ളുകൾ|കുന്തം|കാട്ടിലെ ലക്ഷക്കണക്കിന്/.test(text)) {
|
|
304
|
-
return 'doodle_hunter_savannah';
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// 7. Bone Needle / Sewing / Stitching / Animal Pelt / Prehistoric Tools
|
|
301
|
+
// 6. Bone Needle / Sewing / Stitching / Animal Pelt / Prehistoric Tools (Scene 4)
|
|
308
302
|
if (/(?:^|[^\w])(needle|bone needle|needles|sew|sewing|stitch|stitching|thread|animal hide|leather hide|hide sewing|fur pelt|tailor|sewing tools)(?:[^\w]|$)/i.test(text)
|
|
309
303
|
|| /സൂചി|സൂചിയും|തുന്ന|തുന്നാൻ|തോൽ തുന്നാൻ|തുന്നൽ|തോൽ തുന്നാനുള്ള/.test(text)) {
|
|
310
304
|
return 'doodle_sewing_needle';
|
|
311
305
|
}
|
|
312
306
|
|
|
313
|
-
//
|
|
307
|
+
// 7. Shame / Mirror / Reflection / Water / Self Awareness / Theory of Mind (Scene 3, 9)
|
|
314
308
|
if (/(?:^|[^\w])(shame|blush|embarrass|mirror|reflection|water pool|theory of mind|how others see|self-aware|identity|looking at me|psychology|mind-shift)(?:[^\w]|$)/i.test(text)
|
|
315
|
-
||
|
|
309
|
+
|| /കണ്ണാടി|പ്രതിഫലനം|മറ്റുള്ളവർ എന്നെ|തിയറി ഓഫ് മൈൻഡ്|സ്വന്തം ശരീരം മറച്ചു|വ്യക്തിത്വം|ശ്രദ്ധിക്കുന്നുണ്ട്|യഥാർത്ഥ രഹസ്യം|നാണം മറയ്ക്കാൻ/.test(text)) {
|
|
316
310
|
return 'doodle_shame_reflection';
|
|
317
311
|
}
|
|
318
312
|
|
|
313
|
+
// 8. Dressing / Wardrobe / Wearing Clothes / Outfit / Morning Routine (Scene 1, 11)
|
|
314
|
+
if (/(?:^|[^\w])(dress|dressing|wearing clothes|shirt|wardrobe|closet|hanger|outfit|choose clothes|wear|garment|wedding dress|interview|daily habit|why we wear clothes)(?:[^\w]|$)/i.test(text)
|
|
315
|
+
|| /വസ്ത്രധാരണം|ധരിച്ചിട്ടുണ്ടാ|ഷർട്ട്|തിരഞ്ഞെടുത്തത്|തുണി എടുത്തിടുകയല്ല|കല്യാണത്തിന്|ഇന്റർവ്യൂ|ആമുഖം: വസ്ത്രധാരണം|വസ്ത്രം തിരഞ്ഞെടുക്കുമ്പോൾ|വസ്ത്രം തിരഞ്ഞെടുക്കും/.test(text)) {
|
|
316
|
+
return 'doodle_dressing';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
319
|
// 9. General Human Character
|
|
320
320
|
if (/(?:^|[^\w])(human|person|man|woman|people|child|guy|doodle|character|actor)(?:[^\w]|$)/i.test(text)
|
|
321
321
|
|| /മനുഷ്യൻ|ആളുകൾ|വ്യക്തി/.test(text)) {
|
|
@@ -497,6 +497,13 @@ window.seek = async (t) => {
|
|
|
497
497
|
</html>`;
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
+
// ── Captions Requested Detector ───────────────────────────────────────────────
|
|
501
|
+
/** Returns true ONLY if caller explicitly asks for on-screen captions or subtitles in prompt. */
|
|
502
|
+
function detectCaptionsRequested(text = '') {
|
|
503
|
+
if (!text || typeof text !== 'string') return false;
|
|
504
|
+
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);
|
|
505
|
+
}
|
|
506
|
+
|
|
500
507
|
// ── HTML Scene Code Generator ──────────────────────────────────────────────────
|
|
501
508
|
/**
|
|
502
509
|
* Generate a complete, self-contained HTML scene conforming to window.seek(t).
|
|
@@ -517,7 +524,13 @@ function generateSceneHtml({
|
|
|
517
524
|
width = 1920,
|
|
518
525
|
height = 1080,
|
|
519
526
|
sceneIndex = 0,
|
|
527
|
+
showCaptions = false,
|
|
520
528
|
} = {}) {
|
|
529
|
+
const captionsWanted = Boolean(
|
|
530
|
+
showCaptions ||
|
|
531
|
+
detectCaptionsRequested(`${caption} ${narration} ${topic} ${visualDescription} ${style} ${title}`) ||
|
|
532
|
+
process.env.FRAMEFORGE_SHOW_CAPTIONS === '1'
|
|
533
|
+
);
|
|
521
534
|
const aestheticKey = detectAesthetic({
|
|
522
535
|
topic,
|
|
523
536
|
style,
|
|
@@ -627,6 +640,7 @@ function spring(t, delay = 0, tension = ${aesthetic.springTension}, friction = $
|
|
|
627
640
|
return 1 - Math.exp(-omega * dt);
|
|
628
641
|
}
|
|
629
642
|
|
|
643
|
+
const SHOW_CAPTIONS = ${JSON.stringify(captionsWanted)};
|
|
630
644
|
const AESTHETIC = ${JSON.stringify(aestheticKey)};
|
|
631
645
|
const BG_TYPE = ${JSON.stringify(aesthetic.backgroundType)};
|
|
632
646
|
const COMP_THEME = ${JSON.stringify(compTheme)};
|
|
@@ -740,10 +754,51 @@ function drawBackground(t) {
|
|
|
740
754
|
// ── Full-Canvas Cinematic Visual Directors ─────────────────────────────────────
|
|
741
755
|
function drawMainScene(t) {
|
|
742
756
|
ctx.save();
|
|
757
|
+
const p = Math.min(1, Math.max(0, t / window.DURATION));
|
|
743
758
|
const enterSpring = spring(t, 0.05, 170, 13);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
759
|
+
|
|
760
|
+
// Multi-Shot Dynamic Director: cut camera framing every 5-7s to keep pacing energetic
|
|
761
|
+
const shotDur = 5.5;
|
|
762
|
+
const shotIdx = Math.floor(t / shotDur);
|
|
763
|
+
const shotLocalT = t - shotIdx * shotDur;
|
|
764
|
+
const shotCutSpring = spring(shotLocalT, 0.02, 220, 18);
|
|
765
|
+
const shotProgress = (t % shotDur) / shotDur;
|
|
766
|
+
|
|
767
|
+
let targetZoom = 1.0;
|
|
768
|
+
let targetPanX = 0;
|
|
769
|
+
let targetPanY = 0;
|
|
770
|
+
|
|
771
|
+
if (window.DURATION > 6.5) {
|
|
772
|
+
const angle = shotIdx % 4;
|
|
773
|
+
if (angle === 1) {
|
|
774
|
+
targetZoom = 1.14 + shotProgress * 0.03;
|
|
775
|
+
targetPanX = -40 + Math.sin(t * 0.4) * 8;
|
|
776
|
+
targetPanY = -12;
|
|
777
|
+
} else if (angle === 2) {
|
|
778
|
+
targetZoom = 1.20 + shotProgress * 0.03;
|
|
779
|
+
targetPanX = 40 + Math.cos(t * 0.4) * 8;
|
|
780
|
+
targetPanY = -18;
|
|
781
|
+
} else if (angle === 3) {
|
|
782
|
+
targetZoom = 1.08 + shotProgress * 0.02;
|
|
783
|
+
targetPanX = Math.sin(t * 0.3) * 12;
|
|
784
|
+
targetPanY = 12;
|
|
785
|
+
} else {
|
|
786
|
+
targetZoom = 1.02 + shotProgress * 0.03;
|
|
787
|
+
targetPanX = Math.sin(t * 0.3) * 12;
|
|
788
|
+
targetPanY = Math.cos(t * 0.25) * 6;
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
targetZoom = 1.0 + Math.sin(t * 0.45) * 0.02 + p * 0.04;
|
|
792
|
+
targetPanX = Math.sin(t * 0.35) * 12;
|
|
793
|
+
targetPanY = Math.cos(t * 0.28) * 6;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const camZoom = 1.0 + (targetZoom - 1.0) * Math.min(1, Math.max(0, shotCutSpring));
|
|
797
|
+
const camPanX = targetPanX * Math.min(1, Math.max(0, shotCutSpring));
|
|
798
|
+
const camPanY = targetPanY * Math.min(1, Math.max(0, shotCutSpring));
|
|
799
|
+
|
|
800
|
+
ctx.translate(W / 2 + camPanX, H * 0.50 + camPanY);
|
|
801
|
+
ctx.scale(Math.min(1.04, Math.max(0.88, enterSpring)) * camZoom, Math.min(1.04, Math.max(0.88, enterSpring)) * camZoom);
|
|
747
802
|
ctx.translate(-W / 2, -H * 0.50);
|
|
748
803
|
|
|
749
804
|
const cx = W / 2;
|
|
@@ -825,6 +880,12 @@ function drawMainScene(t) {
|
|
|
825
880
|
ctx.translate(x, y);
|
|
826
881
|
ctx.scale(scale, scale);
|
|
827
882
|
|
|
883
|
+
// Soft ground contact shadow
|
|
884
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.28)';
|
|
885
|
+
ctx.beginPath();
|
|
886
|
+
ctx.ellipse(0, legLength, 34, 7, 0, 0, Math.PI * 2);
|
|
887
|
+
ctx.fill();
|
|
888
|
+
|
|
828
889
|
const blink = (t % 3.2 < 0.14) ? 0.15 : 1.0;
|
|
829
890
|
const mouthTalk = Math.abs(Math.sin(t * 14)) * 4;
|
|
830
891
|
const torsoTopY = -torsoHeight;
|
|
@@ -901,6 +962,22 @@ function drawMainScene(t) {
|
|
|
901
962
|
sCircle(-12, eyeY, Math.max(1, eyeR), seed + 23, '#0f172a', 2, '#0f172a');
|
|
902
963
|
sCircle(12, eyeY, Math.max(1, eyeR), seed + 24, '#0f172a', 2, '#0f172a');
|
|
903
964
|
|
|
965
|
+
// Specular eye highlights
|
|
966
|
+
if (blink > 0.4) {
|
|
967
|
+
ctx.fillStyle = '#ffffff';
|
|
968
|
+
ctx.beginPath();
|
|
969
|
+
ctx.arc(-14, eyeY - 1.5, 1.2, 0, Math.PI * 2);
|
|
970
|
+
ctx.arc(10, eyeY - 1.5, 1.2, 0, Math.PI * 2);
|
|
971
|
+
ctx.fill();
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Warm cheek blush
|
|
975
|
+
ctx.fillStyle = 'rgba(251, 113, 133, 0.28)';
|
|
976
|
+
ctx.beginPath();
|
|
977
|
+
ctx.ellipse(-14, headCenterY + 12, 6, 3.5, 0, 0, Math.PI * 2);
|
|
978
|
+
ctx.ellipse(14, headCenterY + 12, 6, 3.5, 0, 0, Math.PI * 2);
|
|
979
|
+
ctx.fill();
|
|
980
|
+
|
|
904
981
|
// Eyebrows
|
|
905
982
|
if (expression === 'shivering') {
|
|
906
983
|
sLine(-18, eyeY - 12 + wb(seed, 2), -6, eyeY - 7 + wb(seed, 2), seed + 25, '#0f172a', 2.5);
|
|
@@ -1027,12 +1104,15 @@ function drawMainScene(t) {
|
|
|
1027
1104
|
ctx.restore();
|
|
1028
1105
|
}
|
|
1029
1106
|
|
|
1030
|
-
// ── Doodled Character Narrative Themes
|
|
1107
|
+
// ── Doodled Character Narrative Themes (3-Beat Progressive Choreography) ─
|
|
1031
1108
|
if (COMP_THEME === 'doodle_dressing') {
|
|
1032
|
-
//
|
|
1109
|
+
// Beat 1: Browsing wardrobe -> Beat 2: Trying shirt -> Beat 3: Looking in mirror reflection
|
|
1110
|
+
const beat2 = spring(t, window.DURATION * 0.35, 180, 16);
|
|
1111
|
+
const beat3 = spring(t, window.DURATION * 0.70, 190, 16);
|
|
1112
|
+
|
|
1033
1113
|
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.25)', 2.5);
|
|
1034
1114
|
|
|
1035
|
-
// Clothes rack on left
|
|
1115
|
+
// Clothes rack on left (sways in beat 1)
|
|
1036
1116
|
sLine(cx - 360, cy + 220, cx - 360, cy - 140, 11, '#94a3b8', 5);
|
|
1037
1117
|
sLine(cx - 410, cy + 220, cx - 310, cy + 220, 12, '#94a3b8', 5);
|
|
1038
1118
|
sLine(cx - 420, cy - 140, cx - 180, cy - 140, 13, '#cbd5e1', 5);
|
|
@@ -1053,96 +1133,132 @@ function drawMainScene(t) {
|
|
|
1053
1133
|
ctx.fill();
|
|
1054
1134
|
}
|
|
1055
1135
|
|
|
1056
|
-
// Mirror on right
|
|
1136
|
+
// Mirror on right - emerges and glows in Beat 2 & 3
|
|
1057
1137
|
const mirrorX = cx + 320;
|
|
1058
1138
|
const mirrorY = cy + 20;
|
|
1059
|
-
ctx.fillStyle = 'rgba(56, 189, 248, 0.08)';
|
|
1139
|
+
ctx.fillStyle = 'rgba(56, 189, 248, ' + (0.05 + beat2 * 0.08) + ')';
|
|
1060
1140
|
ctx.beginPath();
|
|
1061
1141
|
ctx.roundRect(mirrorX - 85, mirrorY - 210, 170, 390, 24);
|
|
1062
1142
|
ctx.fill();
|
|
1063
|
-
ctx.strokeStyle = '#
|
|
1143
|
+
ctx.strokeStyle = beat3 > 0.5 ? '#f59e0b' : '#94a3b8';
|
|
1064
1144
|
ctx.lineWidth = 4;
|
|
1065
1145
|
ctx.stroke();
|
|
1066
1146
|
|
|
1067
|
-
// Reflected character in mirror (
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1147
|
+
// Reflected character in mirror (smiling and thumbs up in beat 3)
|
|
1148
|
+
if (beat2 > 0.1) {
|
|
1149
|
+
ctx.save();
|
|
1150
|
+
ctx.globalAlpha = Math.min(0.65, beat2 * 0.65);
|
|
1151
|
+
drawDoodleCharacter({
|
|
1152
|
+
x: mirrorX,
|
|
1153
|
+
y: cy + 120,
|
|
1154
|
+
scale: 1.05,
|
|
1155
|
+
hairStyle: 'modern',
|
|
1156
|
+
expression: beat3 > 0.5 ? 'smiling' : 'curious',
|
|
1157
|
+
clothing: 'shirt',
|
|
1158
|
+
armPose: beat3 > 0.5 ? 'peace_gesture' : 'dress_shirt',
|
|
1159
|
+
seed: 88,
|
|
1160
|
+
});
|
|
1161
|
+
ctx.restore();
|
|
1162
|
+
}
|
|
1081
1163
|
|
|
1082
|
-
// Main character
|
|
1164
|
+
// Main character moves progressively from rack -> center -> mirror
|
|
1165
|
+
const charX = cx - 220 + beat2 * 170 + beat3 * 60;
|
|
1083
1166
|
drawDoodleCharacter({
|
|
1084
|
-
x:
|
|
1167
|
+
x: charX,
|
|
1085
1168
|
y: cy + 140,
|
|
1086
1169
|
scale: 1.35,
|
|
1087
1170
|
hairStyle: 'modern',
|
|
1088
|
-
expression: 'talking',
|
|
1171
|
+
expression: beat3 > 0.5 ? 'smiling' : 'talking',
|
|
1089
1172
|
clothing: 'shirt',
|
|
1090
|
-
armPose: 'dress_shirt',
|
|
1173
|
+
armPose: beat2 > 0.5 ? 'dress_shirt' : 'natural',
|
|
1091
1174
|
seed: 101,
|
|
1092
1175
|
});
|
|
1093
1176
|
|
|
1094
1177
|
} else if (COMP_THEME === 'doodle_shiver_cold') {
|
|
1095
|
-
//
|
|
1178
|
+
// Beat 1: Shivering in cold -> Beat 2: NOT FOR COLD stamp -> Beat 3: NOT FOR PROTECTION stamp
|
|
1179
|
+
const stamp1 = spring(t, window.DURATION * 0.38, 220, 15);
|
|
1180
|
+
const stamp2 = spring(t, window.DURATION * 0.72, 220, 15);
|
|
1181
|
+
|
|
1096
1182
|
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1097
1183
|
|
|
1098
1184
|
// Freezing wind streaks rushing across
|
|
1099
|
-
for (let w = 0; w <
|
|
1100
|
-
const windX = ((t *
|
|
1101
|
-
const windY = cy - 250 + (w *
|
|
1102
|
-
sLine(windX, windY, windX +
|
|
1185
|
+
for (let w = 0; w < 14; w++) {
|
|
1186
|
+
const windX = ((t * 750 + w * 210) % (W + 600)) - 300;
|
|
1187
|
+
const windY = cy - 250 + (w * 42) + Math.sin(t * 3.5 + w) * 15;
|
|
1188
|
+
sLine(windX, windY, windX + 170, windY + 5, 200 + w, 'rgba(255,255,255,0.3)', 2);
|
|
1103
1189
|
}
|
|
1104
1190
|
|
|
1105
1191
|
// Drifting snow particles
|
|
1106
|
-
for (let
|
|
1107
|
-
const px = ((
|
|
1108
|
-
const py = (
|
|
1109
|
-
sCircle(px, py, 2.5, 300 +
|
|
1192
|
+
for (let sp = 0; sp < 25; sp++) {
|
|
1193
|
+
const px = ((sp * 85 + t * 450) % W);
|
|
1194
|
+
const py = (sp * 45 + t * 90) % H;
|
|
1195
|
+
sCircle(px, py, 2.5, 300 + sp, '#e0f2fe', 1, '#ffffff');
|
|
1110
1196
|
}
|
|
1111
1197
|
|
|
1112
|
-
// Shivering breath vapor cloud
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1198
|
+
// Shivering breath vapor cloud (calms in Beat 3)
|
|
1199
|
+
if (stamp2 < 0.8) {
|
|
1200
|
+
const breathPhase = (t * 2.2) % 1;
|
|
1201
|
+
const bCloudX = cx - 50 + breathPhase * 60;
|
|
1202
|
+
const bCloudY = cy - 90 - breathPhase * 25;
|
|
1203
|
+
ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.5) + ')';
|
|
1204
|
+
ctx.beginPath();
|
|
1205
|
+
ctx.arc(bCloudX, bCloudY, 15 + breathPhase * 25, 0, Math.PI * 2);
|
|
1206
|
+
ctx.fill();
|
|
1207
|
+
}
|
|
1120
1208
|
|
|
1121
|
-
// Main
|
|
1122
|
-
const
|
|
1123
|
-
const
|
|
1209
|
+
// Main character: shivers in Beat 1 & 2, then realizes / curious in Beat 3
|
|
1210
|
+
const isShivering = stamp2 < 0.7;
|
|
1211
|
+
const vibX = isShivering ? Math.sin(t * 38) * 4.5 : 0;
|
|
1212
|
+
const vibY = isShivering ? Math.cos(t * 32) * 2 : 0;
|
|
1124
1213
|
drawDoodleCharacter({
|
|
1125
|
-
x: cx + vibX,
|
|
1214
|
+
x: cx - 120 + vibX,
|
|
1126
1215
|
y: cy + 140 + vibY,
|
|
1127
1216
|
scale: 1.35,
|
|
1128
1217
|
hairStyle: 'wild',
|
|
1129
|
-
expression: 'shivering',
|
|
1218
|
+
expression: isShivering ? 'shivering' : 'curious',
|
|
1130
1219
|
clothing: 'none',
|
|
1131
|
-
armPose: 'shiver',
|
|
1220
|
+
armPose: isShivering ? 'shiver' : 'thinker',
|
|
1132
1221
|
seed: 102,
|
|
1133
1222
|
});
|
|
1134
1223
|
|
|
1135
|
-
//
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1224
|
+
// Stamp 1: NOT FOR COLD (slams down in Beat 2)
|
|
1225
|
+
if (stamp1 > 0.05) {
|
|
1226
|
+
ctx.save();
|
|
1227
|
+
const s1X = cx + 240;
|
|
1228
|
+
const s1Y = cy - 100;
|
|
1229
|
+
ctx.translate(s1X, s1Y);
|
|
1230
|
+
ctx.scale(stamp1, stamp1);
|
|
1231
|
+
sCircle(0, 0, 50, 400, 'rgba(239,68,68,0.9)', 4);
|
|
1232
|
+
sLine(-36, -36, 36, 36, 401, 'rgba(239,68,68,0.95)', 4.5);
|
|
1233
|
+
ctx.font = 'bold 20px monospace';
|
|
1234
|
+
ctx.fillStyle = '#ef4444';
|
|
1235
|
+
ctx.textAlign = 'center';
|
|
1236
|
+
ctx.fillText('NOT FOR COLD', 0, 75);
|
|
1237
|
+
ctx.restore();
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Stamp 2: NOT FOR PROTECTION (slams down in Beat 3)
|
|
1241
|
+
if (stamp2 > 0.05) {
|
|
1242
|
+
ctx.save();
|
|
1243
|
+
const s2X = cx + 240;
|
|
1244
|
+
const s2Y = cy + 90;
|
|
1245
|
+
ctx.translate(s2X, s2Y);
|
|
1246
|
+
ctx.scale(stamp2, stamp2);
|
|
1247
|
+
sCircle(0, 0, 50, 410, 'rgba(239,68,68,0.9)', 4);
|
|
1248
|
+
sLine(-36, -36, 36, 36, 411, 'rgba(239,68,68,0.95)', 4.5);
|
|
1249
|
+
ctx.font = 'bold 20px monospace';
|
|
1250
|
+
ctx.fillStyle = '#ef4444';
|
|
1251
|
+
ctx.textAlign = 'center';
|
|
1252
|
+
ctx.fillText('NOT FOR PROTECTION', 0, 75);
|
|
1253
|
+
ctx.restore();
|
|
1254
|
+
}
|
|
1142
1255
|
|
|
1143
1256
|
} else if (COMP_THEME === 'doodle_hunter_savannah') {
|
|
1144
|
-
//
|
|
1145
|
-
|
|
1257
|
+
// Beat 1: Walking with spear -> Beat 2: Swatting mosquitoes -> Beat 3: Pausing alert
|
|
1258
|
+
const walkProg = Math.min(1, Math.max(0, t / window.DURATION));
|
|
1259
|
+
const swatting = (t % 2.4 > 1.2);
|
|
1260
|
+
|
|
1261
|
+
// Blazing midday sun with undulating rays
|
|
1146
1262
|
const sunR = 75 + Math.sin(t * 2) * 6;
|
|
1147
1263
|
ctx.fillStyle = '#f59e0b';
|
|
1148
1264
|
ctx.beginPath();
|
|
@@ -1165,27 +1281,31 @@ function drawMainScene(t) {
|
|
|
1165
1281
|
sLine(gx, cy + 220, gx + sway, cy + 140, 500 + g, '#d97706', 2.5);
|
|
1166
1282
|
}
|
|
1167
1283
|
|
|
1168
|
-
// Buzzing mosquitoes orbiting
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1284
|
+
// Buzzing mosquitoes orbiting hunter
|
|
1285
|
+
const charX = cx - 180 + walkProg * 240;
|
|
1286
|
+
for (let m = 0; m < 4; m++) {
|
|
1287
|
+
const mx = charX + 50 + Math.sin(t * 8 + m * 2) * 65;
|
|
1288
|
+
const my = cy - 60 + Math.cos(t * 7 + m * 2) * 45;
|
|
1289
|
+
sCircle(mx, my, 2.5, 550 + m, '#0f172a', 1.5, '#ef4444');
|
|
1173
1290
|
}
|
|
1174
1291
|
|
|
1175
|
-
// Walking early hunter
|
|
1292
|
+
// Walking early hunter with spear
|
|
1176
1293
|
drawDoodleCharacter({
|
|
1177
|
-
x:
|
|
1294
|
+
x: charX,
|
|
1178
1295
|
y: cy + 140,
|
|
1179
1296
|
scale: 1.35,
|
|
1180
1297
|
hairStyle: 'wild',
|
|
1181
|
-
expression: 'talking',
|
|
1298
|
+
expression: swatting ? 'shivering' : 'talking',
|
|
1182
1299
|
clothing: 'none',
|
|
1183
1300
|
armPose: 'hold_spear',
|
|
1184
1301
|
seed: 103,
|
|
1185
1302
|
});
|
|
1186
1303
|
|
|
1187
1304
|
} else if (COMP_THEME === 'doodle_sewing_needle') {
|
|
1188
|
-
//
|
|
1305
|
+
// Beat 1: Seated by campfire -> Beat 2: Zoom loupe with bone needle -> Beat 3: Timeline badges
|
|
1306
|
+
const loupeSpring = spring(t, window.DURATION * 0.32, 180, 15);
|
|
1307
|
+
const badgeSpring = spring(t, window.DURATION * 0.68, 200, 15);
|
|
1308
|
+
|
|
1189
1309
|
// Campfire with animated flames and sparks
|
|
1190
1310
|
const fireX = cx - 380;
|
|
1191
1311
|
const fireY = cy + 180;
|
|
@@ -1197,16 +1317,15 @@ function drawMainScene(t) {
|
|
|
1197
1317
|
ctx.quadraticCurveTo(fireX + (f - 2) * 8, fireY - flH, fireX - 10 + f * 12, fireY);
|
|
1198
1318
|
ctx.fill();
|
|
1199
1319
|
}
|
|
1200
|
-
// Floating fire sparks
|
|
1201
1320
|
for (let sp = 0; sp < 8; sp++) {
|
|
1202
1321
|
const sx = fireX + Math.sin(t * 4 + sp) * 35;
|
|
1203
1322
|
const sy = fireY - 30 - ((t * 80 + sp * 35) % 140);
|
|
1204
1323
|
sCircle(sx, sy, 2, 600 + sp, '#fbbf24', 1, '#fbbf24');
|
|
1205
1324
|
}
|
|
1206
1325
|
|
|
1207
|
-
//
|
|
1326
|
+
// Seated human stitching hide with bone needle
|
|
1208
1327
|
drawDoodleCharacter({
|
|
1209
|
-
x: cx -
|
|
1328
|
+
x: cx - 120,
|
|
1210
1329
|
y: cy + 140,
|
|
1211
1330
|
scale: 1.35,
|
|
1212
1331
|
hairStyle: 'wild',
|
|
@@ -1216,39 +1335,57 @@ function drawMainScene(t) {
|
|
|
1216
1335
|
seed: 104,
|
|
1217
1336
|
});
|
|
1218
1337
|
|
|
1219
|
-
// Zoomed
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
ctx.fill();
|
|
1227
|
-
ctx.strokeStyle = '#d4af37';
|
|
1228
|
-
ctx.lineWidth = 5;
|
|
1229
|
-
ctx.stroke();
|
|
1338
|
+
// Zoomed loupe expands open in Beat 2
|
|
1339
|
+
if (loupeSpring > 0.05) {
|
|
1340
|
+
ctx.save();
|
|
1341
|
+
const lensX = cx + 270;
|
|
1342
|
+
const lensY = cy - 20;
|
|
1343
|
+
ctx.translate(lensX, lensY);
|
|
1344
|
+
ctx.scale(loupeSpring, loupeSpring);
|
|
1230
1345
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1346
|
+
const lensR = 140;
|
|
1347
|
+
ctx.fillStyle = 'rgba(15, 23, 42, 0.92)';
|
|
1348
|
+
ctx.beginPath();
|
|
1349
|
+
ctx.arc(0, 0, lensR, 0, Math.PI * 2);
|
|
1350
|
+
ctx.fill();
|
|
1351
|
+
ctx.strokeStyle = '#d4af37';
|
|
1352
|
+
ctx.lineWidth = 5;
|
|
1353
|
+
ctx.stroke();
|
|
1354
|
+
|
|
1355
|
+
// Two thick leather pelt pieces being stitched
|
|
1356
|
+
sLine(-85, -20, 85, -20, 650, '#92400e', 8);
|
|
1357
|
+
sLine(-85, 20, 85, 20, 651, '#78350f', 8);
|
|
1234
1358
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1359
|
+
// Drilled bone needle piercing through with animated wiggle
|
|
1360
|
+
const nWiggle = Math.sin(t * 4.5) * 18;
|
|
1361
|
+
sLine(-45 + nWiggle, -65, 25 + nWiggle, 55, 652, '#fef08a', 6);
|
|
1362
|
+
sCircle(-40 + nWiggle, -58, 4, 653, '#0f172a', 1.5, '#0f172a');
|
|
1239
1363
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1364
|
+
// Stitches across pelts
|
|
1365
|
+
for (let st = 0; st < 6; st++) {
|
|
1366
|
+
const stX = -70 + st * 28;
|
|
1367
|
+
sLine(stX, -22, stX + 8, 22, 660 + st, '#ffffff', 3.5);
|
|
1368
|
+
}
|
|
1369
|
+
ctx.restore();
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Timeline badge in Beat 3
|
|
1373
|
+
if (badgeSpring > 0.05) {
|
|
1374
|
+
ctx.save();
|
|
1375
|
+
ctx.globalAlpha = Math.min(1, badgeSpring);
|
|
1376
|
+
sRect(cx + 120, cy + 140, 300, 42, 670, '#d4af37', 2.5, 'rgba(15, 23, 42, 0.85)');
|
|
1377
|
+
ctx.font = 'bold 16px monospace';
|
|
1378
|
+
ctx.fillStyle = '#fef08a';
|
|
1379
|
+
ctx.textAlign = 'center';
|
|
1380
|
+
ctx.fillText('BONE NEEDLE • 120,000 YEARS', cx + 270, cy + 167);
|
|
1381
|
+
ctx.restore();
|
|
1244
1382
|
}
|
|
1245
|
-
ctx.font = 'bold 18px monospace';
|
|
1246
|
-
ctx.fillStyle = '#d4af37';
|
|
1247
|
-
ctx.textAlign = 'center';
|
|
1248
|
-
ctx.fillText('BONE NEEDLE • 120,000 YEARS', lensX, lensY + lensR + 32);
|
|
1249
1383
|
|
|
1250
1384
|
} else if (COMP_THEME === 'doodle_shame_reflection') {
|
|
1251
|
-
// Water
|
|
1385
|
+
// Beat 1: Looking down -> Beat 2: Water reflection & blushing -> Beat 3: Theory of Mind halo
|
|
1386
|
+
const refSpring = spring(t, window.DURATION * 0.30, 180, 15);
|
|
1387
|
+
const haloSpring = spring(t, window.DURATION * 0.65, 190, 15);
|
|
1388
|
+
|
|
1252
1389
|
const poolX = cx;
|
|
1253
1390
|
const poolY = cy + 180;
|
|
1254
1391
|
ctx.fillStyle = 'rgba(14, 165, 233, 0.15)';
|
|
@@ -1269,22 +1406,24 @@ function drawMainScene(t) {
|
|
|
1269
1406
|
ctx.stroke();
|
|
1270
1407
|
}
|
|
1271
1408
|
|
|
1272
|
-
// Inverted rippling reflection of character in water
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1409
|
+
// Inverted rippling reflection of character in water (clarifies in Beat 2)
|
|
1410
|
+
if (refSpring > 0.1) {
|
|
1411
|
+
ctx.save();
|
|
1412
|
+
ctx.globalAlpha = Math.min(0.45, refSpring * 0.45);
|
|
1413
|
+
ctx.translate(cx, poolY + 20);
|
|
1414
|
+
ctx.scale(1.2, -0.75);
|
|
1415
|
+
drawDoodleCharacter({
|
|
1416
|
+
x: 0,
|
|
1417
|
+
y: 0,
|
|
1418
|
+
scale: 1.0,
|
|
1419
|
+
hairStyle: 'wild',
|
|
1420
|
+
expression: 'surprised',
|
|
1421
|
+
clothing: 'none',
|
|
1422
|
+
armPose: 'cover_body',
|
|
1423
|
+
seed: 700,
|
|
1424
|
+
});
|
|
1425
|
+
ctx.restore();
|
|
1426
|
+
}
|
|
1288
1427
|
|
|
1289
1428
|
// Standing character looking down and covering body
|
|
1290
1429
|
drawDoodleCharacter({
|
|
@@ -1292,77 +1431,98 @@ function drawMainScene(t) {
|
|
|
1292
1431
|
y: cy + 120,
|
|
1293
1432
|
scale: 1.35,
|
|
1294
1433
|
hairStyle: 'wild',
|
|
1295
|
-
expression: 'surprised',
|
|
1434
|
+
expression: refSpring > 0.4 ? 'surprised' : 'talking',
|
|
1296
1435
|
clothing: 'none',
|
|
1297
|
-
armPose: 'cover_body',
|
|
1436
|
+
armPose: refSpring > 0.4 ? 'cover_body' : 'natural',
|
|
1298
1437
|
seed: 105,
|
|
1299
1438
|
});
|
|
1300
1439
|
|
|
1301
|
-
// Blushing cheeks
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1440
|
+
// Blushing cheeks appear in Beat 2
|
|
1441
|
+
if (refSpring > 0.3) {
|
|
1442
|
+
const blushA = Math.min(0.7, refSpring * 0.7);
|
|
1443
|
+
sCircle(cx - 18, cy - 8, 7, 750, 'rgba(244,63,94,' + blushA + ')', 1, 'rgba(244,63,94,' + blushA + ')');
|
|
1444
|
+
sCircle(cx + 18, cy - 8, 7, 751, 'rgba(244,63,94,' + blushA + ')', 1, 'rgba(244,63,94,' + blushA + ')');
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Theory of Mind Thought Halo in Beat 3
|
|
1448
|
+
if (haloSpring > 0.05) {
|
|
1449
|
+
ctx.save();
|
|
1450
|
+
const haloX = cx + 180;
|
|
1451
|
+
const haloY = cy - 120;
|
|
1452
|
+
ctx.translate(haloX, haloY);
|
|
1453
|
+
ctx.scale(haloSpring, haloSpring);
|
|
1454
|
+
sCircle(0, 0, 48, 760, '#38bdf8', 3);
|
|
1455
|
+
sCircle(0, 0, 18, 761, '#ffffff', 2.5);
|
|
1456
|
+
sCircle(0, 0, 8, 762, '#38bdf8', 1, '#38bdf8');
|
|
1457
|
+
ctx.font = 'bold 16px monospace';
|
|
1458
|
+
ctx.fillStyle = '#38bdf8';
|
|
1459
|
+
ctx.textAlign = 'center';
|
|
1460
|
+
ctx.fillText('THEORY OF MIND', 0, 72);
|
|
1461
|
+
ctx.restore();
|
|
1462
|
+
}
|
|
1316
1463
|
|
|
1317
1464
|
} else if (COMP_THEME === 'doodle_tribe_meeting') {
|
|
1318
|
-
//
|
|
1465
|
+
// Beat 1: Walking toward center -> Beat 2: Greet & check patterns -> Beat 3: Social language arc
|
|
1466
|
+
const approach = spring(t, 0.05, 140, 16);
|
|
1467
|
+
const greet = spring(t, window.DURATION * 0.38, 190, 15);
|
|
1468
|
+
const arcSpring = spring(t, window.DURATION * 0.68, 200, 15);
|
|
1469
|
+
|
|
1319
1470
|
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1320
1471
|
|
|
1321
|
-
// Left Clan Member
|
|
1472
|
+
// Left Clan Member walks toward center
|
|
1473
|
+
const leftX = cx - 380 + Math.min(1, approach) * 160;
|
|
1322
1474
|
drawDoodleCharacter({
|
|
1323
|
-
x:
|
|
1475
|
+
x: leftX,
|
|
1324
1476
|
y: cy + 140,
|
|
1325
1477
|
scale: 1.3,
|
|
1326
1478
|
hairStyle: 'wild',
|
|
1327
1479
|
expression: 'talking',
|
|
1328
1480
|
clothing: 'pelt',
|
|
1329
|
-
armPose: 'peace_gesture',
|
|
1481
|
+
armPose: greet > 0.5 ? 'peace_gesture' : 'natural',
|
|
1330
1482
|
seed: 106,
|
|
1331
1483
|
});
|
|
1332
1484
|
|
|
1333
|
-
// Right Clan Member
|
|
1485
|
+
// Right Clan Member walks toward center
|
|
1486
|
+
const rightX = cx + 380 - Math.min(1, approach) * 160;
|
|
1334
1487
|
drawDoodleCharacter({
|
|
1335
|
-
x:
|
|
1488
|
+
x: rightX,
|
|
1336
1489
|
y: cy + 140,
|
|
1337
1490
|
scale: 1.3,
|
|
1338
1491
|
hairStyle: 'feather',
|
|
1339
1492
|
expression: 'curious',
|
|
1340
1493
|
clothing: 'cloak',
|
|
1341
|
-
armPose: 'natural',
|
|
1494
|
+
armPose: greet > 0.5 ? 'peace_gesture' : 'natural',
|
|
1342
1495
|
seed: 107,
|
|
1343
1496
|
});
|
|
1344
1497
|
|
|
1345
|
-
// Glowing connection arc and
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1498
|
+
// Glowing connection arc and symbols in Beat 3
|
|
1499
|
+
if (arcSpring > 0.05) {
|
|
1500
|
+
ctx.save();
|
|
1501
|
+
ctx.globalAlpha = Math.min(1, arcSpring);
|
|
1502
|
+
ctx.strokeStyle = ACCENT;
|
|
1503
|
+
ctx.lineWidth = 3;
|
|
1504
|
+
ctx.setLineDash([8, 8]);
|
|
1505
|
+
ctx.beginPath();
|
|
1506
|
+
ctx.moveTo(leftX + 40, cy);
|
|
1507
|
+
ctx.quadraticCurveTo(cx, cy - 100, rightX - 40, cy);
|
|
1508
|
+
ctx.stroke();
|
|
1509
|
+
ctx.setLineDash([]);
|
|
1354
1510
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1511
|
+
sCircle(cx, cy - 65, 34, 800, ACCENT, 3, 'rgba(15, 23, 42, 0.85)');
|
|
1512
|
+
ctx.font = 'bold 22px monospace';
|
|
1513
|
+
ctx.fillStyle = ACCENT;
|
|
1514
|
+
ctx.textAlign = 'center';
|
|
1515
|
+
ctx.fillText('🤝', cx, cy - 56);
|
|
1516
|
+
ctx.font = 'bold 16px monospace';
|
|
1517
|
+
ctx.fillText('CLOTHING AS SOCIAL LANGUAGE', cx, cy + 15);
|
|
1518
|
+
ctx.restore();
|
|
1519
|
+
}
|
|
1363
1520
|
|
|
1364
1521
|
} else if (COMP_THEME === 'doodle_thinking_future') {
|
|
1365
|
-
//
|
|
1522
|
+
// Beat 1: Seated thinker by fire -> Beat 2: Thought cloud floats up -> Beat 3: Tomorrow's rack & tools
|
|
1523
|
+
const cloudSpring = spring(t, window.DURATION * 0.32, 170, 14);
|
|
1524
|
+
const ideaSpring = spring(t, window.DURATION * 0.65, 190, 15);
|
|
1525
|
+
|
|
1366
1526
|
// Starlit night and crescent moon
|
|
1367
1527
|
ctx.fillStyle = '#fef08a';
|
|
1368
1528
|
ctx.beginPath();
|
|
@@ -1391,102 +1551,120 @@ function drawMainScene(t) {
|
|
|
1391
1551
|
seed: 108,
|
|
1392
1552
|
});
|
|
1393
1553
|
|
|
1394
|
-
// Expanding thought cloud overhead
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1554
|
+
// Expanding thought cloud overhead in Beat 2
|
|
1555
|
+
if (cloudSpring > 0.05) {
|
|
1556
|
+
const cloudX = cx + 180;
|
|
1557
|
+
const cloudY = cy - 80;
|
|
1558
|
+
ctx.save();
|
|
1559
|
+
ctx.translate(cloudX, cloudY);
|
|
1560
|
+
ctx.scale(cloudSpring, cloudSpring);
|
|
1398
1561
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1562
|
+
sCircle(-180, 160, 12, 850, '#ffffff', 2, '#ffffff');
|
|
1563
|
+
sCircle(-140, 120, 18, 851, '#ffffff', 2, '#ffffff');
|
|
1564
|
+
sCircle(-90, 70, 24, 852, '#ffffff', 2, '#ffffff');
|
|
1402
1565
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1566
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
|
1567
|
+
ctx.beginPath();
|
|
1568
|
+
ctx.arc(-70, 0, 55, 0, Math.PI * 2);
|
|
1569
|
+
ctx.arc(0, -25, 65, 0, Math.PI * 2);
|
|
1570
|
+
ctx.arc(70, 0, 55, 0, Math.PI * 2);
|
|
1571
|
+
ctx.arc(0, 30, 60, 0, Math.PI * 2);
|
|
1572
|
+
ctx.fill();
|
|
1407
1573
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
ctx.arc(0, 30, 60, 0, Math.PI * 2);
|
|
1415
|
-
ctx.fill();
|
|
1574
|
+
// Inside thought cloud: Tomorrow's sunrise & stretched hide on rack in Beat 3
|
|
1575
|
+
if (ideaSpring > 0.1) {
|
|
1576
|
+
ctx.fillStyle = '#f59e0b';
|
|
1577
|
+
ctx.beginPath();
|
|
1578
|
+
ctx.arc(0, 15, 30, Math.PI, Math.PI * 2);
|
|
1579
|
+
ctx.fill();
|
|
1416
1580
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
ctx.arc(0, 15, 30, Math.PI, Math.PI * 2);
|
|
1421
|
-
ctx.fill();
|
|
1422
|
-
|
|
1423
|
-
// Drying frame rack
|
|
1424
|
-
sLine(-45, 15, -45, -35, 860, '#78350f', 4);
|
|
1425
|
-
sLine(45, 15, 45, -35, 861, '#78350f', 4);
|
|
1426
|
-
sLine(-50, -30, 50, -30, 862, '#78350f', 4);
|
|
1427
|
-
sRect(-35, -28, 70, 40, 863, '#92400e', 3, '#b45309');
|
|
1581
|
+
sLine(-35, 15, -35, -25, 860, '#78350f', 3);
|
|
1582
|
+
sLine(35, 15, 35, -25, 861, '#78350f', 3);
|
|
1583
|
+
sLine(-40, -25, 40, -25, 862, '#78350f', 3);
|
|
1428
1584
|
|
|
1429
|
-
|
|
1585
|
+
ctx.fillStyle = '#b45309';
|
|
1586
|
+
ctx.fillRect(-28, -20, 56, 32);
|
|
1430
1587
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1588
|
+
ctx.font = 'bold 15px monospace';
|
|
1589
|
+
ctx.fillStyle = '#d97706';
|
|
1590
|
+
ctx.textAlign = 'center';
|
|
1591
|
+
ctx.fillText('💡 PLANNING FOR TOMORROW', 0, 52);
|
|
1592
|
+
}
|
|
1593
|
+
ctx.restore();
|
|
1594
|
+
}
|
|
1435
1595
|
|
|
1436
1596
|
} else if (COMP_THEME === 'doodle_microscope_lice') {
|
|
1437
|
-
//
|
|
1438
|
-
|
|
1597
|
+
// Beat 1: Scratching pelt -> Beat 2: Giant microscope slides in -> Beat 3: Divergence tree
|
|
1598
|
+
const scopeSpring = spring(t, window.DURATION * 0.30, 180, 15);
|
|
1599
|
+
const treeSpring = spring(t, window.DURATION * 0.65, 200, 15);
|
|
1600
|
+
|
|
1601
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1602
|
+
|
|
1603
|
+
// Early human scratching pelt with itch lines
|
|
1439
1604
|
drawDoodleCharacter({
|
|
1440
|
-
x: cx -
|
|
1605
|
+
x: cx - 240,
|
|
1441
1606
|
y: cy + 140,
|
|
1442
1607
|
scale: 1.35,
|
|
1443
1608
|
hairStyle: 'wild',
|
|
1444
|
-
expression: '
|
|
1609
|
+
expression: 'shivering',
|
|
1445
1610
|
clothing: 'pelt',
|
|
1446
1611
|
armPose: 'shiver',
|
|
1447
1612
|
seed: 109,
|
|
1448
1613
|
});
|
|
1449
1614
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
ctx.fillStyle = 'rgba(15, 23, 42, 0.94)';
|
|
1455
|
-
ctx.beginPath();
|
|
1456
|
-
ctx.arc(lensX, lensY, lensR, 0, Math.PI * 2);
|
|
1457
|
-
ctx.fill();
|
|
1458
|
-
ctx.strokeStyle = '#d4af37';
|
|
1459
|
-
ctx.lineWidth = 6;
|
|
1460
|
-
ctx.stroke();
|
|
1461
|
-
|
|
1462
|
-
// Fabric thread weave lines in lens background
|
|
1463
|
-
for (let f = -4; f <= 4; f++) {
|
|
1464
|
-
sLine(lensX - 140, lensY + f * 28, lensX + 140, lensY + f * 28, 900 + f, 'rgba(255,255,255,0.12)', 2);
|
|
1465
|
-
sLine(lensX + f * 28, lensY - 140, lensX + f * 28, lensY + 140, 920 + f, 'rgba(255,255,255,0.12)', 2);
|
|
1615
|
+
for (let it = 0; it < 3; it++) {
|
|
1616
|
+
const itX = cx - 290 - it * 12;
|
|
1617
|
+
const itY = cy + 50 + it * 20;
|
|
1618
|
+
sLine(itX, itY, itX + 18, itY - 14, 900 + it, '#f59e0b', 2.5);
|
|
1466
1619
|
}
|
|
1467
1620
|
|
|
1468
|
-
//
|
|
1469
|
-
|
|
1470
|
-
|
|
1621
|
+
// Microscope slides in in Beat 2
|
|
1622
|
+
if (scopeSpring > 0.05) {
|
|
1623
|
+
const lensX = cx + 180;
|
|
1624
|
+
const lensY = cy;
|
|
1625
|
+
const lensR = 150;
|
|
1626
|
+
|
|
1627
|
+
ctx.save();
|
|
1628
|
+
ctx.translate(lensX, lensY);
|
|
1629
|
+
ctx.scale(scopeSpring, scopeSpring);
|
|
1471
1630
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1631
|
+
ctx.fillStyle = 'rgba(15, 23, 42, 0.95)';
|
|
1632
|
+
ctx.beginPath();
|
|
1633
|
+
ctx.arc(0, 0, lensR, 0, Math.PI * 2);
|
|
1634
|
+
ctx.fill();
|
|
1635
|
+
ctx.strokeStyle = '#d4af37';
|
|
1636
|
+
ctx.lineWidth = 6;
|
|
1637
|
+
ctx.stroke();
|
|
1478
1638
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1639
|
+
// Fabric weave grid lines inside lens
|
|
1640
|
+
for (let g = -4; g <= 4; g++) {
|
|
1641
|
+
sLine(g * 24, -lensR * 0.8, g * 24, lensR * 0.8, 920 + g, 'rgba(56, 189, 248, 0.15)', 2);
|
|
1642
|
+
sLine(-lensR * 0.8, g * 24, lensR * 0.8, g * 24, 930 + g, 'rgba(56, 189, 248, 0.15)', 2);
|
|
1643
|
+
}
|
|
1483
1644
|
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1645
|
+
// Divergence tree in Beat 3
|
|
1646
|
+
if (treeSpring > 0.1) {
|
|
1647
|
+
sLine(-100, 0, -20, 0, 940, '#f8fafc', 4);
|
|
1648
|
+
sCircle(-20, 0, 7, 941, '#d4af37', 2, '#d4af37');
|
|
1649
|
+
|
|
1650
|
+
sLine(-20, 0, 60, -45, 942, '#38bdf8', 4);
|
|
1651
|
+
ctx.font = 'bold 14px monospace';
|
|
1652
|
+
ctx.fillStyle = '#38bdf8';
|
|
1653
|
+
ctx.textAlign = 'left';
|
|
1654
|
+
ctx.fillText('HEAD LICE (HAIR)', 70, -42);
|
|
1655
|
+
|
|
1656
|
+
sLine(-20, 0, 60, 45, 943, '#f43f5e', 4);
|
|
1657
|
+
ctx.fillStyle = '#f43f5e';
|
|
1658
|
+
ctx.fillText('BODY LICE (CLOTHES)', 70, 48);
|
|
1659
|
+
|
|
1660
|
+
sRect(-90, 80, 180, 32, 950, '#d4af37', 2, 'rgba(212, 175, 55, 0.25)');
|
|
1661
|
+
ctx.font = 'bold 14px monospace';
|
|
1662
|
+
ctx.fillStyle = '#fef08a';
|
|
1663
|
+
ctx.textAlign = 'center';
|
|
1664
|
+
ctx.fillText('170,000 YEARS AGO', 0, 101);
|
|
1665
|
+
}
|
|
1666
|
+
ctx.restore();
|
|
1667
|
+
}
|
|
1490
1668
|
|
|
1491
1669
|
} else if (COMP_THEME === 'doodle_general_human') {
|
|
1492
1670
|
// General Expressive Doodled Human Narrator
|
|
@@ -1976,49 +2154,70 @@ function drawMainScene(t) {
|
|
|
1976
2154
|
ctx.restore();
|
|
1977
2155
|
}
|
|
1978
2156
|
|
|
1979
|
-
// ──
|
|
2157
|
+
// ── Sentence-Synchronized Broadcast Subtitles Overlay ───────────────────────
|
|
1980
2158
|
function drawCinematicOverlay(t) {
|
|
1981
2159
|
ctx.save();
|
|
1982
|
-
const textSpring = spring(t, 0.12, 190, 15);
|
|
1983
|
-
const headlineAlpha = Math.min(1, Math.max(0, textSpring));
|
|
1984
|
-
ctx.globalAlpha = headlineAlpha;
|
|
1985
2160
|
|
|
1986
|
-
//
|
|
1987
|
-
|
|
2161
|
+
// Subtitles / Spoken Narration - ONLY rendered when explicitly asked in prompt or options
|
|
2162
|
+
if (SHOW_CAPTIONS && NARRATION && NARRATION.trim().length > 0) {
|
|
2163
|
+
// Split narration into natural spoken sentence chunks
|
|
2164
|
+
const sentences = NARRATION.split(/[.!?;|]+/).map(s => s.trim()).filter(Boolean);
|
|
1988
2165
|
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
ctx.shadowColor = 'rgba(0, 0, 0, 0.85)';
|
|
1994
|
-
ctx.shadowBlur = 10;
|
|
1995
|
-
ctx.textAlign = 'left';
|
|
1996
|
-
ctx.fillText('// ' + TAG, 80, capY - 22);
|
|
1997
|
-
}
|
|
2166
|
+
const count = Math.max(1, sentences.length);
|
|
2167
|
+
const sentDur = window.DURATION / count;
|
|
2168
|
+
const currIdx = Math.min(count - 1, Math.floor(t / sentDur));
|
|
2169
|
+
const activeText = sentences[currIdx] || NARRATION;
|
|
1998
2170
|
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
// Subtitle / Spoken Narration - bottom center clean broadcast subtitles
|
|
2010
|
-
if (NARRATION) {
|
|
2011
|
-
const subSpring = spring(t, 0.22, 180, 16);
|
|
2012
|
-
ctx.globalAlpha = Math.min(1, Math.max(0, subSpring));
|
|
2013
|
-
ctx.font = '500 28px ' + FONT_BODY;
|
|
2014
|
-
ctx.fillStyle = '#f8fafc';
|
|
2015
|
-
ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
|
|
2016
|
-
ctx.shadowBlur = 18;
|
|
2017
|
-
ctx.shadowOffsetY = 3;
|
|
2171
|
+
// Sentence local spring transition (smooth entrance per sentence)
|
|
2172
|
+
const localT = t - currIdx * sentDur;
|
|
2173
|
+
const sentSpring = spring(localT, 0.04, 210, 16);
|
|
2174
|
+
const sentAlpha = Math.min(1, Math.max(0, sentSpring));
|
|
2175
|
+
const sentSlideY = (1 - sentSpring) * 10;
|
|
2176
|
+
|
|
2177
|
+
ctx.save();
|
|
2178
|
+
ctx.globalAlpha = sentAlpha;
|
|
2179
|
+
ctx.font = '500 32px ' + FONT_BODY;
|
|
2018
2180
|
ctx.textAlign = 'center';
|
|
2019
|
-
|
|
2020
|
-
|
|
2181
|
+
|
|
2182
|
+
// Word wrap if sentence is long (> 58 chars)
|
|
2183
|
+
const maxChars = 56;
|
|
2184
|
+
const words = activeText.split(' ');
|
|
2185
|
+
const lines = [];
|
|
2186
|
+
let currentLine = '';
|
|
2187
|
+
for (let w = 0; w < words.length; w++) {
|
|
2188
|
+
const testLine = currentLine ? currentLine + ' ' + words[w] : words[w];
|
|
2189
|
+
if (testLine.length > maxChars && currentLine.length > 0) {
|
|
2190
|
+
lines.push(currentLine);
|
|
2191
|
+
currentLine = words[w];
|
|
2192
|
+
} else {
|
|
2193
|
+
currentLine = testLine;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
if (currentLine) lines.push(currentLine);
|
|
2197
|
+
|
|
2198
|
+
const subBaseY = H * 0.90 + sentSlideY;
|
|
2199
|
+
const lineSpacing = 44;
|
|
2200
|
+
const startY = subBaseY - ((lines.length - 1) * lineSpacing) / 2;
|
|
2201
|
+
|
|
2202
|
+
for (let l = 0; l < lines.length; l++) {
|
|
2203
|
+
const ly = startY + l * lineSpacing;
|
|
2204
|
+
// Soft translucent background backing for broadcast legibility
|
|
2205
|
+
const textMetrics = ctx.measureText(lines[l]);
|
|
2206
|
+
const pillW = textMetrics.width + 40;
|
|
2207
|
+
ctx.fillStyle = 'rgba(6, 9, 14, 0.72)';
|
|
2208
|
+
ctx.beginPath();
|
|
2209
|
+
ctx.roundRect((W - pillW) / 2, ly - 28, pillW, 38, 8);
|
|
2210
|
+
ctx.fill();
|
|
2211
|
+
|
|
2212
|
+
// Subtitle text with subtle glow
|
|
2213
|
+
ctx.fillStyle = '#ffffff';
|
|
2214
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
|
|
2215
|
+
ctx.shadowBlur = 12;
|
|
2216
|
+
ctx.fillText(lines[l], W / 2, ly);
|
|
2217
|
+
}
|
|
2218
|
+
ctx.restore();
|
|
2021
2219
|
}
|
|
2220
|
+
|
|
2022
2221
|
ctx.restore();
|
|
2023
2222
|
|
|
2024
2223
|
// Bottom Progress Indicator (minimal line)
|
|
@@ -2109,6 +2308,7 @@ module.exports = {
|
|
|
2109
2308
|
generateSceneCodeWithLLM,
|
|
2110
2309
|
detectAesthetic,
|
|
2111
2310
|
detectComponentTheme,
|
|
2311
|
+
detectCaptionsRequested,
|
|
2112
2312
|
getScriptFontStack,
|
|
2113
2313
|
AESTHETICS,
|
|
2114
2314
|
REGIONAL_FONTS,
|