@amg555/frameforge-mcp 2.4.0 → 2.4.2
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 +25 -0
- package/package.json +2 -1
- package/scripts/deep-validate.js +2 -2
- package/src/cli/make.js +14 -0
- package/src/mcp-server/server.js +1 -1
- package/src/mcp-server/stdio.js +28 -0
- package/src/video-renderer/scene-animator.js +814 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to FrameForge are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [2.4.2] — 2026-11-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Procedural Doodled Character Animation Engine (`src/video-renderer/scene-animator.js`)**:
|
|
11
|
+
- Pure-function-of-time character generator (`drawDoodleCharacter`) supporting expressive facial animations (talking, shivering, smiling, surprised), multiple hairstyles (wild, modern, feather), clothing types (none, pelt, shirt, cloak), and animated poses (hold_spear, shiver, sewing, dress_shirt, peace_gesture, thinker, natural).
|
|
12
|
+
- 12fps hand-drawn boiling wobble (`wb(seed, mag)`) and organic sketchy vector primitives (`sLine`, `sCircle`, `sRect`) conforming to the pure-$t$ rendering contract.
|
|
13
|
+
- 9 narrative-matched character scene themes: `doodle_dressing`, `doodle_shiver_cold`, `doodle_sewing_needle`, `doodle_hunter_savannah`, `doodle_shame_reflection`, `doodle_tribe_meeting`, `doodle_thinking_future`, `doodle_microscope_lice`, and `doodle_general_human`.
|
|
14
|
+
- Action-grounded theme detection in `detectComponentTheme`: Human narrative, evolution, psychology, and daily action scripts automatically map to physical character animations rather than abstract geometry.
|
|
15
|
+
- Enhanced LLM prompt in `generateSceneCodeWithLLM` directing AI models to animate expressive human figures actively performing narrated actions.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [2.4.1] — 2026-11-09
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **CLI & npx Invocation Handling (`src/mcp-server/stdio.js`, `package.json`)**:
|
|
23
|
+
- Registered `frameforge-make` binary alias in `package.json` so `npx --package=@amg555/frameforge-mcp frameforge-make` works immediately.
|
|
24
|
+
- Added CLI dispatch interception in `stdio.js` when flags or subcommands (`make`, `--topic`, `doctor`, `setup`, `harness`) are passed, preventing npx CLI invocations from hanging on stdio JSON-RPC stdin.
|
|
25
|
+
- **Language & Orientation Forwarding in CLI (`src/cli/make.js`)**:
|
|
26
|
+
- Added `--orientation <landscape|portrait>` and `--portrait` flags.
|
|
27
|
+
- Forwarded `language` and auto-selected `voice` (`ml-IN-MidhunNeural`, `hi-IN-MadhurNeural`, etc.) to `runAssets` and `runRender`.
|
|
28
|
+
- Forwarded `renderWidth` and `renderHeight` to `runRender` so canvas dimensions match requested orientation.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
7
32
|
## [2.4.0] — 2026-11-09
|
|
8
33
|
|
|
9
34
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amg555/frameforge-mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "FrameForge — AI video production engine for agentic IDEs. npx @amg555/frameforge-mcp. FFmpeg + TTS run on your machine. 21 MCP tools.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"main": "src/mcp-server/server.js",
|
|
9
9
|
"bin": {
|
|
10
10
|
"frameforge": "src/cli/make.js",
|
|
11
|
+
"frameforge-make": "src/cli/make.js",
|
|
11
12
|
"frameforge-mcp": "src/mcp-server/stdio.js",
|
|
12
13
|
"frameforge-setup": "src/cli/setup-keys.js",
|
|
13
14
|
"frameforge-doctor": "src/cli/doctor.js",
|
package/scripts/deep-validate.js
CHANGED
|
@@ -20,7 +20,7 @@ check('batch handling', code.includes('Array.isArray(body)'));
|
|
|
20
20
|
check('ping handler', code.includes("method === 'ping'"));
|
|
21
21
|
check('notifications/init', code.includes("notifications/initialized"));
|
|
22
22
|
check('body size limit', code.includes('MAX_BODY_BYTES'));
|
|
23
|
-
check('SERVER_INFO version 2.4.
|
|
23
|
+
check('SERVER_INFO version 2.4.2', code.includes("version: '2.4.2'"));
|
|
24
24
|
check('18 tools defined (incl get_job_status)', code.includes('get_job_status'));
|
|
25
25
|
check('export_to_editor tool defined', code.includes('export_to_editor'));
|
|
26
26
|
check('edit_video tool defined', code.includes('edit_video'));
|
|
@@ -299,7 +299,7 @@ check('cf-worker has checkRateLimit', cfWorker.includes('checkRateLimit'));
|
|
|
299
299
|
check('cf-worker hashes token (SHA-256)', cfWorker.includes('SHA-256'));
|
|
300
300
|
check('cf-worker uses RATE_LIMITS KV', cfWorker.includes('env.RATE_LIMITS'));
|
|
301
301
|
check('cf-worker returns 429 on limit', cfWorker.includes('429'));
|
|
302
|
-
check('cf-worker version 2.4.
|
|
302
|
+
check('cf-worker version 2.4.2', cfWorker.includes("'2.4.2'"));
|
|
303
303
|
check('wrangler.toml has kv_namespaces',fs.readFileSync('./cloudflare-mcp/wrangler.toml','utf8').includes('kv_namespaces'));
|
|
304
304
|
|
|
305
305
|
// ── plugins/skills present ────────────────────────────────────────────────
|
package/src/cli/make.js
CHANGED
|
@@ -98,6 +98,7 @@ function parseArgs(argv) {
|
|
|
98
98
|
mode: 'full-auto',
|
|
99
99
|
topic: '', topicsFile: '', language: 'en',
|
|
100
100
|
style: 'educational comedy', channels: ['file'],
|
|
101
|
+
orientation: 'landscape',
|
|
101
102
|
scenesFile: '', scriptFile: '', mediaDir: '',
|
|
102
103
|
shorts: true, dryRun: false,
|
|
103
104
|
noResearch: false, noScript: false, noImages: false, noTts: false, noPublish: false,
|
|
@@ -112,6 +113,8 @@ function parseArgs(argv) {
|
|
|
112
113
|
case '--script-file': args.scriptFile = argv[++i] || ''; break;
|
|
113
114
|
case '--media-dir': args.mediaDir = argv[++i] || ''; break;
|
|
114
115
|
case '--language': args.language = (argv[++i] || 'en').toLowerCase(); break;
|
|
116
|
+
case '--orientation': args.orientation = (argv[++i] || 'landscape').toLowerCase(); break;
|
|
117
|
+
case '--portrait': args.orientation = 'portrait'; break;
|
|
115
118
|
case '--style': args.style = argv[++i] || args.style; break;
|
|
116
119
|
case '--channels': args.channels = (argv[++i] || 'file').split(',').map(s => s.trim()); break;
|
|
117
120
|
case '--no-research': args.noResearch = true; break;
|
|
@@ -399,10 +402,13 @@ async function runSingleVideo(args, topic, outDir, workflows) {
|
|
|
399
402
|
// Pass stage-skip flags into scenes so runAssets can honour them
|
|
400
403
|
const preparedScenes = scenes.map(s => ({
|
|
401
404
|
...s,
|
|
405
|
+
language: s.language || args.language,
|
|
402
406
|
_skipImageGen: skipImages,
|
|
403
407
|
_skipTts: args.noTts,
|
|
404
408
|
}));
|
|
405
409
|
|
|
410
|
+
const resolvedVoice = LANGUAGE_VOICES[args.language] || null;
|
|
411
|
+
|
|
406
412
|
const assetsResult = await runAssets({
|
|
407
413
|
productionId,
|
|
408
414
|
scenes: preparedScenes,
|
|
@@ -413,17 +419,25 @@ async function runSingleVideo(args, topic, outDir, workflows) {
|
|
|
413
419
|
topic,
|
|
414
420
|
title,
|
|
415
421
|
style: args.style || '',
|
|
422
|
+
language: args.language,
|
|
423
|
+
voice: resolvedVoice,
|
|
416
424
|
});
|
|
417
425
|
log('Assets', `✅ ${assetsResult.sceneCount} scenes prepared`);
|
|
418
426
|
|
|
419
427
|
// ── Stage D: Render ────────────────────────────────────────────────────────
|
|
420
428
|
logSection('Stage D: Video Render (FFmpeg)');
|
|
429
|
+
const renderWidth = args.orientation === 'portrait' ? 1080 : 1920;
|
|
430
|
+
const renderHeight = args.orientation === 'portrait' ? 1920 : 1080;
|
|
431
|
+
|
|
421
432
|
const renderResult = await runRender({
|
|
422
433
|
productionId,
|
|
423
434
|
sceneAssets: assetsResult.sceneAssets,
|
|
424
435
|
topic,
|
|
425
436
|
title,
|
|
426
437
|
style: args.style || '',
|
|
438
|
+
language: args.language,
|
|
439
|
+
renderWidth,
|
|
440
|
+
renderHeight,
|
|
427
441
|
});
|
|
428
442
|
if (!renderResult.passed) {
|
|
429
443
|
throw new Error(`Quality gate FAILED: ${JSON.stringify(renderResult.qcReport?.checks?.filter(c => !c.passed).map(c => c.name))}`);
|
package/src/mcp-server/server.js
CHANGED
package/src/mcp-server/stdio.js
CHANGED
|
@@ -34,6 +34,34 @@
|
|
|
34
34
|
// Load config BEFORE requiring server.js so env vars are set when providers initialise
|
|
35
35
|
require('../config/load-config').loadConfig();
|
|
36
36
|
|
|
37
|
+
// ── CLI Dispatch Fallback ──────────────────────────────────────────────────
|
|
38
|
+
// When run via `npx @amg555/frameforge-mcp` with CLI arguments (e.g. `make`, `--topic`,
|
|
39
|
+
// `doctor`, `setup`, `harness`, or `--help`), dispatch to the CLI tool instead of
|
|
40
|
+
// hanging on stdin JSON-RPC.
|
|
41
|
+
const firstArg = process.argv[2];
|
|
42
|
+
if (firstArg && (firstArg === 'make' || firstArg.startsWith('-') || firstArg === 'doctor' || firstArg === 'setup' || firstArg === 'harness')) {
|
|
43
|
+
if (firstArg === 'doctor') {
|
|
44
|
+
require('../cli/doctor');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (firstArg === 'setup') {
|
|
48
|
+
require('../cli/setup-keys');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (firstArg === 'harness') {
|
|
52
|
+
const harnessBin = require('path').resolve(__dirname, '../harness/bin/harness.mjs');
|
|
53
|
+
const { spawnSync } = require('child_process');
|
|
54
|
+
const res = spawnSync(process.execPath, [harnessBin, ...process.argv.slice(3)], { stdio: 'inherit' });
|
|
55
|
+
process.exit(res.status ?? 0);
|
|
56
|
+
}
|
|
57
|
+
// Otherwise dispatch to make.js (stripping 'make' if present)
|
|
58
|
+
if (firstArg === 'make') {
|
|
59
|
+
process.argv.splice(2, 1);
|
|
60
|
+
}
|
|
61
|
+
require('../cli/make');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
37
65
|
const readline = require('readline');
|
|
38
66
|
const { handleMCPRequest } = require('./server');
|
|
39
67
|
|
|
@@ -267,6 +267,61 @@ function detectComponentTheme({ visualDescription = '', caption = '', narration
|
|
|
267
267
|
if (imagePath) return 'image';
|
|
268
268
|
const text = `${visualDescription} ${caption} ${narration} ${topic}`.toLowerCase();
|
|
269
269
|
|
|
270
|
+
// ── Doodled Character Actions (Human Stories, History, Evolution, Daily Life) ──
|
|
271
|
+
// 1. Microscope / Lice Theory / Parasite / Genetics (170,000 Years Lice Proof)
|
|
272
|
+
if (/(?:^|[^\w])(lice|louse|microscope|fiber|weave|parasite|lice evolution|genetic biology proof)(?:[^\w]|$)/i.test(text)
|
|
273
|
+
|| /പേൻ|പേൻ തിയറി|തലയിലെ പേൻ|ശരീരത്തിലെ പേൻ|ലൈസ് തിയറി|മൈക്രോസ്കോപ്പ്/.test(text)) {
|
|
274
|
+
return 'doodle_microscope_lice';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 2. Dressing / Wardrobe / Wearing Clothes / Outfit / Morning Routine / Choosing clothes
|
|
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)
|
|
284
|
+
if (/(?:^|[^\w])(shiver|shivering|cold|freeze|freezing|ice|winter|teeth|chatter|protection flaw)(?:[^\w]|$)/i.test(text)
|
|
285
|
+
|| /തണുപ്പ്|വിറയ്ക്കുക|തണുപ്പിൽ|തണുപ്പിൽ നിന്ന്|പരിക്കു|തള്ളിക്കളയുന്നു|ശരീരത്തെ സംരക്ഷിക്കാൻ|ശരീര സംരക്ഷണം|സാധാരണ ധാരണകൾ/.test(text)) {
|
|
286
|
+
return 'doodle_shiver_cold';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 4. Meeting Tribes / Social Language / Status / Community / Strangers
|
|
290
|
+
if (/(?:^|[^\w])(tribe|tribal|clan|stranger|meeting strangers|social language|status in society|hierarchy|friend or enemy)(?:[^\w]|$)/i.test(text)
|
|
291
|
+
|| /കൂട്ടമായി|സമൂഹം|അപരിചിത|ഗോത്രം|ശത്രു|സുഹൃത്ത്|സാമൂഹിക ഭാഷ|സാമൂഹിക|സ്ഥാനം തിരിച്ചറിഞ്ഞിരുന്നത്/.test(text)) {
|
|
292
|
+
return 'doodle_tribe_meeting';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 5. Future Planning / Cognitive Revolution / Tomorrow / Foresight / Campfire thinking
|
|
296
|
+
if (/(?:^|[^\w])(planning for tomorrow|future planning|cognitive revolution|thinking future|contemplate|foresight|ian gilligan)(?:[^\w]|$)/i.test(text)
|
|
297
|
+
|| /നാളത്തേക്ക്|പ്ലാൻ|ഭാവി|ഭാവിയെ കുറിച്ച്|ബുദ്ധി ഉപയോഗിച്ച്|ചിന്തയിലെ മാറ്റം|ഇയാൻ ഗില്ലിഗൻ|ചിന്തിക്കാൻ തുടങ്ങി/.test(text)) {
|
|
298
|
+
return 'doodle_thinking_future';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 6. Hunter in Savannah / Wild / Blazing Sun / Spear / Mosquitoes / Naked in Jungle
|
|
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
|
|
308
|
+
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
|
+
|| /സൂചി|സൂചിയും|തുന്ന|തുന്നാൻ|തോൽ തുന്നാൻ|തുന്നൽ|തോൽ തുന്നാനുള്ള/.test(text)) {
|
|
310
|
+
return 'doodle_sewing_needle';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 8. Shame / Mirror / Reflection / Water / Self Awareness / Theory of Mind
|
|
314
|
+
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
|
+
|| /നാണം|നാണം മറയ്ക്കാൻ|കണ്ണാടി|പ്രതിഫലനം|മറ്റുള്ളവർ എന്നെ|തിയറി ഓഫ് മൈൻഡ്|സ്വന്തം ശരീരം മറച്ചു|വ്യക്തിത്വം|ശ്രദ്ധിക്കുന്നുണ്ട്|യഥാർത്ഥ രഹസ്യം|മനഃശാസ്ത്ര/.test(text)) {
|
|
316
|
+
return 'doodle_shame_reflection';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 9. General Human Character
|
|
320
|
+
if (/(?:^|[^\w])(human|person|man|woman|people|child|guy|doodle|character|actor)(?:[^\w]|$)/i.test(text)
|
|
321
|
+
|| /മനുഷ്യൻ|ആളുകൾ|വ്യക്തി/.test(text)) {
|
|
322
|
+
return 'doodle_general_human';
|
|
323
|
+
}
|
|
324
|
+
|
|
270
325
|
// Celestial / Orbiting satellites (e.g. GPS, orbital mechanics, planetary science)
|
|
271
326
|
if (/(?:^|[^\w])(orbit|satellite|satellites|gps|planet|earth|mars|moon|galaxy|universe|solar|astronomy|gravity|space|telescope|interstellar)(?:[^\w]|$)/.test(text)
|
|
272
327
|
|| /ബഹിരാകാശ|ഗ്രഹം|ഉപഗ്രഹ|ഭൂമി|നക്ഷത്ര|പ്രപഞ്ച|अंतरिक्ष|ग्रह|तारा|விண்வெளி/.test(text)) {
|
|
@@ -694,7 +749,760 @@ function drawMainScene(t) {
|
|
|
694
749
|
const cx = W / 2;
|
|
695
750
|
const cy = H * 0.50;
|
|
696
751
|
|
|
697
|
-
|
|
752
|
+
// ── Hand-Drawn Boil Wobble & Sketch Helpers (12fps Hand-Drawn Animation) ──
|
|
753
|
+
function wb(seed, mag = 2.4) {
|
|
754
|
+
const step = Math.floor(t * 12);
|
|
755
|
+
return Math.sin(seed * 19.37 + step * 2.41) * mag;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function sLine(x1, y1, x2, y2, seed, color = '#f8fafc', width = 3.5) {
|
|
759
|
+
ctx.save();
|
|
760
|
+
ctx.strokeStyle = color;
|
|
761
|
+
ctx.lineWidth = width;
|
|
762
|
+
ctx.lineCap = 'round';
|
|
763
|
+
ctx.lineJoin = 'round';
|
|
764
|
+
const mx = (x1 + x2) / 2 + wb(seed, 3.5);
|
|
765
|
+
const my = (y1 + y2) / 2 + wb(seed + 1, 3.5);
|
|
766
|
+
ctx.beginPath();
|
|
767
|
+
ctx.moveTo(x1 + wb(seed + 2, 1.5), y1 + wb(seed + 3, 1.5));
|
|
768
|
+
ctx.quadraticCurveTo(mx, my, x2 + wb(seed + 4, 1.5), y2 + wb(seed + 5, 1.5));
|
|
769
|
+
ctx.stroke();
|
|
770
|
+
ctx.restore();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function sCircle(x, y, r, seed, color = '#f8fafc', width = 3, fillColor = null) {
|
|
774
|
+
ctx.save();
|
|
775
|
+
if (fillColor) {
|
|
776
|
+
ctx.fillStyle = fillColor;
|
|
777
|
+
ctx.beginPath();
|
|
778
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
779
|
+
ctx.fill();
|
|
780
|
+
}
|
|
781
|
+
ctx.strokeStyle = color;
|
|
782
|
+
ctx.lineWidth = width;
|
|
783
|
+
ctx.beginPath();
|
|
784
|
+
const segs = 10;
|
|
785
|
+
for (let s = 0; s <= segs; s++) {
|
|
786
|
+
const a = (s / segs) * Math.PI * 2;
|
|
787
|
+
const curR = r + wb(seed + s, 2.5);
|
|
788
|
+
const px = x + Math.cos(a) * curR;
|
|
789
|
+
const py = y + Math.sin(a) * curR;
|
|
790
|
+
if (s === 0) ctx.moveTo(px, py);
|
|
791
|
+
else ctx.lineTo(px, py);
|
|
792
|
+
}
|
|
793
|
+
ctx.closePath();
|
|
794
|
+
ctx.stroke();
|
|
795
|
+
ctx.restore();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function sRect(x, y, w, h, seed, color = '#f8fafc', width = 3, fillColor = null) {
|
|
799
|
+
ctx.save();
|
|
800
|
+
if (fillColor) {
|
|
801
|
+
ctx.fillStyle = fillColor;
|
|
802
|
+
ctx.fillRect(x, y, w, h);
|
|
803
|
+
}
|
|
804
|
+
sLine(x, y, x + w, y, seed, color, width);
|
|
805
|
+
sLine(x + w, y, x + w, y + h, seed + 1, color, width);
|
|
806
|
+
sLine(x + w, y + h, x, y + h, seed + 2, color, width);
|
|
807
|
+
sLine(x, y + h, x, y, seed + 3, color, width);
|
|
808
|
+
ctx.restore();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function drawDoodleCharacter({
|
|
812
|
+
x = cx,
|
|
813
|
+
y = cy,
|
|
814
|
+
scale = 1.0,
|
|
815
|
+
headRadius = 38,
|
|
816
|
+
torsoHeight = 90,
|
|
817
|
+
legLength = 95,
|
|
818
|
+
hairStyle = 'wild',
|
|
819
|
+
expression = 'talking',
|
|
820
|
+
clothing = 'none',
|
|
821
|
+
armPose = 'natural',
|
|
822
|
+
seed = 100,
|
|
823
|
+
} = {}) {
|
|
824
|
+
ctx.save();
|
|
825
|
+
ctx.translate(x, y);
|
|
826
|
+
ctx.scale(scale, scale);
|
|
827
|
+
|
|
828
|
+
const blink = (t % 3.2 < 0.14) ? 0.15 : 1.0;
|
|
829
|
+
const mouthTalk = Math.abs(Math.sin(t * 14)) * 4;
|
|
830
|
+
const torsoTopY = -torsoHeight;
|
|
831
|
+
const torsoBottomY = 0;
|
|
832
|
+
|
|
833
|
+
// Torso Clothing Fills
|
|
834
|
+
if (clothing === 'pelt') {
|
|
835
|
+
ctx.fillStyle = '#b45309';
|
|
836
|
+
ctx.beginPath();
|
|
837
|
+
ctx.moveTo(-24 + wb(seed, 2), torsoTopY);
|
|
838
|
+
ctx.lineTo(24 + wb(seed + 1, 2), torsoTopY);
|
|
839
|
+
ctx.lineTo(32 + wb(seed + 2, 2), torsoBottomY + 28);
|
|
840
|
+
ctx.lineTo(-28 + wb(seed + 3, 2), torsoBottomY + 28);
|
|
841
|
+
ctx.closePath();
|
|
842
|
+
ctx.fill();
|
|
843
|
+
sLine(-28, torsoBottomY + 2, 32, torsoBottomY + 2, seed + 4, '#78350f', 4);
|
|
844
|
+
} else if (clothing === 'shirt') {
|
|
845
|
+
ctx.fillStyle = ACCENT;
|
|
846
|
+
ctx.beginPath();
|
|
847
|
+
ctx.moveTo(-26 + wb(seed, 2), torsoTopY);
|
|
848
|
+
ctx.lineTo(26 + wb(seed + 1, 2), torsoTopY);
|
|
849
|
+
ctx.lineTo(28 + wb(seed + 2, 2), torsoBottomY + 12);
|
|
850
|
+
ctx.lineTo(-28 + wb(seed + 3, 2), torsoBottomY + 12);
|
|
851
|
+
ctx.closePath();
|
|
852
|
+
ctx.fill();
|
|
853
|
+
sLine(-14, torsoTopY, 0, torsoTopY + 18, seed + 4, '#ffffff', 3);
|
|
854
|
+
sLine(14, torsoTopY, 0, torsoTopY + 18, seed + 5, '#ffffff', 3);
|
|
855
|
+
sLine(0, torsoTopY + 18, 0, torsoBottomY + 10, seed + 6, '#ffffff', 2.5);
|
|
856
|
+
} else if (clothing === 'cloak') {
|
|
857
|
+
ctx.fillStyle = '#0f766e';
|
|
858
|
+
ctx.beginPath();
|
|
859
|
+
ctx.moveTo(-32, torsoTopY - 6);
|
|
860
|
+
ctx.lineTo(32, torsoTopY - 6);
|
|
861
|
+
ctx.lineTo(44, torsoBottomY + 32);
|
|
862
|
+
ctx.lineTo(-44, torsoBottomY + 32);
|
|
863
|
+
ctx.closePath();
|
|
864
|
+
ctx.fill();
|
|
865
|
+
} else {
|
|
866
|
+
sLine(0, torsoTopY, 0, torsoBottomY, seed + 7, '#fcd34d', 4);
|
|
867
|
+
sLine(-15, torsoTopY + 30, -2, torsoTopY + 40, seed + 8, 'rgba(255,255,255,0.3)', 2);
|
|
868
|
+
sLine(15, torsoTopY + 30, 2, torsoTopY + 40, seed + 9, 'rgba(255,255,255,0.3)', 2);
|
|
869
|
+
}
|
|
870
|
+
sLine(0, torsoTopY, 0, torsoBottomY, seed + 10, '#f8fafc', 4);
|
|
871
|
+
|
|
872
|
+
// Head
|
|
873
|
+
const headCenterY = torsoTopY - headRadius - 6 + wb(seed + 11, 1.5);
|
|
874
|
+
sCircle(0, headCenterY, headRadius, seed + 12, '#f8fafc', 3.5, '#fef3c7');
|
|
875
|
+
|
|
876
|
+
// Hair
|
|
877
|
+
if (hairStyle === 'wild') {
|
|
878
|
+
for (let h = 0; h < 6; h++) {
|
|
879
|
+
const hairAng = -Math.PI * 0.85 + (h / 5) * Math.PI * 0.7;
|
|
880
|
+
const hx1 = Math.cos(hairAng) * headRadius;
|
|
881
|
+
const hy1 = headCenterY + Math.sin(hairAng) * headRadius;
|
|
882
|
+
const hx2 = Math.cos(hairAng) * (headRadius + 22 + wb(seed + 13 + h, 4));
|
|
883
|
+
const hy2 = headCenterY + Math.sin(hairAng) * (headRadius + 22 + wb(seed + 14 + h, 4));
|
|
884
|
+
sLine(hx1, hy1, hx2, hy2, seed + 15 + h, '#78350f', 3.5);
|
|
885
|
+
}
|
|
886
|
+
} else if (hairStyle === 'modern') {
|
|
887
|
+
ctx.fillStyle = '#1e293b';
|
|
888
|
+
ctx.beginPath();
|
|
889
|
+
ctx.arc(0, headCenterY - 12, headRadius * 0.96, Math.PI, Math.PI * 2);
|
|
890
|
+
ctx.closePath();
|
|
891
|
+
ctx.fill();
|
|
892
|
+
sLine(-headRadius * 0.9, headCenterY - 12, headRadius * 0.9, headCenterY - 12, seed + 20, '#1e293b', 4);
|
|
893
|
+
} else if (hairStyle === 'feather') {
|
|
894
|
+
sLine(8, headCenterY - headRadius, 28, headCenterY - headRadius - 35, seed + 21, '#38bdf8', 4);
|
|
895
|
+
sLine(28, headCenterY - headRadius - 35, 18, headCenterY - headRadius - 20, seed + 22, '#f43f5e', 3);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Eyes
|
|
899
|
+
const eyeY = headCenterY + 4;
|
|
900
|
+
const eyeR = 4 * blink;
|
|
901
|
+
sCircle(-12, eyeY, Math.max(1, eyeR), seed + 23, '#0f172a', 2, '#0f172a');
|
|
902
|
+
sCircle(12, eyeY, Math.max(1, eyeR), seed + 24, '#0f172a', 2, '#0f172a');
|
|
903
|
+
|
|
904
|
+
// Eyebrows
|
|
905
|
+
if (expression === 'shivering') {
|
|
906
|
+
sLine(-18, eyeY - 12 + wb(seed, 2), -6, eyeY - 7 + wb(seed, 2), seed + 25, '#0f172a', 2.5);
|
|
907
|
+
sLine(6, eyeY - 7 + wb(seed, 2), 18, eyeY - 12 + wb(seed, 2), seed + 26, '#0f172a', 2.5);
|
|
908
|
+
} else if (expression === 'surprised' || expression === 'curious') {
|
|
909
|
+
sLine(-18, eyeY - 15, -6, eyeY - 13, seed + 27, '#0f172a', 2.5);
|
|
910
|
+
sLine(6, eyeY - 13, 18, eyeY - 15, seed + 28, '#0f172a', 2.5);
|
|
911
|
+
} else {
|
|
912
|
+
sLine(-17, eyeY - 9, -7, eyeY - 10, seed + 29, '#0f172a', 2.5);
|
|
913
|
+
sLine(7, eyeY - 10, 17, eyeY - 9, seed + 30, '#0f172a', 2.5);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Mouth
|
|
917
|
+
const mouthY = headCenterY + 22;
|
|
918
|
+
if (expression === 'shivering') {
|
|
919
|
+
const mChatter = Math.sin(t * 32) * 2.5;
|
|
920
|
+
sLine(-10, mouthY + mChatter, -4, mouthY - 3 + mChatter, seed + 31, '#0f172a', 2.5);
|
|
921
|
+
sLine(-4, mouthY - 3 + mChatter, 3, mouthY + 3 + mChatter, seed + 32, '#0f172a', 2.5);
|
|
922
|
+
sLine(3, mouthY + 3 + mChatter, 10, mouthY + mChatter, seed + 33, '#0f172a', 2.5);
|
|
923
|
+
} else if (expression === 'talking') {
|
|
924
|
+
ctx.fillStyle = '#0f172a';
|
|
925
|
+
ctx.beginPath();
|
|
926
|
+
ctx.ellipse(0, mouthY, 7, Math.max(2, mouthTalk), 0, 0, Math.PI * 2);
|
|
927
|
+
ctx.fill();
|
|
928
|
+
} else if (expression === 'smiling') {
|
|
929
|
+
ctx.beginPath();
|
|
930
|
+
ctx.arc(0, mouthY - 4, 10, 0.2 * Math.PI, 0.8 * Math.PI);
|
|
931
|
+
ctx.strokeStyle = '#0f172a';
|
|
932
|
+
ctx.lineWidth = 2.5;
|
|
933
|
+
ctx.stroke();
|
|
934
|
+
} else {
|
|
935
|
+
sLine(-8, mouthY, 8, mouthY, seed + 34, '#0f172a', 2.5);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Legs
|
|
939
|
+
const hipY = torsoBottomY;
|
|
940
|
+
if (armPose === 'sewing' || armPose === 'thinker') {
|
|
941
|
+
sLine(-10, hipY, -45, hipY + 45, seed + 35, '#f8fafc', 4);
|
|
942
|
+
sLine(-45, hipY + 45, 10, hipY + 50, seed + 36, '#f8fafc', 4);
|
|
943
|
+
sLine(10, hipY, 45, hipY + 45, seed + 37, '#f8fafc', 4);
|
|
944
|
+
sLine(45, hipY + 45, -10, hipY + 50, seed + 38, '#f8fafc', 4);
|
|
945
|
+
} else if (armPose === 'hold_spear') {
|
|
946
|
+
const legSwing = Math.sin(t * 5.5);
|
|
947
|
+
const l1KneeX = -15 + legSwing * 22;
|
|
948
|
+
const l1KneeY = hipY + legLength * 0.55;
|
|
949
|
+
const l1FootX = -15 + legSwing * 38;
|
|
950
|
+
const l1FootY = hipY + legLength;
|
|
951
|
+
sLine(-10, hipY, l1KneeX, l1KneeY, seed + 39, '#f8fafc', 4);
|
|
952
|
+
sLine(l1KneeX, l1KneeY, l1FootX, l1FootY, seed + 40, '#f8fafc', 4);
|
|
953
|
+
|
|
954
|
+
const l2KneeX = 15 - legSwing * 22;
|
|
955
|
+
const l2KneeY = hipY + legLength * 0.55;
|
|
956
|
+
const l2FootX = 15 - legSwing * 38;
|
|
957
|
+
const l2FootY = hipY + legLength;
|
|
958
|
+
sLine(10, hipY, l2KneeX, l2KneeY, seed + 41, '#f8fafc', 4);
|
|
959
|
+
sLine(l2KneeX, l2KneeY, l2FootX, l2FootY, seed + 42, '#f8fafc', 4);
|
|
960
|
+
} else {
|
|
961
|
+
sLine(-12, hipY, -14, hipY + legLength, seed + 43, '#f8fafc', 4);
|
|
962
|
+
sLine(12, hipY, 14, hipY + legLength, seed + 44, '#f8fafc', 4);
|
|
963
|
+
sLine(-14, hipY + legLength, -24, hipY + legLength, seed + 45, '#f8fafc', 3.5);
|
|
964
|
+
sLine(14, hipY + legLength, 24, hipY + legLength, seed + 46, '#f8fafc', 3.5);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Arms & Hands
|
|
968
|
+
const shoulderY = torsoTopY + 12;
|
|
969
|
+
if (armPose === 'shiver') {
|
|
970
|
+
const shivA = Math.sin(t * 34) * 3;
|
|
971
|
+
sLine(-20, shoulderY, 16 + shivA, shoulderY + 35, seed + 47, '#f8fafc', 4);
|
|
972
|
+
sLine(20, shoulderY, -16 + shivA, shoulderY + 35, seed + 48, '#f8fafc', 4);
|
|
973
|
+
} else if (armPose === 'hold_spear') {
|
|
974
|
+
const spearX = -45;
|
|
975
|
+
sLine(-18, shoulderY, -35, shoulderY + 30, seed + 49, '#f8fafc', 4);
|
|
976
|
+
sLine(-35, shoulderY + 30, spearX, shoulderY + 5, seed + 50, '#f8fafc', 4);
|
|
977
|
+
sLine(spearX, -torsoHeight - 130, spearX, hipY + legLength + 10, seed + 51, '#92400e', 5);
|
|
978
|
+
ctx.fillStyle = '#64748b';
|
|
979
|
+
ctx.beginPath();
|
|
980
|
+
ctx.moveTo(spearX, -torsoHeight - 165);
|
|
981
|
+
ctx.lineTo(spearX - 10, -torsoHeight - 128);
|
|
982
|
+
ctx.lineTo(spearX + 10, -torsoHeight - 128);
|
|
983
|
+
ctx.closePath();
|
|
984
|
+
ctx.fill();
|
|
985
|
+
sLine(18, shoulderY, 35, shoulderY + 25 + Math.sin(t * 5.5) * 20, seed + 52, '#f8fafc', 4);
|
|
986
|
+
sLine(35, shoulderY + 25 + Math.sin(t * 5.5) * 20, 48, shoulderY + 45, seed + 53, '#f8fafc', 4);
|
|
987
|
+
} else if (armPose === 'sewing') {
|
|
988
|
+
sLine(-18, shoulderY, -35, shoulderY + 40, seed + 54, '#f8fafc', 4);
|
|
989
|
+
sLine(-35, shoulderY + 40, -18, shoulderY + 55, seed + 55, '#f8fafc', 4);
|
|
990
|
+
const sewCycle = Math.sin(t * 4);
|
|
991
|
+
const handX = 20 + sewCycle * 25;
|
|
992
|
+
const handY = shoulderY + 45 - Math.cos(t * 4) * 15;
|
|
993
|
+
sLine(18, shoulderY, 38, shoulderY + 25, seed + 56, '#f8fafc', 4);
|
|
994
|
+
sLine(38, shoulderY + 25, handX, handY, seed + 57, '#f8fafc', 4);
|
|
995
|
+
sLine(handX - 5, handY - 10, handX + 18, handY + 12, seed + 58, '#fef08a', 3.5);
|
|
996
|
+
ctx.strokeStyle = '#ffffff';
|
|
997
|
+
ctx.lineWidth = 1.5;
|
|
998
|
+
ctx.beginPath();
|
|
999
|
+
ctx.moveTo(handX - 5, handY - 10);
|
|
1000
|
+
ctx.quadraticCurveTo(handX - 25, handY - 25, 0, hipY + 10);
|
|
1001
|
+
ctx.stroke();
|
|
1002
|
+
} else if (armPose === 'cover_body') {
|
|
1003
|
+
sLine(-18, shoulderY, -12, hipY - 10, seed + 59, '#f8fafc', 4);
|
|
1004
|
+
sLine(18, shoulderY, 12, hipY - 10, seed + 60, '#f8fafc', 4);
|
|
1005
|
+
} else if (armPose === 'dress_shirt') {
|
|
1006
|
+
sLine(-18, shoulderY, -38, shoulderY + 30, seed + 61, '#f8fafc', 4);
|
|
1007
|
+
sLine(-38, shoulderY + 30, -22, hipY - 5, seed + 62, '#f8fafc', 4);
|
|
1008
|
+
const adjustY = shoulderY + 8 + Math.sin(t * 3) * 6;
|
|
1009
|
+
sLine(18, shoulderY, 35, shoulderY + 15, seed + 63, '#f8fafc', 4);
|
|
1010
|
+
sLine(35, shoulderY + 15, 10, adjustY, seed + 64, '#f8fafc', 4);
|
|
1011
|
+
} else if (armPose === 'peace_gesture') {
|
|
1012
|
+
sLine(-18, shoulderY, -26, shoulderY + 40, seed + 65, '#f8fafc', 4);
|
|
1013
|
+
sLine(18, shoulderY, 40, shoulderY - 10 + Math.sin(t * 2) * 5, seed + 66, '#f8fafc', 4);
|
|
1014
|
+
sLine(40, shoulderY - 10 + Math.sin(t * 2) * 5, 45, shoulderY - 35, seed + 67, '#f8fafc', 4);
|
|
1015
|
+
} else if (armPose === 'thinker') {
|
|
1016
|
+
sLine(18, shoulderY, 36, hipY + 15, seed + 68, '#f8fafc', 4);
|
|
1017
|
+
sLine(36, hipY + 15, 8, headCenterY + 18, seed + 69, '#f8fafc', 4);
|
|
1018
|
+
sLine(-18, shoulderY, -30, hipY + 25, seed + 70, '#f8fafc', 4);
|
|
1019
|
+
} else {
|
|
1020
|
+
const gArm = Math.sin(t * 2.5) * 12;
|
|
1021
|
+
sLine(-18, shoulderY, -35, shoulderY + 35, seed + 71, '#f8fafc', 4);
|
|
1022
|
+
sLine(-35, shoulderY + 35, -42, shoulderY + 60, seed + 72, '#f8fafc', 4);
|
|
1023
|
+
sLine(18, shoulderY, 35, shoulderY + 20 - gArm, seed + 73, '#f8fafc', 4);
|
|
1024
|
+
sLine(35, shoulderY + 20 - gArm, 45, shoulderY + 5 - gArm, seed + 74, '#f8fafc', 4);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
ctx.restore();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ── Doodled Character Narrative Themes ────────────────────────────────────
|
|
1031
|
+
if (COMP_THEME === 'doodle_dressing') {
|
|
1032
|
+
// Modern Dressing Room & Mirror Reflection
|
|
1033
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.25)', 2.5);
|
|
1034
|
+
|
|
1035
|
+
// Clothes rack on left
|
|
1036
|
+
sLine(cx - 360, cy + 220, cx - 360, cy - 140, 11, '#94a3b8', 5);
|
|
1037
|
+
sLine(cx - 410, cy + 220, cx - 310, cy + 220, 12, '#94a3b8', 5);
|
|
1038
|
+
sLine(cx - 420, cy - 140, cx - 180, cy - 140, 13, '#cbd5e1', 5);
|
|
1039
|
+
|
|
1040
|
+
// 3 shirts on hangers
|
|
1041
|
+
const shirtColors = ['#38bdf8', '#f43f5e', '#fbbf24'];
|
|
1042
|
+
for (let c = 0; c < 3; c++) {
|
|
1043
|
+
const hx = cx - 380 + c * 80;
|
|
1044
|
+
const sway = Math.sin(t * 2.5 + c * 1.5) * 5;
|
|
1045
|
+
sLine(hx, cy - 140, hx + sway, cy - 110, 20 + c, '#e2e8f0', 2.5);
|
|
1046
|
+
ctx.fillStyle = shirtColors[c];
|
|
1047
|
+
ctx.beginPath();
|
|
1048
|
+
ctx.moveTo(hx + sway - 22, cy - 110);
|
|
1049
|
+
ctx.lineTo(hx + sway + 22, cy - 110);
|
|
1050
|
+
ctx.lineTo(hx + sway + 28, cy - 10);
|
|
1051
|
+
ctx.lineTo(hx + sway - 28, cy - 10);
|
|
1052
|
+
ctx.closePath();
|
|
1053
|
+
ctx.fill();
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Mirror on right
|
|
1057
|
+
const mirrorX = cx + 320;
|
|
1058
|
+
const mirrorY = cy + 20;
|
|
1059
|
+
ctx.fillStyle = 'rgba(56, 189, 248, 0.08)';
|
|
1060
|
+
ctx.beginPath();
|
|
1061
|
+
ctx.roundRect(mirrorX - 85, mirrorY - 210, 170, 390, 24);
|
|
1062
|
+
ctx.fill();
|
|
1063
|
+
ctx.strokeStyle = '#d4af37';
|
|
1064
|
+
ctx.lineWidth = 4;
|
|
1065
|
+
ctx.stroke();
|
|
1066
|
+
|
|
1067
|
+
// Reflected character in mirror (faint glow)
|
|
1068
|
+
ctx.save();
|
|
1069
|
+
ctx.globalAlpha = 0.55;
|
|
1070
|
+
drawDoodleCharacter({
|
|
1071
|
+
x: mirrorX,
|
|
1072
|
+
y: cy + 120,
|
|
1073
|
+
scale: 1.05,
|
|
1074
|
+
hairStyle: 'modern',
|
|
1075
|
+
expression: 'smiling',
|
|
1076
|
+
clothing: 'shirt',
|
|
1077
|
+
armPose: 'dress_shirt',
|
|
1078
|
+
seed: 88,
|
|
1079
|
+
});
|
|
1080
|
+
ctx.restore();
|
|
1081
|
+
|
|
1082
|
+
// Main character in center
|
|
1083
|
+
drawDoodleCharacter({
|
|
1084
|
+
x: cx - 40,
|
|
1085
|
+
y: cy + 140,
|
|
1086
|
+
scale: 1.35,
|
|
1087
|
+
hairStyle: 'modern',
|
|
1088
|
+
expression: 'talking',
|
|
1089
|
+
clothing: 'shirt',
|
|
1090
|
+
armPose: 'dress_shirt',
|
|
1091
|
+
seed: 101,
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
} else if (COMP_THEME === 'doodle_shiver_cold') {
|
|
1095
|
+
// Freezing Wind & Shivering Early Human
|
|
1096
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1097
|
+
|
|
1098
|
+
// Freezing wind streaks rushing across
|
|
1099
|
+
for (let w = 0; w < 12; w++) {
|
|
1100
|
+
const windX = ((t * 700 + w * 220) % (W + 600)) - 300;
|
|
1101
|
+
const windY = cy - 250 + (w * 45) + Math.sin(t * 3 + w) * 15;
|
|
1102
|
+
sLine(windX, windY, windX + 160, windY + 5, 200 + w, 'rgba(255,255,255,0.3)', 2);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Drifting snow particles
|
|
1106
|
+
for (let p = 0; p < 25; p++) {
|
|
1107
|
+
const px = ((p * 85 + t * 450) % W);
|
|
1108
|
+
const py = (p * 45 + t * 90) % H;
|
|
1109
|
+
sCircle(px, py, 2.5, 300 + p, '#e0f2fe', 1, '#ffffff');
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Shivering breath vapor cloud
|
|
1113
|
+
const breathPhase = (t * 2.2) % 1;
|
|
1114
|
+
const bCloudX = cx + 25 + breathPhase * 60;
|
|
1115
|
+
const bCloudY = cy - 90 - breathPhase * 25;
|
|
1116
|
+
ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.5) + ')';
|
|
1117
|
+
ctx.beginPath();
|
|
1118
|
+
ctx.arc(bCloudX, bCloudY, 15 + breathPhase * 25, 0, Math.PI * 2);
|
|
1119
|
+
ctx.fill();
|
|
1120
|
+
|
|
1121
|
+
// Main shivering character
|
|
1122
|
+
const vibX = Math.sin(t * 38) * 4.5;
|
|
1123
|
+
const vibY = Math.cos(t * 32) * 2;
|
|
1124
|
+
drawDoodleCharacter({
|
|
1125
|
+
x: cx + vibX,
|
|
1126
|
+
y: cy + 140 + vibY,
|
|
1127
|
+
scale: 1.35,
|
|
1128
|
+
hairStyle: 'wild',
|
|
1129
|
+
expression: 'shivering',
|
|
1130
|
+
clothing: 'none',
|
|
1131
|
+
armPose: 'shiver',
|
|
1132
|
+
seed: 102,
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// Big crossed out cold icon on right
|
|
1136
|
+
sCircle(cx + 340, cy - 80, 55, 400, 'rgba(239,68,68,0.85)', 4);
|
|
1137
|
+
sLine(cx + 300, cy - 120, cx + 380, cy - 40, 401, 'rgba(239,68,68,0.95)', 4.5);
|
|
1138
|
+
ctx.font = 'bold 22px monospace';
|
|
1139
|
+
ctx.fillStyle = '#ef4444';
|
|
1140
|
+
ctx.textAlign = 'center';
|
|
1141
|
+
ctx.fillText('NOT FOR COLD', cx + 340, cy - 5);
|
|
1142
|
+
|
|
1143
|
+
} else if (COMP_THEME === 'doodle_hunter_savannah') {
|
|
1144
|
+
// Blazing African Savannah Hunter with Spear
|
|
1145
|
+
// Blazing midday sun
|
|
1146
|
+
const sunR = 75 + Math.sin(t * 2) * 6;
|
|
1147
|
+
ctx.fillStyle = '#f59e0b';
|
|
1148
|
+
ctx.beginPath();
|
|
1149
|
+
ctx.arc(cx - 380, cy - 180, sunR, 0, Math.PI * 2);
|
|
1150
|
+
ctx.fill();
|
|
1151
|
+
for (let r = 0; r < 8; r++) {
|
|
1152
|
+
const rayA = (r / 8) * Math.PI * 2 + t * 0.4;
|
|
1153
|
+
const rx1 = cx - 380 + Math.cos(rayA) * (sunR + 10);
|
|
1154
|
+
const ry1 = cy - 180 + Math.sin(rayA) * (sunR + 10);
|
|
1155
|
+
const rx2 = cx - 380 + Math.cos(rayA) * (sunR + 32);
|
|
1156
|
+
const ry2 = cy - 180 + Math.sin(rayA) * (sunR + 32);
|
|
1157
|
+
sLine(rx1, ry1, rx2, ry2, 450 + r, '#fbbf24', 3);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Ground and tall swaying savannah grass
|
|
1161
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, '#78350f', 3);
|
|
1162
|
+
for (let g = 0; g < 32; g++) {
|
|
1163
|
+
const gx = cx - 550 + g * 36;
|
|
1164
|
+
const sway = Math.sin(t * 3.2 + g * 0.4) * 16;
|
|
1165
|
+
sLine(gx, cy + 220, gx + sway, cy + 140, 500 + g, '#d97706', 2.5);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Buzzing mosquitoes orbiting
|
|
1169
|
+
for (let m = 0; m < 3; m++) {
|
|
1170
|
+
const mx = cx + 40 + Math.sin(t * 8 + m * 2) * 65;
|
|
1171
|
+
const my = cy - 80 + Math.cos(t * 7 + m * 2) * 45;
|
|
1172
|
+
sCircle(mx, my, 2, 550 + m, '#0f172a', 1.5, '#ef4444');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Walking early hunter
|
|
1176
|
+
drawDoodleCharacter({
|
|
1177
|
+
x: cx - 40,
|
|
1178
|
+
y: cy + 140,
|
|
1179
|
+
scale: 1.35,
|
|
1180
|
+
hairStyle: 'wild',
|
|
1181
|
+
expression: 'talking',
|
|
1182
|
+
clothing: 'none',
|
|
1183
|
+
armPose: 'hold_spear',
|
|
1184
|
+
seed: 103,
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
} else if (COMP_THEME === 'doodle_sewing_needle') {
|
|
1188
|
+
// Prehistoric Campfire & Bone Needle Sewing
|
|
1189
|
+
// Campfire with animated flames and sparks
|
|
1190
|
+
const fireX = cx - 380;
|
|
1191
|
+
const fireY = cy + 180;
|
|
1192
|
+
for (let f = 0; f < 5; f++) {
|
|
1193
|
+
const flH = 50 + Math.sin(t * 12 + f) * 22;
|
|
1194
|
+
ctx.fillStyle = f % 2 === 0 ? '#ef4444' : '#f59e0b';
|
|
1195
|
+
ctx.beginPath();
|
|
1196
|
+
ctx.moveTo(fireX - 30 + f * 12, fireY);
|
|
1197
|
+
ctx.quadraticCurveTo(fireX + (f - 2) * 8, fireY - flH, fireX - 10 + f * 12, fireY);
|
|
1198
|
+
ctx.fill();
|
|
1199
|
+
}
|
|
1200
|
+
// Floating fire sparks
|
|
1201
|
+
for (let sp = 0; sp < 8; sp++) {
|
|
1202
|
+
const sx = fireX + Math.sin(t * 4 + sp) * 35;
|
|
1203
|
+
const sy = fireY - 30 - ((t * 80 + sp * 35) % 140);
|
|
1204
|
+
sCircle(sx, sy, 2, 600 + sp, '#fbbf24', 1, '#fbbf24');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Sitting craftsman with hide on lap
|
|
1208
|
+
drawDoodleCharacter({
|
|
1209
|
+
x: cx - 110,
|
|
1210
|
+
y: cy + 140,
|
|
1211
|
+
scale: 1.35,
|
|
1212
|
+
hairStyle: 'wild',
|
|
1213
|
+
expression: 'talking',
|
|
1214
|
+
clothing: 'pelt',
|
|
1215
|
+
armPose: 'sewing',
|
|
1216
|
+
seed: 104,
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
// Zoomed-in bone needle microscope circle on right
|
|
1220
|
+
const lensX = cx + 280;
|
|
1221
|
+
const lensY = cy - 10;
|
|
1222
|
+
const lensR = 145;
|
|
1223
|
+
ctx.fillStyle = 'rgba(15, 23, 42, 0.9)';
|
|
1224
|
+
ctx.beginPath();
|
|
1225
|
+
ctx.arc(lensX, lensY, lensR, 0, Math.PI * 2);
|
|
1226
|
+
ctx.fill();
|
|
1227
|
+
ctx.strokeStyle = '#d4af37';
|
|
1228
|
+
ctx.lineWidth = 5;
|
|
1229
|
+
ctx.stroke();
|
|
1230
|
+
|
|
1231
|
+
// Two thick leather pelt pieces being stitched
|
|
1232
|
+
sLine(lensX - 90, lensY - 20, lensX + 90, lensY - 20, 650, '#92400e', 8);
|
|
1233
|
+
sLine(lensX - 90, lensY + 20, lensX + 90, lensY + 20, 651, '#78350f', 8);
|
|
1234
|
+
|
|
1235
|
+
// Drilled bone needle piercing through
|
|
1236
|
+
const nWiggle = Math.sin(t * 4) * 20;
|
|
1237
|
+
sLine(lensX - 45 + nWiggle, lensY - 65, lensX + 25 + nWiggle, lensY + 55, 652, '#fef08a', 6);
|
|
1238
|
+
sCircle(lensX - 40 + nWiggle, lensY - 58, 4, 653, '#0f172a', 1.5, '#0f172a'); // Eyelet
|
|
1239
|
+
|
|
1240
|
+
// Stitches
|
|
1241
|
+
for (let st = 0; st < 6; st++) {
|
|
1242
|
+
const stX = lensX - 70 + st * 28;
|
|
1243
|
+
sLine(stX, lensY - 22, stX + 8, lensY + 22, 660 + st, '#ffffff', 3.5);
|
|
1244
|
+
}
|
|
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
|
+
|
|
1250
|
+
} else if (COMP_THEME === 'doodle_shame_reflection') {
|
|
1251
|
+
// Water Pool Reflection & Theory of Mind
|
|
1252
|
+
const poolX = cx;
|
|
1253
|
+
const poolY = cy + 180;
|
|
1254
|
+
ctx.fillStyle = 'rgba(14, 165, 233, 0.15)';
|
|
1255
|
+
ctx.beginPath();
|
|
1256
|
+
ctx.ellipse(poolX, poolY, 260, 60, 0, 0, Math.PI * 2);
|
|
1257
|
+
ctx.fill();
|
|
1258
|
+
ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)';
|
|
1259
|
+
ctx.lineWidth = 2.5;
|
|
1260
|
+
ctx.stroke();
|
|
1261
|
+
|
|
1262
|
+
// Ripples in pool
|
|
1263
|
+
for (let rip = 1; rip <= 3; rip++) {
|
|
1264
|
+
const ripR = (rip * 65 + t * 45) % 250;
|
|
1265
|
+
const ripAlpha = (1 - ripR / 250) * 0.4;
|
|
1266
|
+
ctx.strokeStyle = 'rgba(56, 189, 248, ' + ripAlpha + ')';
|
|
1267
|
+
ctx.beginPath();
|
|
1268
|
+
ctx.ellipse(poolX, poolY, ripR, ripR * 0.23, 0, 0, Math.PI * 2);
|
|
1269
|
+
ctx.stroke();
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Inverted rippling reflection of character in water
|
|
1273
|
+
ctx.save();
|
|
1274
|
+
ctx.globalAlpha = 0.35;
|
|
1275
|
+
ctx.translate(cx, poolY + 20);
|
|
1276
|
+
ctx.scale(1.2, -0.75);
|
|
1277
|
+
drawDoodleCharacter({
|
|
1278
|
+
x: 0,
|
|
1279
|
+
y: 0,
|
|
1280
|
+
scale: 1.0,
|
|
1281
|
+
hairStyle: 'wild',
|
|
1282
|
+
expression: 'surprised',
|
|
1283
|
+
clothing: 'none',
|
|
1284
|
+
armPose: 'cover_body',
|
|
1285
|
+
seed: 700,
|
|
1286
|
+
});
|
|
1287
|
+
ctx.restore();
|
|
1288
|
+
|
|
1289
|
+
// Standing character looking down and covering body
|
|
1290
|
+
drawDoodleCharacter({
|
|
1291
|
+
x: cx,
|
|
1292
|
+
y: cy + 120,
|
|
1293
|
+
scale: 1.35,
|
|
1294
|
+
hairStyle: 'wild',
|
|
1295
|
+
expression: 'surprised',
|
|
1296
|
+
clothing: 'none',
|
|
1297
|
+
armPose: 'cover_body',
|
|
1298
|
+
seed: 105,
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// Blushing cheeks
|
|
1302
|
+
sCircle(cx - 18, cy - 10, 7, 750, 'rgba(244,63,94,0.6)', 1, 'rgba(244,63,94,0.6)');
|
|
1303
|
+
sCircle(cx + 18, cy - 10, 7, 751, 'rgba(244,63,94,0.6)', 1, 'rgba(244,63,94,0.6)');
|
|
1304
|
+
|
|
1305
|
+
// Overhead Theory of Mind Thought Halo
|
|
1306
|
+
const haloX = cx + 180;
|
|
1307
|
+
const haloY = cy - 120;
|
|
1308
|
+
sCircle(haloX, haloY, 55, 760, '#38bdf8', 3);
|
|
1309
|
+
// Eye icon inside halo
|
|
1310
|
+
sCircle(haloX, haloY, 20, 761, '#ffffff', 2.5);
|
|
1311
|
+
sCircle(haloX, haloY, 8, 762, '#38bdf8', 1, '#38bdf8');
|
|
1312
|
+
ctx.font = 'bold 18px monospace';
|
|
1313
|
+
ctx.fillStyle = '#38bdf8';
|
|
1314
|
+
ctx.textAlign = 'center';
|
|
1315
|
+
ctx.fillText('THEORY OF MIND', haloX, haloY + 78);
|
|
1316
|
+
|
|
1317
|
+
} else if (COMP_THEME === 'doodle_tribe_meeting') {
|
|
1318
|
+
// Two Clans Meeting & Communicating via Clothing Language
|
|
1319
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1320
|
+
|
|
1321
|
+
// Left Clan Member (Mountain Clan with Pelt & Peace Gesture)
|
|
1322
|
+
drawDoodleCharacter({
|
|
1323
|
+
x: cx - 220,
|
|
1324
|
+
y: cy + 140,
|
|
1325
|
+
scale: 1.3,
|
|
1326
|
+
hairStyle: 'wild',
|
|
1327
|
+
expression: 'talking',
|
|
1328
|
+
clothing: 'pelt',
|
|
1329
|
+
armPose: 'peace_gesture',
|
|
1330
|
+
seed: 106,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
// Right Clan Member (River Clan with Cloak & Feather)
|
|
1334
|
+
drawDoodleCharacter({
|
|
1335
|
+
x: cx + 220,
|
|
1336
|
+
y: cy + 140,
|
|
1337
|
+
scale: 1.3,
|
|
1338
|
+
hairStyle: 'feather',
|
|
1339
|
+
expression: 'curious',
|
|
1340
|
+
clothing: 'cloak',
|
|
1341
|
+
armPose: 'natural',
|
|
1342
|
+
seed: 107,
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
// Glowing connection arc and communication symbols between them
|
|
1346
|
+
ctx.strokeStyle = ACCENT;
|
|
1347
|
+
ctx.lineWidth = 3;
|
|
1348
|
+
ctx.setLineDash([8, 8]);
|
|
1349
|
+
ctx.beginPath();
|
|
1350
|
+
ctx.moveTo(cx - 160, cy);
|
|
1351
|
+
ctx.quadraticCurveTo(cx, cy - 100, cx + 160, cy);
|
|
1352
|
+
ctx.stroke();
|
|
1353
|
+
ctx.setLineDash([]);
|
|
1354
|
+
|
|
1355
|
+
// Central status emblem
|
|
1356
|
+
sCircle(cx, cy - 65, 36, 800, ACCENT, 3, 'rgba(15, 23, 42, 0.85)');
|
|
1357
|
+
ctx.font = 'bold 24px monospace';
|
|
1358
|
+
ctx.fillStyle = ACCENT;
|
|
1359
|
+
ctx.textAlign = 'center';
|
|
1360
|
+
ctx.fillText('🤝', cx, cy - 56);
|
|
1361
|
+
ctx.font = 'bold 18px monospace';
|
|
1362
|
+
ctx.fillText('CLOTHING AS SOCIAL LANGUAGE', cx, cy + 15);
|
|
1363
|
+
|
|
1364
|
+
} else if (COMP_THEME === 'doodle_thinking_future') {
|
|
1365
|
+
// Prehistoric Thinker & Planning for Tomorrow
|
|
1366
|
+
// Starlit night and crescent moon
|
|
1367
|
+
ctx.fillStyle = '#fef08a';
|
|
1368
|
+
ctx.beginPath();
|
|
1369
|
+
ctx.arc(cx - 360, cy - 190, 50, 0, Math.PI * 2);
|
|
1370
|
+
ctx.fill();
|
|
1371
|
+
ctx.fillStyle = BG2;
|
|
1372
|
+
ctx.beginPath();
|
|
1373
|
+
ctx.arc(cx - 345, cy - 200, 48, 0, Math.PI * 2);
|
|
1374
|
+
ctx.fill();
|
|
1375
|
+
|
|
1376
|
+
// Large rock boulder for thinker
|
|
1377
|
+
ctx.fillStyle = '#334155';
|
|
1378
|
+
ctx.beginPath();
|
|
1379
|
+
ctx.ellipse(cx - 140, cy + 175, 85, 55, 0, 0, Math.PI * 2);
|
|
1380
|
+
ctx.fill();
|
|
1381
|
+
|
|
1382
|
+
// Thinker seated on rock
|
|
1383
|
+
drawDoodleCharacter({
|
|
1384
|
+
x: cx - 140,
|
|
1385
|
+
y: cy + 130,
|
|
1386
|
+
scale: 1.35,
|
|
1387
|
+
hairStyle: 'wild',
|
|
1388
|
+
expression: 'talking',
|
|
1389
|
+
clothing: 'pelt',
|
|
1390
|
+
armPose: 'thinker',
|
|
1391
|
+
seed: 108,
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
// Expanding thought cloud overhead
|
|
1395
|
+
const cloudX = cx + 180;
|
|
1396
|
+
const cloudY = cy - 80;
|
|
1397
|
+
const cloudScale = spring(t, 0.15, 170, 14);
|
|
1398
|
+
|
|
1399
|
+
ctx.save();
|
|
1400
|
+
ctx.translate(cloudX, cloudY);
|
|
1401
|
+
ctx.scale(cloudScale, cloudScale);
|
|
1402
|
+
|
|
1403
|
+
// Thought bubbles leading up
|
|
1404
|
+
sCircle(-180, 160, 12, 850, '#ffffff', 2, '#ffffff');
|
|
1405
|
+
sCircle(-140, 120, 18, 851, '#ffffff', 2, '#ffffff');
|
|
1406
|
+
sCircle(-90, 70, 24, 852, '#ffffff', 2, '#ffffff');
|
|
1407
|
+
|
|
1408
|
+
// Main cloud body
|
|
1409
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
|
1410
|
+
ctx.beginPath();
|
|
1411
|
+
ctx.arc(-70, 0, 55, 0, Math.PI * 2);
|
|
1412
|
+
ctx.arc(0, -25, 65, 0, Math.PI * 2);
|
|
1413
|
+
ctx.arc(70, 0, 55, 0, Math.PI * 2);
|
|
1414
|
+
ctx.arc(0, 30, 60, 0, Math.PI * 2);
|
|
1415
|
+
ctx.fill();
|
|
1416
|
+
|
|
1417
|
+
// Inside thought cloud: Tomorrow's sunrise & stretched hide on rack
|
|
1418
|
+
ctx.fillStyle = '#f59e0b';
|
|
1419
|
+
ctx.beginPath();
|
|
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');
|
|
1428
|
+
|
|
1429
|
+
ctx.restore();
|
|
1430
|
+
|
|
1431
|
+
ctx.font = 'bold 20px monospace';
|
|
1432
|
+
ctx.fillStyle = '#facc15';
|
|
1433
|
+
ctx.textAlign = 'center';
|
|
1434
|
+
ctx.fillText('💡 PLANNING FOR TOMORROW', cloudX, cloudY + 95);
|
|
1435
|
+
|
|
1436
|
+
} else if (COMP_THEME === 'doodle_microscope_lice') {
|
|
1437
|
+
// The Lice Theory & Genetic Divergence Proof
|
|
1438
|
+
// Left scratching character
|
|
1439
|
+
drawDoodleCharacter({
|
|
1440
|
+
x: cx - 290,
|
|
1441
|
+
y: cy + 140,
|
|
1442
|
+
scale: 1.35,
|
|
1443
|
+
hairStyle: 'wild',
|
|
1444
|
+
expression: 'curious',
|
|
1445
|
+
clothing: 'pelt',
|
|
1446
|
+
armPose: 'shiver',
|
|
1447
|
+
seed: 109,
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
// Massive microscope lens on right
|
|
1451
|
+
const lensX = cx + 140;
|
|
1452
|
+
const lensY = cy + 10;
|
|
1453
|
+
const lensR = 190;
|
|
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);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Branching evolutionary divergence diagram inside lens
|
|
1469
|
+
sLine(lensX - 120, lensY, lensX - 40, lensY, 940, '#ffffff', 4);
|
|
1470
|
+
sCircle(lensX - 40, lensY, 8, 941, '#d4af37', 2, '#d4af37'); // Divergence point
|
|
1471
|
+
|
|
1472
|
+
// Head lice branch
|
|
1473
|
+
sLine(lensX - 40, lensY, lensX + 80, lensY - 60, 942, '#38bdf8', 4);
|
|
1474
|
+
ctx.font = 'bold 16px monospace';
|
|
1475
|
+
ctx.fillStyle = '#38bdf8';
|
|
1476
|
+
ctx.textAlign = 'left';
|
|
1477
|
+
ctx.fillText('HEAD LICE (HAIR)', lensX + 90, lensY - 56);
|
|
1478
|
+
|
|
1479
|
+
// Body lice branch (clothes only)
|
|
1480
|
+
sLine(lensX - 40, lensY, lensX + 80, lensY + 60, 943, '#f43f5e', 4);
|
|
1481
|
+
ctx.fillStyle = '#f43f5e';
|
|
1482
|
+
ctx.fillText('BODY LICE (CLOTHES)', lensX + 90, lensY + 64);
|
|
1483
|
+
|
|
1484
|
+
// Divergence date highlight badge
|
|
1485
|
+
sRect(lensX - 110, lensY + 105, 220, 36, 950, '#d4af37', 2, 'rgba(212, 175, 55, 0.2)');
|
|
1486
|
+
ctx.font = 'bold 16px monospace';
|
|
1487
|
+
ctx.fillStyle = '#fef08a';
|
|
1488
|
+
ctx.textAlign = 'center';
|
|
1489
|
+
ctx.fillText('170,000 YEARS AGO', lensX, lensY + 129);
|
|
1490
|
+
|
|
1491
|
+
} else if (COMP_THEME === 'doodle_general_human') {
|
|
1492
|
+
// General Expressive Doodled Human Narrator
|
|
1493
|
+
sLine(cx - W * 0.4, cy + 220, cx + W * 0.4, cy + 220, 10, 'rgba(255,255,255,0.2)', 2);
|
|
1494
|
+
drawDoodleCharacter({
|
|
1495
|
+
x: cx,
|
|
1496
|
+
y: cy + 140,
|
|
1497
|
+
scale: 1.4,
|
|
1498
|
+
hairStyle: 'wild',
|
|
1499
|
+
expression: 'talking',
|
|
1500
|
+
clothing: 'pelt',
|
|
1501
|
+
armPose: 'natural',
|
|
1502
|
+
seed: 110,
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
} else if (COMP_THEME === 'celestial_orbit') {
|
|
698
1506
|
// 3D Celestial Body (Earth) with orbiting satellites & signal pulses
|
|
699
1507
|
const earthR = Math.min(W, H) * 0.22;
|
|
700
1508
|
|
|
@@ -1265,10 +2073,11 @@ AVAILABLE HELPERS:
|
|
|
1265
2073
|
- easeIn(t), easeOut(t), easeInOut(t), boilSeed(t, seed), hash(i)
|
|
1266
2074
|
RULES:
|
|
1267
2075
|
1. Every element must animate smoothly over time t [0..DURATION].
|
|
1268
|
-
2.
|
|
1269
|
-
3.
|
|
1270
|
-
4.
|
|
1271
|
-
5.
|
|
2076
|
+
2. If narration, key phrase, or visual direction involves human actions, history, emotions, characters, or daily life (e.g. choosing clothes, shivering in cold, sewing hides, meeting another tribe, mirror reflection, thinking by fire), ALWAYS animate expressive hand-drawn doodled human figures actively performing those actions (limbs, head, gestures, facial expressions, setting props) rather than sterile abstract diagrams.
|
|
2077
|
+
3. Fill the canvas with rich visual geometry, fluid strokes, animated characters, particles, or cinematic diagrams matching the prompt.
|
|
2078
|
+
4. NEVER use Math.random(), setTimeout, setInterval, Date.now(), performance.now(). All motion must be a pure mathematical function of time t.
|
|
2079
|
+
5. Output ONLY raw executable JavaScript code for the body of window.seek = async (t) => { ... }.
|
|
2080
|
+
6. NO markdown backticks, NO explanations. Start directly with JavaScript.`;
|
|
1272
2081
|
|
|
1273
2082
|
const user = `TOPIC: ${topic || 'Visual Explainer'}
|
|
1274
2083
|
VISUAL DIRECTION: ${visualDescription || caption || 'Dynamic motion graphics'}
|