@amg555/frameforge-mcp 2.4.4 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -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 +1 -1
- package/src/video-renderer/renderer.js +9 -0
- package/src/video-renderer/scene-animator.js +367 -243
- package/src/workflows/assets.js +2 -1
- package/src/workflows/ingest.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to FrameForge are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [2.4.5] — 2026-11-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Browser Code Animation Dispatch in `src/video-renderer/renderer.js`**:
|
|
11
|
+
- Fixed `ReferenceError: camera is not defined` inside `_renderCodeAnimationScene`. Previously, missing parameter destructuring caused the browser animation engine to throw on every scene, silently falling back to static FFmpeg image slideshows.
|
|
12
|
+
- Added full pass-through of `camera`, `visualType`, and `focalPoint` into `_renderCodeAnimationScene`.
|
|
13
|
+
- **Manifest Ingest Preservation (`src/workflows/ingest.js`, `src/workflows/assets.js`)**:
|
|
14
|
+
- `normaliseIngestScene` now retains `prompt`, `style`, and `theme`, and falls back to `raw.prompt` for `visual_description`.
|
|
15
|
+
- Added `--theme` and `--aesthetic` CLI options to `src/cli/make.js`.
|
|
16
|
+
|
|
17
|
+
### Enhanced
|
|
18
|
+
- **Audio-Driven Continuous Camera Director (`src/video-renderer/scene-animator.js`)**:
|
|
19
|
+
- Replaced hardcoded timer modulo cuts with continuous trajectory over `p = t / window.DURATION`.
|
|
20
|
+
- Camera movements (`slow_zoom_in`, `slow_zoom_out`, `pan_left`, `pan_right`, `tilt_up`, `tilt_down`, `ken_burns`, `static`, and `auto`) now smoothly scale to exact narration length.
|
|
21
|
+
- **Max-Quality Organic Doodle Anatomy (`src/video-renderer/scene-animator.js`)**:
|
|
22
|
+
- Overhauled `drawDoodleCharacter`: replaced straight vertical spine line with contoured human torso (shoulders, ribcage, waist, hips), clavicle accents, articulated knees, and planted feet.
|
|
23
|
+
- Removed forced ground drop shadows, forced rosy cheek blush, and forced specular eye highlights.
|
|
24
|
+
- **Light Parchment Hand-Drawn Inking Aesthetic (`hand_drawn_doodle`)**:
|
|
25
|
+
- Warm parchment paper backgrounds (`#faf7f2`, `#f6f1e8`, `#fdfbf7`) with dark charcoal inking (`#1c1917`), subtle line grain, and organic 12fps line boil.
|
|
26
|
+
- Removed all unwanted text stamps and banners (`NOT FOR COLD`, `THEORY OF MIND`, etc.) from narrative doodle themes.
|
|
27
|
+
- Added `doodle_modern_vs_prehistoric` split-screen comparison theme.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
7
31
|
## [2.4.4] — 2026-11-09
|
|
8
32
|
|
|
9
33
|
### 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.5",
|
|
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"
|
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.5', code.includes("version: '2.4.5'"));
|
|
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.5', cfWorker.includes("'2.4.5'"));
|
|
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
|
+
theme: '',
|
|
101
102
|
orientation: 'landscape',
|
|
102
103
|
captions: false, subtitles: false,
|
|
103
104
|
scenesFile: '', scriptFile: '', mediaDir: '',
|
|
@@ -120,6 +121,8 @@ function parseArgs(argv) {
|
|
|
120
121
|
case '--subtitles':
|
|
121
122
|
case '--with-captions': args.captions = true; break;
|
|
122
123
|
case '--style': args.style = argv[++i] || args.style; break;
|
|
124
|
+
case '--theme':
|
|
125
|
+
case '--aesthetic': args.theme = argv[++i] || ''; break;
|
|
123
126
|
case '--channels': args.channels = (argv[++i] || 'file').split(',').map(s => s.trim()); break;
|
|
124
127
|
case '--no-research': args.noResearch = true; break;
|
|
125
128
|
case '--no-script': args.noScript = true; break;
|
|
@@ -423,6 +426,7 @@ async function runSingleVideo(args, topic, outDir, workflows) {
|
|
|
423
426
|
topic,
|
|
424
427
|
title,
|
|
425
428
|
style: args.style || '',
|
|
429
|
+
theme: args.theme || '',
|
|
426
430
|
language: args.language,
|
|
427
431
|
voice: resolvedVoice,
|
|
428
432
|
});
|
|
@@ -439,6 +443,7 @@ async function runSingleVideo(args, topic, outDir, workflows) {
|
|
|
439
443
|
topic,
|
|
440
444
|
title,
|
|
441
445
|
style: args.style || '',
|
|
446
|
+
theme: args.theme || '',
|
|
442
447
|
language: args.language,
|
|
443
448
|
showCaptions: Boolean(args.captions || args.subtitles),
|
|
444
449
|
renderWidth,
|
package/src/mcp-server/server.js
CHANGED
|
@@ -278,11 +278,14 @@ class VideoRenderer {
|
|
|
278
278
|
caption,
|
|
279
279
|
narration,
|
|
280
280
|
visualDescription,
|
|
281
|
+
visualType,
|
|
281
282
|
topic,
|
|
282
283
|
style,
|
|
283
284
|
theme,
|
|
284
285
|
title,
|
|
285
286
|
language,
|
|
287
|
+
camera,
|
|
288
|
+
focalPoint,
|
|
286
289
|
html,
|
|
287
290
|
code,
|
|
288
291
|
clampedDur,
|
|
@@ -360,11 +363,14 @@ class VideoRenderer {
|
|
|
360
363
|
caption,
|
|
361
364
|
narration,
|
|
362
365
|
visualDescription,
|
|
366
|
+
visualType = 'code_animation',
|
|
363
367
|
topic = '',
|
|
364
368
|
style = '',
|
|
365
369
|
theme = '',
|
|
366
370
|
title = '',
|
|
367
371
|
language = '',
|
|
372
|
+
camera = 'auto',
|
|
373
|
+
focalPoint = null,
|
|
368
374
|
html = null,
|
|
369
375
|
code = null,
|
|
370
376
|
clampedDur,
|
|
@@ -393,6 +399,7 @@ class VideoRenderer {
|
|
|
393
399
|
topic,
|
|
394
400
|
});
|
|
395
401
|
} else {
|
|
402
|
+
const effectiveCam = camera || selectCameraHint(sceneIndex, focalPoint || {});
|
|
396
403
|
htmlContent = generateSceneHtml({
|
|
397
404
|
caption,
|
|
398
405
|
narration,
|
|
@@ -409,6 +416,8 @@ class VideoRenderer {
|
|
|
409
416
|
sceneIndex,
|
|
410
417
|
musicMood,
|
|
411
418
|
showCaptions,
|
|
419
|
+
camera: effectiveCam,
|
|
420
|
+
cameraHint: effectiveCam,
|
|
412
421
|
});
|
|
413
422
|
}
|
|
414
423
|
|
|
@@ -45,6 +45,19 @@ function getScriptFontStack(text = '', language = '') {
|
|
|
45
45
|
|
|
46
46
|
// ── Aesthetic Definitions ──────────────────────────────────────────────────────
|
|
47
47
|
const AESTHETICS = {
|
|
48
|
+
hand_drawn_doodle: {
|
|
49
|
+
name: 'Hand-Drawn Doodle',
|
|
50
|
+
palettes: [
|
|
51
|
+
{ bg0: '#ffffff', bg1: '#faf7f2', bg2: '#f6f1e8', stroke: '#1c1917', strokeAlt: '#44403c', accent: '#c2410c', accent2: '#d97706', cardBg: 'rgba(255, 255, 255, 0.95)', cardBorder: 'rgba(28, 25, 23, 0.25)', tag: 'DOODLE', isLight: true },
|
|
52
|
+
{ bg0: '#fdfbf7', bg1: '#f8f4eb', bg2: '#f3ece0', stroke: '#18181b', strokeAlt: '#3f3f46', accent: '#15803d', accent2: '#b45309', cardBg: 'rgba(255, 255, 255, 0.95)', cardBorder: 'rgba(24, 24, 27, 0.25)', tag: 'SKETCH', isLight: true },
|
|
53
|
+
{ bg0: '#fcfaf5', bg1: '#f7f2e7', bg2: '#f0e8d8', stroke: '#0f172a', strokeAlt: '#334155', accent: '#0369a1', accent2: '#c2410c', cardBg: 'rgba(255, 255, 255, 0.95)', cardBorder: 'rgba(15, 23, 42, 0.25)', tag: 'INK', isLight: true },
|
|
54
|
+
],
|
|
55
|
+
fontFamily: (script) => script ? `${script}, 'Caveat', 'Patrick Hand', 'Comic Sans MS', cursive, sans-serif` : `'Caveat', 'Patrick Hand', 'Segoe UI', cursive, sans-serif`,
|
|
56
|
+
fontDisplayWeight: '700',
|
|
57
|
+
backgroundType: 'paper_texture',
|
|
58
|
+
springTension: 160,
|
|
59
|
+
springFriction: 14,
|
|
60
|
+
},
|
|
48
61
|
cyber_tech: {
|
|
49
62
|
name: 'Cyber Tech',
|
|
50
63
|
palettes: [
|
|
@@ -191,6 +204,7 @@ function detectAesthetic({
|
|
|
191
204
|
// Explicit override wins
|
|
192
205
|
const explicit = (theme || style || '').toLowerCase().trim();
|
|
193
206
|
if (AESTHETICS[explicit]) return explicit;
|
|
207
|
+
if (/doodle|sketch|hand[-\s]?drawn|drawing|white background|cream background|cartoon|illustration/i.test(explicit)) return 'hand_drawn_doodle';
|
|
194
208
|
if (/cyber|tech|code|ai|crypto|hacker|futur/i.test(explicit)) return 'cyber_tech';
|
|
195
209
|
if (/doc|history|ancient|story|cinema|kerala|temple|culture/i.test(explicit)) return 'cinematic_documentary';
|
|
196
210
|
if (/finan|market|stock|money|invest|crypto|business/i.test(explicit)) return 'finance_executive';
|
|
@@ -204,6 +218,12 @@ function detectAesthetic({
|
|
|
204
218
|
// Keyword scan across all textual signals (English + Multilingual)
|
|
205
219
|
const combined = `${topic} ${title} ${visualDescription} ${narration} ${caption} ${musicMood}`.toLowerCase();
|
|
206
220
|
|
|
221
|
+
// Hand-Drawn Doodle / Sketch / Cartoon
|
|
222
|
+
if (/(?:^|[^\w])(doodle|sketch|hand[-\s]?drawn|drawing|white background|cream background|paper|cartoon)(?:[^\w]|$)/.test(combined)
|
|
223
|
+
|| /ഡൂഡിൽ|വര|വരച്ച|ചിത്രരചന|रेखाचित्र|कार्टून/.test(combined)) {
|
|
224
|
+
return 'hand_drawn_doodle';
|
|
225
|
+
}
|
|
226
|
+
|
|
207
227
|
// Cosmic Space / Quantum / Orbital
|
|
208
228
|
if (/(?:^|[^\w])(quantum|astronomy|galaxy|universe|space|mars|satellite|telescope|cosmos|interstellar|orbit|gps|satellites)(?:[^\w]|$)/.test(combined)
|
|
209
229
|
|| /പ്രപഞ്ച|ബഹിരാകാശ|നക്ഷത്ര|ഗ്രഹം|ഉപഗ്രഹ|ജ്യോതിശാസ്ത്ര|अंतरिक्ष|ब्रह्मांड|तारा|விண்வெளி|பிரபஞ்சம்/.test(combined)) {
|
|
@@ -263,9 +283,10 @@ function detectAesthetic({
|
|
|
263
283
|
/**
|
|
264
284
|
* Detect the graphic entity to render on the full canvas for a given scene.
|
|
265
285
|
*/
|
|
266
|
-
function detectComponentTheme({ visualDescription = '', caption = '', narration = '', topic = '', imagePath = null, aesthetic = 'cyber_tech' } = {}) {
|
|
286
|
+
function detectComponentTheme({ visualDescription = '', visual_description = '', prompt = '', caption = '', narration = '', topic = '', imagePath = null, aesthetic = 'cyber_tech' } = {}) {
|
|
267
287
|
if (imagePath) return 'image';
|
|
268
|
-
const
|
|
288
|
+
const visDesc = visualDescription || visual_description || prompt || '';
|
|
289
|
+
const text = `${visDesc} ${caption} ${narration} ${topic}`.toLowerCase();
|
|
269
290
|
|
|
270
291
|
// ── Doodled Character Actions (Human Stories, History, Evolution, Daily Life) ──
|
|
271
292
|
// 1. Microscope / Lice Theory / Parasite / Genetics (Scene 7)
|
|
@@ -310,13 +331,19 @@ function detectComponentTheme({ visualDescription = '', caption = '', narration
|
|
|
310
331
|
return 'doodle_shame_reflection';
|
|
311
332
|
}
|
|
312
333
|
|
|
313
|
-
// 8.
|
|
334
|
+
// 8. Modern vs Prehistoric Human Comparison (Scene 2)
|
|
335
|
+
if (/(?:^|[^\w])(modern vs ancient|modern human|fashionable|modern vs prehistoric|split-screen|split screen|transformation|then and now|evolution of human)(?:[^\w]|$)/i.test(text)
|
|
336
|
+
|| /ഫാഷൻ|ആധുനിക മനുഷ്യൻ|പുരാതന മനുഷ്യൻ|കടകളോ|ആധുനികവും പുരാതനവും/.test(text)) {
|
|
337
|
+
return 'doodle_modern_vs_prehistoric';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 9. Dressing / Wardrobe / Wearing Clothes / Outfit / Morning Routine (Scene 1, 11)
|
|
314
341
|
if (/(?:^|[^\w])(dress|dressing|wearing clothes|shirt|wardrobe|closet|hanger|outfit|choose clothes|wear|garment|wedding dress|interview|daily habit|why we wear clothes)(?:[^\w]|$)/i.test(text)
|
|
315
|
-
||
|
|
342
|
+
|| /വസ്ത്രധാരണം|ധരിച്ചിട്ടുണ്ടാ|ധരിച്ച|വസ്ത്രം ധരിക്കാൻ|വസ്ത്രത്തിന്|വസ്ത്രം തിരഞ്ഞെടുക്കു|ഷർട്ട്|തിരഞ്ഞെടുത്തത്|തുണി എടുത്തിടുകയല്ല|കല്യാണത്തിന്|ഇന്റർവ്യൂ|ആമുഖം: വസ്ത്രധാരണം|വസ്ത്രം തിരഞ്ഞെടുക്കുമ്പോൾ|വസ്ത്രം തിരഞ്ഞെടുക്കും/.test(text)) {
|
|
316
343
|
return 'doodle_dressing';
|
|
317
344
|
}
|
|
318
345
|
|
|
319
|
-
//
|
|
346
|
+
// 10. General Human Character
|
|
320
347
|
if (/(?:^|[^\w])(human|person|man|woman|people|child|guy|doodle|character|actor)(?:[^\w]|$)/i.test(text)
|
|
321
348
|
|| /മനുഷ്യൻ|ആളുകൾ|വ്യക്തി/.test(text)) {
|
|
322
349
|
return 'doodle_general_human';
|
|
@@ -520,6 +547,8 @@ function generateSceneHtml({
|
|
|
520
547
|
title = '',
|
|
521
548
|
language = '',
|
|
522
549
|
musicMood = 'neutral',
|
|
550
|
+
camera = 'auto',
|
|
551
|
+
cameraHint = 'auto',
|
|
523
552
|
duration = 3,
|
|
524
553
|
width = 1920,
|
|
525
554
|
height = 1080,
|
|
@@ -598,6 +627,7 @@ function generateSceneHtml({
|
|
|
598
627
|
<canvas id="stage" width="${width}" height="${height}"></canvas>
|
|
599
628
|
<script>
|
|
600
629
|
window.DURATION = ${clampedDur};
|
|
630
|
+
window.CAMERA_HINT = ${JSON.stringify(camera || cameraHint || 'auto')};
|
|
601
631
|
const canvas = document.getElementById('stage');
|
|
602
632
|
const ctx = canvas.getContext('2d');
|
|
603
633
|
const W = ${width};
|
|
@@ -658,6 +688,10 @@ const FONT_DISPLAY = ${JSON.stringify(fontDisplay)};
|
|
|
658
688
|
const FONT_BODY = ${JSON.stringify(fontBody)};
|
|
659
689
|
const FONT_WEIGHT = ${JSON.stringify(aesthetic.fontDisplayWeight)};
|
|
660
690
|
const IMAGE_SRC = ${JSON.stringify(imageDataUri)};
|
|
691
|
+
const IS_LIGHT = ${Boolean(pal.isLight)};
|
|
692
|
+
const STROKE_COLOR = ${JSON.stringify(pal.isLight ? (pal.stroke || '#1c1917') : '#f8fafc')};
|
|
693
|
+
const STROKE_ALT = ${JSON.stringify(pal.isLight ? (pal.strokeAlt || '#44403c') : 'rgba(255, 255, 255, 0.7)')};
|
|
694
|
+
const AMBIENT_LINE = ${JSON.stringify(pal.isLight ? 'rgba(28, 25, 23, 0.22)' : 'rgba(255, 255, 255, 0.25)')};
|
|
661
695
|
|
|
662
696
|
let cachedImage = null;
|
|
663
697
|
if (IMAGE_SRC) {
|
|
@@ -677,7 +711,24 @@ function drawBackground(t) {
|
|
|
677
711
|
ctx.fillRect(0, 0, W, H);
|
|
678
712
|
|
|
679
713
|
ctx.save();
|
|
680
|
-
if (BG_TYPE === '
|
|
714
|
+
if (BG_TYPE === 'paper_texture' || IS_LIGHT) {
|
|
715
|
+
// Subtle paper grain / parchment texture
|
|
716
|
+
ctx.strokeStyle = 'rgba(120, 80, 40, 0.04)';
|
|
717
|
+
ctx.lineWidth = 1;
|
|
718
|
+
for (let y = 40; y < H; y += 40) {
|
|
719
|
+
const wobble = Math.sin(y * 0.1) * 3;
|
|
720
|
+
ctx.beginPath();
|
|
721
|
+
ctx.moveTo(0, y + wobble);
|
|
722
|
+
ctx.lineTo(W, y - wobble);
|
|
723
|
+
ctx.stroke();
|
|
724
|
+
}
|
|
725
|
+
// Subtle organic vignette
|
|
726
|
+
const vig = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.45, W / 2, H / 2, Math.max(W, H) * 0.75);
|
|
727
|
+
vig.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
|
728
|
+
vig.addColorStop(1, 'rgba(120, 90, 60, 0.08)');
|
|
729
|
+
ctx.fillStyle = vig;
|
|
730
|
+
ctx.fillRect(0, 0, W, H);
|
|
731
|
+
} else if (BG_TYPE === 'grid_perspective') {
|
|
681
732
|
// 3D Perspective Grid
|
|
682
733
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
|
|
683
734
|
ctx.lineWidth = 1;
|
|
@@ -754,51 +805,61 @@ function drawBackground(t) {
|
|
|
754
805
|
// ── Full-Canvas Cinematic Visual Directors ─────────────────────────────────────
|
|
755
806
|
function drawMainScene(t) {
|
|
756
807
|
ctx.save();
|
|
757
|
-
|
|
808
|
+
// Continuous audio-driven cinematic camera movement (no fixed interval timer cuts)
|
|
809
|
+
const dur = window.DURATION || 3;
|
|
810
|
+
const p = Math.min(1, Math.max(0, t / dur));
|
|
758
811
|
const enterSpring = spring(t, 0.05, 170, 13);
|
|
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;
|
|
812
|
+
const camHint = (window.CAMERA_HINT || 'auto').toLowerCase();
|
|
766
813
|
|
|
767
814
|
let targetZoom = 1.0;
|
|
768
815
|
let targetPanX = 0;
|
|
769
816
|
let targetPanY = 0;
|
|
770
817
|
|
|
771
|
-
if (
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
818
|
+
if (camHint === 'slow_zoom_in' || camHint === 'zoom_in') {
|
|
819
|
+
targetZoom = 1.0 + p * 0.08;
|
|
820
|
+
targetPanX = Math.sin(t * 0.4) * 6;
|
|
821
|
+
targetPanY = Math.cos(t * 0.3) * 4;
|
|
822
|
+
} else if (camHint === 'slow_zoom_out' || camHint === 'zoom_out') {
|
|
823
|
+
targetZoom = 1.08 - p * 0.08;
|
|
824
|
+
targetPanX = Math.sin(t * 0.4) * 6;
|
|
825
|
+
targetPanY = Math.cos(t * 0.3) * 4;
|
|
826
|
+
} else if (camHint === 'pan_left') {
|
|
827
|
+
targetZoom = 1.02;
|
|
828
|
+
targetPanX = 25 - p * 50;
|
|
829
|
+
targetPanY = Math.cos(t * 0.3) * 3;
|
|
830
|
+
} else if (camHint === 'pan_right') {
|
|
831
|
+
targetZoom = 1.02;
|
|
832
|
+
targetPanX = -25 + p * 50;
|
|
833
|
+
targetPanY = Math.cos(t * 0.3) * 3;
|
|
834
|
+
} else if (camHint === 'tilt_up') {
|
|
835
|
+
targetZoom = 1.03;
|
|
836
|
+
targetPanX = Math.sin(t * 0.3) * 4;
|
|
837
|
+
targetPanY = 20 - p * 40;
|
|
838
|
+
} else if (camHint === 'tilt_down') {
|
|
839
|
+
targetZoom = 1.03;
|
|
840
|
+
targetPanX = Math.sin(t * 0.3) * 4;
|
|
841
|
+
targetPanY = -20 + p * 40;
|
|
842
|
+
} else if (camHint === 'ken_burns') {
|
|
843
|
+
targetZoom = 1.0 + p * 0.06;
|
|
844
|
+
targetPanX = -18 + p * 36;
|
|
845
|
+
targetPanY = 12 - p * 24;
|
|
846
|
+
} else if (camHint === 'static') {
|
|
847
|
+
targetZoom = 1.0;
|
|
848
|
+
targetPanX = 0;
|
|
849
|
+
targetPanY = 0;
|
|
790
850
|
} else {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
851
|
+
// 'auto' or narrative default: subtle organic breathing and smooth forward progression
|
|
852
|
+
targetZoom = 1.0 + Math.sin(p * Math.PI * 0.5) * 0.04;
|
|
853
|
+
targetPanX = Math.sin(t * 0.35) * 8;
|
|
854
|
+
targetPanY = Math.cos(t * 0.28) * 4;
|
|
794
855
|
}
|
|
795
856
|
|
|
796
|
-
const camZoom =
|
|
797
|
-
const camPanX = targetPanX
|
|
798
|
-
const camPanY = targetPanY
|
|
857
|
+
const camZoom = targetZoom;
|
|
858
|
+
const camPanX = targetPanX;
|
|
859
|
+
const camPanY = targetPanY;
|
|
799
860
|
|
|
800
861
|
ctx.translate(W / 2 + camPanX, H * 0.50 + camPanY);
|
|
801
|
-
ctx.scale(Math.min(1.
|
|
862
|
+
ctx.scale(Math.min(1.02, Math.max(0.92, enterSpring)) * camZoom, Math.min(1.02, Math.max(0.92, enterSpring)) * camZoom);
|
|
802
863
|
ctx.translate(-W / 2, -H * 0.50);
|
|
803
864
|
|
|
804
865
|
const cx = W / 2;
|
|
@@ -810,7 +871,7 @@ function drawMainScene(t) {
|
|
|
810
871
|
return Math.sin(seed * 19.37 + step * 2.41) * mag;
|
|
811
872
|
}
|
|
812
873
|
|
|
813
|
-
function sLine(x1, y1, x2, y2, seed, color =
|
|
874
|
+
function sLine(x1, y1, x2, y2, seed, color = STROKE_COLOR, width = 3.5) {
|
|
814
875
|
ctx.save();
|
|
815
876
|
ctx.strokeStyle = color;
|
|
816
877
|
ctx.lineWidth = width;
|
|
@@ -825,7 +886,7 @@ function drawMainScene(t) {
|
|
|
825
886
|
ctx.restore();
|
|
826
887
|
}
|
|
827
888
|
|
|
828
|
-
function sCircle(x, y, r, seed, color =
|
|
889
|
+
function sCircle(x, y, r, seed, color = STROKE_COLOR, width = 3, fillColor = null) {
|
|
829
890
|
ctx.save();
|
|
830
891
|
if (fillColor) {
|
|
831
892
|
ctx.fillStyle = fillColor;
|
|
@@ -850,7 +911,7 @@ function drawMainScene(t) {
|
|
|
850
911
|
ctx.restore();
|
|
851
912
|
}
|
|
852
913
|
|
|
853
|
-
function sRect(x, y, w, h, seed, color =
|
|
914
|
+
function sRect(x, y, w, h, seed, color = STROKE_COLOR, width = 3, fillColor = null) {
|
|
854
915
|
ctx.save();
|
|
855
916
|
if (fillColor) {
|
|
856
917
|
ctx.fillStyle = fillColor;
|
|
@@ -874,75 +935,107 @@ function drawMainScene(t) {
|
|
|
874
935
|
expression = 'talking',
|
|
875
936
|
clothing = 'none',
|
|
876
937
|
armPose = 'natural',
|
|
938
|
+
showShadow = false,
|
|
939
|
+
showBlush = false,
|
|
940
|
+
showSpecular = false,
|
|
877
941
|
seed = 100,
|
|
878
942
|
} = {}) {
|
|
879
943
|
ctx.save();
|
|
880
944
|
ctx.translate(x, y);
|
|
881
945
|
ctx.scale(scale, scale);
|
|
882
946
|
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
947
|
+
// Ground contact shadow: strictly conditional (default false)
|
|
948
|
+
if (showShadow) {
|
|
949
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.28)';
|
|
950
|
+
ctx.beginPath();
|
|
951
|
+
ctx.ellipse(0, legLength, 34, 7, 0, 0, Math.PI * 2);
|
|
952
|
+
ctx.fill();
|
|
953
|
+
}
|
|
888
954
|
|
|
889
955
|
const blink = (t % 3.2 < 0.14) ? 0.15 : 1.0;
|
|
890
956
|
const mouthTalk = Math.abs(Math.sin(t * 14)) * 4;
|
|
891
957
|
const torsoTopY = -torsoHeight;
|
|
892
958
|
const torsoBottomY = 0;
|
|
959
|
+
const shoulderW = 28;
|
|
893
960
|
|
|
894
|
-
//
|
|
961
|
+
// High quality contoured torso silhouette (organic human form, never stick figure)
|
|
895
962
|
if (clothing === 'pelt') {
|
|
963
|
+
// Prehistoric animal pelt tunic with textured ragged hem
|
|
896
964
|
ctx.fillStyle = '#b45309';
|
|
897
965
|
ctx.beginPath();
|
|
898
|
-
ctx.moveTo(-
|
|
899
|
-
ctx.lineTo(
|
|
900
|
-
ctx.lineTo(
|
|
901
|
-
ctx.lineTo(-
|
|
966
|
+
ctx.moveTo(-shoulderW + wb(seed, 2), torsoTopY);
|
|
967
|
+
ctx.lineTo(shoulderW + wb(seed + 1, 2), torsoTopY);
|
|
968
|
+
ctx.lineTo(shoulderW + 6 + wb(seed + 2, 2), torsoBottomY + 22);
|
|
969
|
+
ctx.lineTo(-shoulderW - 6 + wb(seed + 3, 2), torsoBottomY + 22);
|
|
902
970
|
ctx.closePath();
|
|
903
971
|
ctx.fill();
|
|
904
|
-
|
|
972
|
+
|
|
973
|
+
// Pelt border stitching & waist wrap
|
|
974
|
+
sLine(-shoulderW - 6, torsoBottomY + 22, shoulderW + 6, torsoBottomY + 22, seed + 4, '#78350f', 4);
|
|
975
|
+
sLine(-shoulderW, torsoBottomY - 8, shoulderW, torsoBottomY - 8, seed + 5, '#451a03', 3);
|
|
976
|
+
// Asymmetric fur shoulder wrap
|
|
977
|
+
sLine(-shoulderW, torsoTopY, shoulderW - 10, torsoTopY + 24, seed + 6, '#78350f', 3.5);
|
|
905
978
|
} else if (clothing === 'shirt') {
|
|
979
|
+
// Modern tailored shirt/jacket with collar and button placket
|
|
906
980
|
ctx.fillStyle = ACCENT;
|
|
907
981
|
ctx.beginPath();
|
|
908
|
-
ctx.moveTo(-
|
|
909
|
-
ctx.lineTo(
|
|
910
|
-
ctx.lineTo(
|
|
911
|
-
ctx.lineTo(-
|
|
982
|
+
ctx.moveTo(-shoulderW + wb(seed, 2), torsoTopY);
|
|
983
|
+
ctx.lineTo(shoulderW + wb(seed + 1, 2), torsoTopY);
|
|
984
|
+
ctx.lineTo(shoulderW + 2 + wb(seed + 2, 2), torsoBottomY + 12);
|
|
985
|
+
ctx.lineTo(-shoulderW - 2 + wb(seed + 3, 2), torsoBottomY + 12);
|
|
912
986
|
ctx.closePath();
|
|
913
987
|
ctx.fill();
|
|
988
|
+
|
|
989
|
+
// Collar lines & placket
|
|
914
990
|
sLine(-14, torsoTopY, 0, torsoTopY + 18, seed + 4, '#ffffff', 3);
|
|
915
991
|
sLine(14, torsoTopY, 0, torsoTopY + 18, seed + 5, '#ffffff', 3);
|
|
916
992
|
sLine(0, torsoTopY + 18, 0, torsoBottomY + 10, seed + 6, '#ffffff', 2.5);
|
|
917
993
|
} else if (clothing === 'cloak') {
|
|
994
|
+
// Flowing draped mantle
|
|
918
995
|
ctx.fillStyle = '#0f766e';
|
|
919
996
|
ctx.beginPath();
|
|
920
|
-
ctx.moveTo(-
|
|
921
|
-
ctx.lineTo(
|
|
922
|
-
ctx.lineTo(
|
|
923
|
-
ctx.lineTo(-
|
|
997
|
+
ctx.moveTo(-shoulderW - 8, torsoTopY - 6);
|
|
998
|
+
ctx.lineTo(shoulderW + 8, torsoTopY - 6);
|
|
999
|
+
ctx.lineTo(shoulderW + 18, torsoBottomY + 32);
|
|
1000
|
+
ctx.lineTo(-shoulderW - 18, torsoBottomY + 32);
|
|
924
1001
|
ctx.closePath();
|
|
925
1002
|
ctx.fill();
|
|
1003
|
+
sLine(-shoulderW - 8, torsoTopY - 6, -shoulderW - 18, torsoBottomY + 32, seed + 7, '#134e4a', 3.5);
|
|
1004
|
+
sLine(shoulderW + 8, torsoTopY - 6, shoulderW + 18, torsoBottomY + 32, seed + 8, '#134e4a', 3.5);
|
|
926
1005
|
} else {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1006
|
+
// Natural contoured human torso (shoulders, ribcage, waist, hips)
|
|
1007
|
+
ctx.fillStyle = IS_LIGHT ? 'rgba(217, 119, 6, 0.12)' : 'rgba(254, 243, 199, 0.22)';
|
|
1008
|
+
ctx.beginPath();
|
|
1009
|
+
ctx.moveTo(-shoulderW + wb(seed, 1.5), torsoTopY + 5);
|
|
1010
|
+
ctx.lineTo(shoulderW + wb(seed + 1, 1.5), torsoTopY + 5);
|
|
1011
|
+
ctx.quadraticCurveTo(shoulderW - 6 + wb(seed + 2, 1.5), torsoTopY + torsoHeight * 0.5, shoulderW - 4, torsoBottomY);
|
|
1012
|
+
ctx.lineTo(-shoulderW + 4, torsoBottomY);
|
|
1013
|
+
ctx.quadraticCurveTo(-shoulderW + 6 + wb(seed + 3, 1.5), torsoTopY + torsoHeight * 0.5, -shoulderW + wb(seed, 1.5), torsoTopY + 5);
|
|
1014
|
+
ctx.closePath();
|
|
1015
|
+
ctx.fill();
|
|
1016
|
+
|
|
1017
|
+
// Contoured torso outline strokes
|
|
1018
|
+
sLine(-shoulderW, torsoTopY + 5, -shoulderW + 4, torsoBottomY, seed + 7, STROKE_COLOR, 3.5);
|
|
1019
|
+
sLine(shoulderW, torsoTopY + 5, shoulderW - 4, torsoBottomY, seed + 8, STROKE_COLOR, 3.5);
|
|
1020
|
+
sLine(-shoulderW + 4, torsoBottomY, shoulderW - 4, torsoBottomY, seed + 9, STROKE_COLOR, 3);
|
|
1021
|
+
// Subtle collarbone / rib accents
|
|
1022
|
+
sLine(-16, torsoTopY + 16, -2, torsoTopY + 22, seed + 10, STROKE_ALT, 2);
|
|
1023
|
+
sLine(16, torsoTopY + 16, 2, torsoTopY + 22, seed + 11, STROKE_ALT, 2);
|
|
930
1024
|
}
|
|
931
|
-
sLine(0, torsoTopY, 0, torsoBottomY, seed + 10, '#f8fafc', 4);
|
|
932
1025
|
|
|
933
|
-
// Head
|
|
934
|
-
const headCenterY = torsoTopY - headRadius - 6 + wb(seed +
|
|
935
|
-
sCircle(0, headCenterY, headRadius, seed +
|
|
1026
|
+
// Head (expressive contoured skull)
|
|
1027
|
+
const headCenterY = torsoTopY - headRadius - 6 + wb(seed + 12, 1.5);
|
|
1028
|
+
sCircle(0, headCenterY, headRadius, seed + 13, STROKE_COLOR, 3.5, '#fef3c7');
|
|
936
1029
|
|
|
937
1030
|
// Hair
|
|
938
1031
|
if (hairStyle === 'wild') {
|
|
939
|
-
for (let h = 0; h <
|
|
940
|
-
const hairAng = -Math.PI * 0.
|
|
1032
|
+
for (let h = 0; h < 7; h++) {
|
|
1033
|
+
const hairAng = -Math.PI * 0.88 + (h / 6) * Math.PI * 0.76;
|
|
941
1034
|
const hx1 = Math.cos(hairAng) * headRadius;
|
|
942
1035
|
const hy1 = headCenterY + Math.sin(hairAng) * headRadius;
|
|
943
|
-
const hx2 = Math.cos(hairAng) * (headRadius +
|
|
944
|
-
const hy2 = headCenterY + Math.sin(hairAng) * (headRadius +
|
|
945
|
-
sLine(hx1, hy1, hx2, hy2, seed +
|
|
1036
|
+
const hx2 = Math.cos(hairAng) * (headRadius + 24 + wb(seed + 14 + h, 4));
|
|
1037
|
+
const hy2 = headCenterY + Math.sin(hairAng) * (headRadius + 24 + wb(seed + 15 + h, 4));
|
|
1038
|
+
sLine(hx1, hy1, hx2, hy2, seed + 16 + h, '#78350f', 3.5);
|
|
946
1039
|
}
|
|
947
1040
|
} else if (hairStyle === 'modern') {
|
|
948
1041
|
ctx.fillStyle = '#1e293b';
|
|
@@ -958,12 +1051,12 @@ function drawMainScene(t) {
|
|
|
958
1051
|
|
|
959
1052
|
// Eyes
|
|
960
1053
|
const eyeY = headCenterY + 4;
|
|
961
|
-
const eyeR = 4 * blink;
|
|
1054
|
+
const eyeR = 4.2 * blink;
|
|
962
1055
|
sCircle(-12, eyeY, Math.max(1, eyeR), seed + 23, '#0f172a', 2, '#0f172a');
|
|
963
1056
|
sCircle(12, eyeY, Math.max(1, eyeR), seed + 24, '#0f172a', 2, '#0f172a');
|
|
964
1057
|
|
|
965
|
-
// Specular eye highlights
|
|
966
|
-
if (blink > 0.4) {
|
|
1058
|
+
// Specular eye highlights: only when explicitly enabled
|
|
1059
|
+
if (showSpecular && blink > 0.4) {
|
|
967
1060
|
ctx.fillStyle = '#ffffff';
|
|
968
1061
|
ctx.beginPath();
|
|
969
1062
|
ctx.arc(-14, eyeY - 1.5, 1.2, 0, Math.PI * 2);
|
|
@@ -971,12 +1064,14 @@ function drawMainScene(t) {
|
|
|
971
1064
|
ctx.fill();
|
|
972
1065
|
}
|
|
973
1066
|
|
|
974
|
-
//
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1067
|
+
// Rosy cheek blush: strictly conditional (default false)
|
|
1068
|
+
if (showBlush || expression === 'blushing' || expression === 'shame') {
|
|
1069
|
+
ctx.fillStyle = 'rgba(251, 113, 133, 0.28)';
|
|
1070
|
+
ctx.beginPath();
|
|
1071
|
+
ctx.ellipse(-14, headCenterY + 12, 6, 3.5, 0, 0, Math.PI * 2);
|
|
1072
|
+
ctx.ellipse(14, headCenterY + 12, 6, 3.5, 0, 0, Math.PI * 2);
|
|
1073
|
+
ctx.fill();
|
|
1074
|
+
}
|
|
980
1075
|
|
|
981
1076
|
// Eyebrows
|
|
982
1077
|
if (expression === 'shivering') {
|
|
@@ -1012,93 +1107,102 @@ function drawMainScene(t) {
|
|
|
1012
1107
|
sLine(-8, mouthY, 8, mouthY, seed + 34, '#0f172a', 2.5);
|
|
1013
1108
|
}
|
|
1014
1109
|
|
|
1015
|
-
// Legs
|
|
1110
|
+
// Legs with articulated knees and planted feet
|
|
1016
1111
|
const hipY = torsoBottomY;
|
|
1017
1112
|
if (armPose === 'sewing' || armPose === 'thinker') {
|
|
1018
|
-
|
|
1019
|
-
sLine(-
|
|
1020
|
-
sLine(
|
|
1021
|
-
sLine(
|
|
1113
|
+
// Cross-legged seated pose
|
|
1114
|
+
sLine(-14, hipY, -48, hipY + 45, seed + 35, STROKE_COLOR, 4);
|
|
1115
|
+
sLine(-48, hipY + 45, 12, hipY + 50, seed + 36, STROKE_COLOR, 4);
|
|
1116
|
+
sLine(14, hipY, 48, hipY + 45, seed + 37, STROKE_COLOR, 4);
|
|
1117
|
+
sLine(48, hipY + 45, -12, hipY + 50, seed + 38, STROKE_COLOR, 4);
|
|
1022
1118
|
} else if (armPose === 'hold_spear') {
|
|
1119
|
+
// Dynamic walking gait
|
|
1023
1120
|
const legSwing = Math.sin(t * 5.5);
|
|
1024
1121
|
const l1KneeX = -15 + legSwing * 22;
|
|
1025
1122
|
const l1KneeY = hipY + legLength * 0.55;
|
|
1026
1123
|
const l1FootX = -15 + legSwing * 38;
|
|
1027
1124
|
const l1FootY = hipY + legLength;
|
|
1028
|
-
sLine(-
|
|
1029
|
-
sLine(l1KneeX, l1KneeY, l1FootX, l1FootY, seed + 40,
|
|
1125
|
+
sLine(-14, hipY, l1KneeX, l1KneeY, seed + 39, STROKE_COLOR, 4);
|
|
1126
|
+
sLine(l1KneeX, l1KneeY, l1FootX, l1FootY, seed + 40, STROKE_COLOR, 4);
|
|
1127
|
+
sLine(l1FootX, l1FootY, l1FootX + 16, l1FootY, seed + 41, STROKE_COLOR, 3.5);
|
|
1030
1128
|
|
|
1031
1129
|
const l2KneeX = 15 - legSwing * 22;
|
|
1032
1130
|
const l2KneeY = hipY + legLength * 0.55;
|
|
1033
1131
|
const l2FootX = 15 - legSwing * 38;
|
|
1034
1132
|
const l2FootY = hipY + legLength;
|
|
1035
|
-
sLine(
|
|
1036
|
-
sLine(l2KneeX, l2KneeY, l2FootX, l2FootY, seed +
|
|
1133
|
+
sLine(14, hipY, l2KneeX, l2KneeY, seed + 42, STROKE_COLOR, 4);
|
|
1134
|
+
sLine(l2KneeX, l2KneeY, l2FootX, l2FootY, seed + 43, STROKE_COLOR, 4);
|
|
1135
|
+
sLine(l2FootX, l2FootY, l2FootX + 16, l2FootY, seed + 44, STROKE_COLOR, 3.5);
|
|
1037
1136
|
} else {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1137
|
+
// Standing legs with natural knee contour and planted feet
|
|
1138
|
+
const lKneeX = -15 + wb(seed + 39, 1);
|
|
1139
|
+
const rKneeX = 15 + wb(seed + 40, 1);
|
|
1140
|
+
const kneeY = hipY + legLength * 0.52;
|
|
1141
|
+
const footY = hipY + legLength;
|
|
1142
|
+
sLine(-16, hipY, lKneeX, kneeY, seed + 41, STROKE_COLOR, 4);
|
|
1143
|
+
sLine(lKneeX, kneeY, -16, footY, seed + 42, STROKE_COLOR, 4);
|
|
1144
|
+
sLine(-16, footY, -28, footY, seed + 43, STROKE_COLOR, 3.5);
|
|
1145
|
+
|
|
1146
|
+
sLine(16, hipY, rKneeX, kneeY, seed + 44, STROKE_COLOR, 4);
|
|
1147
|
+
sLine(rKneeX, kneeY, 16, footY, seed + 45, STROKE_COLOR, 4);
|
|
1148
|
+
sLine(16, footY, 28, footY, seed + 46, STROKE_COLOR, 3.5);
|
|
1042
1149
|
}
|
|
1043
1150
|
|
|
1044
|
-
// Arms & Hands
|
|
1151
|
+
// Arms & Hands with natural articulated elbows and gestures
|
|
1045
1152
|
const shoulderY = torsoTopY + 12;
|
|
1046
1153
|
if (armPose === 'shiver') {
|
|
1047
1154
|
const shivA = Math.sin(t * 34) * 3;
|
|
1048
|
-
sLine(-
|
|
1049
|
-
sLine(
|
|
1155
|
+
sLine(-shoulderW, shoulderY, 16 + shivA, shoulderY + 35, seed + 47, STROKE_COLOR, 4);
|
|
1156
|
+
sLine(shoulderW, shoulderY, -16 + shivA, shoulderY + 35, seed + 48, STROKE_COLOR, 4);
|
|
1050
1157
|
} else if (armPose === 'hold_spear') {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
sLine(-
|
|
1054
|
-
sLine(
|
|
1055
|
-
|
|
1158
|
+
// Holding spear with grip
|
|
1159
|
+
const spearSway = Math.sin(t * 5.5) * 6;
|
|
1160
|
+
sLine(-shoulderW, shoulderY, -10, shoulderY + 28, seed + 49, STROKE_COLOR, 4);
|
|
1161
|
+
sLine(-10, shoulderY + 28, 12, shoulderY + 14, seed + 50, STROKE_COLOR, 4);
|
|
1162
|
+
// Spear pole passing through hands
|
|
1163
|
+
const spearX = 18 + spearSway;
|
|
1164
|
+
sLine(spearX - 25, cy - 140, spearX + 35, cy + 180, seed + 51, '#78350f', 5.5);
|
|
1165
|
+
// Flint spearhead bound with sinew
|
|
1166
|
+
ctx.fillStyle = '#94a3b8';
|
|
1056
1167
|
ctx.beginPath();
|
|
1057
|
-
ctx.moveTo(spearX
|
|
1058
|
-
ctx.lineTo(spearX -
|
|
1059
|
-
ctx.lineTo(spearX
|
|
1168
|
+
ctx.moveTo(spearX - 25, cy - 140);
|
|
1169
|
+
ctx.lineTo(spearX - 32, cy - 180);
|
|
1170
|
+
ctx.lineTo(spearX - 18, cy - 170);
|
|
1060
1171
|
ctx.closePath();
|
|
1061
1172
|
ctx.fill();
|
|
1062
|
-
sLine(
|
|
1063
|
-
sLine(35, shoulderY + 25 + Math.sin(t * 5.5) * 20, 48, shoulderY + 45, seed + 53, '#f8fafc', 4);
|
|
1173
|
+
sLine(spearX - 28, cy - 140, spearX - 20, cy - 150, seed + 52, '#f59e0b', 3);
|
|
1064
1174
|
} else if (armPose === 'sewing') {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
sLine(
|
|
1071
|
-
sLine(38, shoulderY + 25, handX, handY, seed + 57, '#f8fafc', 4);
|
|
1072
|
-
sLine(handX - 5, handY - 10, handX + 18, handY + 12, seed + 58, '#fef08a', 3.5);
|
|
1073
|
-
ctx.strokeStyle = '#ffffff';
|
|
1074
|
-
ctx.lineWidth = 1.5;
|
|
1075
|
-
ctx.beginPath();
|
|
1076
|
-
ctx.moveTo(handX - 5, handY - 10);
|
|
1077
|
-
ctx.quadraticCurveTo(handX - 25, handY - 25, 0, hipY + 10);
|
|
1078
|
-
ctx.stroke();
|
|
1175
|
+
// Left hand holds stretched pelt, right hand drives bone needle with sinew
|
|
1176
|
+
const sewPull = Math.sin(t * 4) * 14;
|
|
1177
|
+
sLine(-shoulderW, shoulderY, -40, shoulderY + 38, seed + 53, STROKE_COLOR, 4);
|
|
1178
|
+
sLine(-40, shoulderY + 38, -15, shoulderY + 45, seed + 54, STROKE_COLOR, 4);
|
|
1179
|
+
sLine(shoulderW, shoulderY, 35 + sewPull * 0.5, shoulderY + 28, seed + 55, STROKE_COLOR, 4);
|
|
1180
|
+
sLine(35 + sewPull * 0.5, shoulderY + 28, 10 + sewPull, shoulderY + 42, seed + 56, STROKE_COLOR, 4);
|
|
1079
1181
|
} else if (armPose === 'cover_body') {
|
|
1080
|
-
|
|
1081
|
-
sLine(
|
|
1182
|
+
// Protective covering gesture
|
|
1183
|
+
sLine(-shoulderW, shoulderY, 10, shoulderY + 28, seed + 57, STROKE_COLOR, 4);
|
|
1184
|
+
sLine(shoulderW, shoulderY, -10, shoulderY + 48, seed + 58, STROKE_COLOR, 4);
|
|
1185
|
+
} else if (armPose === 'thinker') {
|
|
1186
|
+
// Rodin-style contemplative pose: hand under chin, elbow on knee
|
|
1187
|
+
sLine(shoulderW, shoulderY, 15, shoulderY + 42, seed + 59, STROKE_COLOR, 4);
|
|
1188
|
+
sLine(15, shoulderY + 42, 6, headCenterY + 20, seed + 60, STROKE_COLOR, 4);
|
|
1189
|
+
sLine(-shoulderW, shoulderY, -30, shoulderY + 45, seed + 61, STROKE_COLOR, 4);
|
|
1082
1190
|
} else if (armPose === 'dress_shirt') {
|
|
1083
|
-
|
|
1084
|
-
sLine(-
|
|
1085
|
-
|
|
1086
|
-
sLine(18, shoulderY, 35, shoulderY + 15, seed + 63, '#f8fafc', 4);
|
|
1087
|
-
sLine(35, shoulderY + 15, 10, adjustY, seed + 64, '#f8fafc', 4);
|
|
1191
|
+
// Examining clothes, touching button / adjusting collar
|
|
1192
|
+
sLine(-shoulderW, shoulderY, -14, shoulderY + 22, seed + 62, STROKE_COLOR, 4);
|
|
1193
|
+
sLine(shoulderW, shoulderY, 14, shoulderY + 22, seed + 63, STROKE_COLOR, 4);
|
|
1088
1194
|
} else if (armPose === 'peace_gesture') {
|
|
1089
|
-
|
|
1090
|
-
sLine(
|
|
1091
|
-
sLine(
|
|
1092
|
-
|
|
1093
|
-
sLine(18, shoulderY, 36, hipY + 15, seed + 68, '#f8fafc', 4);
|
|
1094
|
-
sLine(36, hipY + 15, 8, headCenterY + 18, seed + 69, '#f8fafc', 4);
|
|
1095
|
-
sLine(-18, shoulderY, -30, hipY + 25, seed + 70, '#f8fafc', 4);
|
|
1195
|
+
// Open welcoming hand
|
|
1196
|
+
sLine(-shoulderW, shoulderY, -35, shoulderY + 30, seed + 64, STROKE_COLOR, 4);
|
|
1197
|
+
sLine(shoulderW, shoulderY, 35, shoulderY + 15, seed + 65, STROKE_COLOR, 4);
|
|
1198
|
+
sLine(35, shoulderY + 15, 52, shoulderY + 2, seed + 66, STROKE_COLOR, 3.5);
|
|
1096
1199
|
} else {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
sLine(-
|
|
1100
|
-
sLine(
|
|
1101
|
-
sLine(
|
|
1200
|
+
// Natural expressive speaking arms with subtle breathing gesture
|
|
1201
|
+
const gArm = Math.sin(t * 3.5) * 8;
|
|
1202
|
+
sLine(-shoulderW, shoulderY, -shoulderW - 8, shoulderY + 38, seed + 67, STROKE_COLOR, 4);
|
|
1203
|
+
sLine(-shoulderW - 8, shoulderY + 38, -shoulderW - 14, shoulderY + 68, seed + 68, STROKE_COLOR, 4);
|
|
1204
|
+
sLine(shoulderW, shoulderY, shoulderW + 12, shoulderY + 32 - gArm, seed + 69, STROKE_COLOR, 4);
|
|
1205
|
+
sLine(shoulderW + 12, shoulderY + 32 - gArm, shoulderW + 24, shoulderY + 18 - gArm, seed + 70, STROKE_COLOR, 4);
|
|
1102
1206
|
}
|
|
1103
1207
|
|
|
1104
1208
|
ctx.restore();
|
|
@@ -1110,7 +1214,7 @@ function drawMainScene(t) {
|
|
|
1110
1214
|
const beat2 = spring(t, window.DURATION * 0.35, 180, 16);
|
|
1111
1215
|
const beat3 = spring(t, window.DURATION * 0.70, 190, 16);
|
|
1112
1216
|
|
|
1113
|
-
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10,
|
|
1217
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2.5);
|
|
1114
1218
|
|
|
1115
1219
|
// Clothes rack on left (sways in beat 1)
|
|
1116
1220
|
sLine(cx - 360, cy + 220, cx - 360, cy - 140, 11, '#94a3b8', 5);
|
|
@@ -1174,44 +1278,120 @@ function drawMainScene(t) {
|
|
|
1174
1278
|
seed: 101,
|
|
1175
1279
|
});
|
|
1176
1280
|
|
|
1177
|
-
} else if (COMP_THEME === '
|
|
1178
|
-
//
|
|
1179
|
-
const
|
|
1180
|
-
const
|
|
1281
|
+
} else if (COMP_THEME === 'doodle_modern_vs_prehistoric') {
|
|
1282
|
+
// Split-screen comparison: Modern stylish human on left vs Prehistoric human on right
|
|
1283
|
+
const dur = window.DURATION || 3;
|
|
1284
|
+
const bridgeSpring = spring(t, dur * 0.35, 170, 15);
|
|
1285
|
+
|
|
1286
|
+
// Natural ground line
|
|
1287
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2.5);
|
|
1288
|
+
|
|
1289
|
+
// Subtle split-screen center divider
|
|
1290
|
+
ctx.save();
|
|
1291
|
+
ctx.setLineDash([10, 10]);
|
|
1292
|
+
sLine(cx, cy - 220, cx, cy + 200, 20, AMBIENT_LINE, 2);
|
|
1293
|
+
ctx.restore();
|
|
1294
|
+
|
|
1295
|
+
// Modern human on left
|
|
1296
|
+
drawDoodleCharacter({
|
|
1297
|
+
x: cx - 260,
|
|
1298
|
+
y: cy + 140,
|
|
1299
|
+
scale: 1.3,
|
|
1300
|
+
hairStyle: 'modern',
|
|
1301
|
+
expression: 'talking',
|
|
1302
|
+
clothing: 'shirt',
|
|
1303
|
+
armPose: 'dress_shirt',
|
|
1304
|
+
seed: 120,
|
|
1305
|
+
});
|
|
1181
1306
|
|
|
1182
|
-
|
|
1307
|
+
// Prehistoric human on right
|
|
1308
|
+
drawDoodleCharacter({
|
|
1309
|
+
x: cx + 260,
|
|
1310
|
+
y: cy + 140,
|
|
1311
|
+
scale: 1.3,
|
|
1312
|
+
hairStyle: 'wild',
|
|
1313
|
+
expression: 'curious',
|
|
1314
|
+
clothing: 'none',
|
|
1315
|
+
armPose: 'natural',
|
|
1316
|
+
seed: 121,
|
|
1317
|
+
});
|
|
1183
1318
|
|
|
1184
|
-
//
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1319
|
+
// Dynamic timeline bridge / evolution arc connecting them
|
|
1320
|
+
if (bridgeSpring > 0.05) {
|
|
1321
|
+
ctx.save();
|
|
1322
|
+
ctx.globalAlpha = Math.min(0.85, bridgeSpring * 0.85);
|
|
1323
|
+
ctx.strokeStyle = ACCENT;
|
|
1324
|
+
ctx.lineWidth = 3;
|
|
1325
|
+
ctx.setLineDash([8, 8]);
|
|
1326
|
+
ctx.beginPath();
|
|
1327
|
+
ctx.moveTo(cx - 200, cy - 30);
|
|
1328
|
+
ctx.quadraticCurveTo(cx, cy - 140, cx + 200, cy - 30);
|
|
1329
|
+
ctx.stroke();
|
|
1330
|
+
ctx.setLineDash([]);
|
|
1331
|
+
|
|
1332
|
+
// Center compass / timeline knot
|
|
1333
|
+
sCircle(cx, cy - 85, 24, 820, ACCENT, 3, IS_LIGHT ? '#ffffff' : 'rgba(15, 23, 42, 0.9)');
|
|
1334
|
+
sCircle(cx, cy - 85, 10, 821, STROKE_COLOR, 2, STROKE_COLOR);
|
|
1335
|
+
ctx.restore();
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
} else if (COMP_THEME === 'doodle_shiver_cold') {
|
|
1339
|
+
// Narrative beat progression based strictly on audio duration (window.DURATION)
|
|
1340
|
+
const dur = window.DURATION || 3;
|
|
1341
|
+
const beat2 = spring(t, dur * 0.45, 160, 15);
|
|
1342
|
+
|
|
1343
|
+
// Natural ground line
|
|
1344
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
|
|
1345
|
+
|
|
1346
|
+
// Freezing wind streaks (rush during cold beat, calm as sun breaks)
|
|
1347
|
+
const windAlpha = Math.max(0, 1 - beat2 * 0.85);
|
|
1348
|
+
if (windAlpha > 0.05) {
|
|
1349
|
+
for (let w = 0; w < 14; w++) {
|
|
1350
|
+
const windX = ((t * 750 + w * 210) % (W + 600)) - 300;
|
|
1351
|
+
const windY = cy - 250 + (w * 42) + Math.sin(t * 3.5 + w) * 15;
|
|
1352
|
+
const windColor = (IS_LIGHT ? 'rgba(100,116,139,' : 'rgba(255,255,255,') + (0.35 * windAlpha) + ')';
|
|
1353
|
+
sLine(windX, windY, windX + 170, windY + 5, 200 + w, windColor, 2);
|
|
1354
|
+
}
|
|
1189
1355
|
}
|
|
1190
1356
|
|
|
1191
1357
|
// Drifting snow particles
|
|
1192
|
-
for (let sp = 0; sp <
|
|
1193
|
-
const px = ((sp * 85 + t *
|
|
1194
|
-
const py = (sp * 45 + t *
|
|
1195
|
-
sCircle(px, py, 2.
|
|
1358
|
+
for (let sp = 0; sp < 22; sp++) {
|
|
1359
|
+
const px = ((sp * 85 + t * 400) % W);
|
|
1360
|
+
const py = (sp * 45 + t * 80) % H;
|
|
1361
|
+
sCircle(px, py, 2.2, 300 + sp, '#e0f2fe', 1, '#ffffff');
|
|
1196
1362
|
}
|
|
1197
1363
|
|
|
1198
|
-
// Shivering breath vapor cloud (calms in Beat
|
|
1199
|
-
if (
|
|
1364
|
+
// Shivering breath vapor cloud (calms in Beat 2)
|
|
1365
|
+
if (beat2 < 0.7) {
|
|
1200
1366
|
const breathPhase = (t * 2.2) % 1;
|
|
1201
1367
|
const bCloudX = cx - 50 + breathPhase * 60;
|
|
1202
1368
|
const bCloudY = cy - 90 - breathPhase * 25;
|
|
1203
|
-
ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.
|
|
1369
|
+
ctx.fillStyle = 'rgba(224, 242, 254, ' + ((1 - breathPhase) * 0.45) + ')';
|
|
1204
1370
|
ctx.beginPath();
|
|
1205
1371
|
ctx.arc(bCloudX, bCloudY, 15 + breathPhase * 25, 0, Math.PI * 2);
|
|
1206
1372
|
ctx.fill();
|
|
1207
1373
|
}
|
|
1208
1374
|
|
|
1209
|
-
//
|
|
1210
|
-
|
|
1375
|
+
// Warm golden sunlight breaks in (visual myth-busting revelation)
|
|
1376
|
+
if (beat2 > 0.05) {
|
|
1377
|
+
ctx.save();
|
|
1378
|
+
ctx.globalAlpha = Math.min(0.6, beat2 * 0.6);
|
|
1379
|
+
const sunBeamGrad = ctx.createRadialGradient(cx + 180, cy - 220, 20, cx + 180, cy - 100, 320);
|
|
1380
|
+
sunBeamGrad.addColorStop(0, 'rgba(251, 191, 36, 0.4)');
|
|
1381
|
+
sunBeamGrad.addColorStop(1, 'rgba(251, 191, 36, 0)');
|
|
1382
|
+
ctx.fillStyle = sunBeamGrad;
|
|
1383
|
+
ctx.beginPath();
|
|
1384
|
+
ctx.arc(cx + 180, cy - 180, 260, 0, Math.PI * 2);
|
|
1385
|
+
ctx.fill();
|
|
1386
|
+
ctx.restore();
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Main character: shivers in cold beat, then pauses and looks at arms in wonder as sun illuminates
|
|
1390
|
+
const isShivering = beat2 < 0.6;
|
|
1211
1391
|
const vibX = isShivering ? Math.sin(t * 38) * 4.5 : 0;
|
|
1212
1392
|
const vibY = isShivering ? Math.cos(t * 32) * 2 : 0;
|
|
1213
1393
|
drawDoodleCharacter({
|
|
1214
|
-
x: cx -
|
|
1394
|
+
x: cx - 100 + beat2 * 80 + vibX,
|
|
1215
1395
|
y: cy + 140 + vibY,
|
|
1216
1396
|
scale: 1.35,
|
|
1217
1397
|
hairStyle: 'wild',
|
|
@@ -1221,38 +1401,6 @@ function drawMainScene(t) {
|
|
|
1221
1401
|
seed: 102,
|
|
1222
1402
|
});
|
|
1223
1403
|
|
|
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
|
-
}
|
|
1255
|
-
|
|
1256
1404
|
} else if (COMP_THEME === 'doodle_hunter_savannah') {
|
|
1257
1405
|
// Beat 1: Walking with spear -> Beat 2: Swatting mosquitoes -> Beat 3: Pausing alert
|
|
1258
1406
|
const walkProg = Math.min(1, Math.max(0, t / window.DURATION));
|
|
@@ -1369,15 +1517,12 @@ function drawMainScene(t) {
|
|
|
1369
1517
|
ctx.restore();
|
|
1370
1518
|
}
|
|
1371
1519
|
|
|
1372
|
-
//
|
|
1520
|
+
// Ancient stone strata and bone needle engraving in Beat 3
|
|
1373
1521
|
if (badgeSpring > 0.05) {
|
|
1374
1522
|
ctx.save();
|
|
1375
|
-
ctx.globalAlpha = Math.min(
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
ctx.fillStyle = '#fef08a';
|
|
1379
|
-
ctx.textAlign = 'center';
|
|
1380
|
-
ctx.fillText('BONE NEEDLE • 120,000 YEARS', cx + 270, cy + 167);
|
|
1523
|
+
ctx.globalAlpha = Math.min(0.65, badgeSpring * 0.65);
|
|
1524
|
+
sLine(cx + 140, cy + 150, cx + 420, cy + 150, 670, '#d4af37', 2);
|
|
1525
|
+
sLine(cx + 180, cy + 168, cx + 380, cy + 168, 671, '#d4af37', 1.5);
|
|
1381
1526
|
ctx.restore();
|
|
1382
1527
|
}
|
|
1383
1528
|
|
|
@@ -1452,12 +1597,8 @@ function drawMainScene(t) {
|
|
|
1452
1597
|
ctx.translate(haloX, haloY);
|
|
1453
1598
|
ctx.scale(haloSpring, haloSpring);
|
|
1454
1599
|
sCircle(0, 0, 48, 760, '#38bdf8', 3);
|
|
1455
|
-
sCircle(0, 0,
|
|
1600
|
+
sCircle(0, 0, 24, 761, STROKE_ALT, 2);
|
|
1456
1601
|
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
1602
|
ctx.restore();
|
|
1462
1603
|
}
|
|
1463
1604
|
|
|
@@ -1467,7 +1608,7 @@ function drawMainScene(t) {
|
|
|
1467
1608
|
const greet = spring(t, window.DURATION * 0.38, 190, 15);
|
|
1468
1609
|
const arcSpring = spring(t, window.DURATION * 0.68, 200, 15);
|
|
1469
1610
|
|
|
1470
|
-
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10,
|
|
1611
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
|
|
1471
1612
|
|
|
1472
1613
|
// Left Clan Member walks toward center
|
|
1473
1614
|
const leftX = cx - 380 + Math.min(1, approach) * 160;
|
|
@@ -1495,10 +1636,10 @@ function drawMainScene(t) {
|
|
|
1495
1636
|
seed: 107,
|
|
1496
1637
|
});
|
|
1497
1638
|
|
|
1498
|
-
// Glowing connection arc
|
|
1639
|
+
// Glowing connection arc in Beat 3
|
|
1499
1640
|
if (arcSpring > 0.05) {
|
|
1500
1641
|
ctx.save();
|
|
1501
|
-
ctx.globalAlpha = Math.min(
|
|
1642
|
+
ctx.globalAlpha = Math.min(0.85, arcSpring * 0.85);
|
|
1502
1643
|
ctx.strokeStyle = ACCENT;
|
|
1503
1644
|
ctx.lineWidth = 3;
|
|
1504
1645
|
ctx.setLineDash([8, 8]);
|
|
@@ -1508,13 +1649,8 @@ function drawMainScene(t) {
|
|
|
1508
1649
|
ctx.stroke();
|
|
1509
1650
|
ctx.setLineDash([]);
|
|
1510
1651
|
|
|
1511
|
-
sCircle(cx, cy - 65,
|
|
1512
|
-
|
|
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);
|
|
1652
|
+
sCircle(cx, cy - 65, 28, 800, ACCENT, 3, IS_LIGHT ? '#ffffff' : 'rgba(15, 23, 42, 0.85)');
|
|
1653
|
+
sCircle(cx, cy - 65, 12, 801, STROKE_COLOR, 2, STROKE_COLOR);
|
|
1518
1654
|
ctx.restore();
|
|
1519
1655
|
}
|
|
1520
1656
|
|
|
@@ -1534,7 +1670,7 @@ function drawMainScene(t) {
|
|
|
1534
1670
|
ctx.fill();
|
|
1535
1671
|
|
|
1536
1672
|
// Large rock boulder for thinker
|
|
1537
|
-
ctx.fillStyle = '#334155';
|
|
1673
|
+
ctx.fillStyle = IS_LIGHT ? '#78716c' : '#334155';
|
|
1538
1674
|
ctx.beginPath();
|
|
1539
1675
|
ctx.ellipse(cx - 140, cy + 175, 85, 55, 0, 0, Math.PI * 2);
|
|
1540
1676
|
ctx.fill();
|
|
@@ -1559,9 +1695,9 @@ function drawMainScene(t) {
|
|
|
1559
1695
|
ctx.translate(cloudX, cloudY);
|
|
1560
1696
|
ctx.scale(cloudSpring, cloudSpring);
|
|
1561
1697
|
|
|
1562
|
-
sCircle(-180, 160, 12, 850,
|
|
1563
|
-
sCircle(-140, 120, 18, 851,
|
|
1564
|
-
sCircle(-90, 70, 24, 852,
|
|
1698
|
+
sCircle(-180, 160, 12, 850, STROKE_COLOR, 2, '#ffffff');
|
|
1699
|
+
sCircle(-140, 120, 18, 851, STROKE_COLOR, 2, '#ffffff');
|
|
1700
|
+
sCircle(-90, 70, 24, 852, STROKE_COLOR, 2, '#ffffff');
|
|
1565
1701
|
|
|
1566
1702
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
|
1567
1703
|
ctx.beginPath();
|
|
@@ -1570,6 +1706,9 @@ function drawMainScene(t) {
|
|
|
1570
1706
|
ctx.arc(70, 0, 55, 0, Math.PI * 2);
|
|
1571
1707
|
ctx.arc(0, 30, 60, 0, Math.PI * 2);
|
|
1572
1708
|
ctx.fill();
|
|
1709
|
+
ctx.strokeStyle = STROKE_COLOR;
|
|
1710
|
+
ctx.lineWidth = 2.5;
|
|
1711
|
+
ctx.stroke();
|
|
1573
1712
|
|
|
1574
1713
|
// Inside thought cloud: Tomorrow's sunrise & stretched hide on rack in Beat 3
|
|
1575
1714
|
if (ideaSpring > 0.1) {
|
|
@@ -1584,11 +1723,6 @@ function drawMainScene(t) {
|
|
|
1584
1723
|
|
|
1585
1724
|
ctx.fillStyle = '#b45309';
|
|
1586
1725
|
ctx.fillRect(-28, -20, 56, 32);
|
|
1587
|
-
|
|
1588
|
-
ctx.font = 'bold 15px monospace';
|
|
1589
|
-
ctx.fillStyle = '#d97706';
|
|
1590
|
-
ctx.textAlign = 'center';
|
|
1591
|
-
ctx.fillText('💡 PLANNING FOR TOMORROW', 0, 52);
|
|
1592
1726
|
}
|
|
1593
1727
|
ctx.restore();
|
|
1594
1728
|
}
|
|
@@ -1598,7 +1732,7 @@ function drawMainScene(t) {
|
|
|
1598
1732
|
const scopeSpring = spring(t, window.DURATION * 0.30, 180, 15);
|
|
1599
1733
|
const treeSpring = spring(t, window.DURATION * 0.65, 200, 15);
|
|
1600
1734
|
|
|
1601
|
-
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10,
|
|
1735
|
+
sLine(cx - W * 0.45, cy + 220, cx + W * 0.45, cy + 220, 10, AMBIENT_LINE, 2);
|
|
1602
1736
|
|
|
1603
1737
|
// Early human scratching pelt with itch lines
|
|
1604
1738
|
drawDoodleCharacter({
|
|
@@ -1647,28 +1781,18 @@ function drawMainScene(t) {
|
|
|
1647
1781
|
sLine(-100, 0, -20, 0, 940, '#f8fafc', 4);
|
|
1648
1782
|
sCircle(-20, 0, 7, 941, '#d4af37', 2, '#d4af37');
|
|
1649
1783
|
|
|
1650
|
-
sLine(-20, 0,
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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);
|
|
1784
|
+
sLine(-20, 0, 75, -45, 942, '#38bdf8', 4);
|
|
1785
|
+
sCircle(82, -45, 8, 943, '#38bdf8', 2, '#38bdf8');
|
|
1786
|
+
|
|
1787
|
+
sLine(-20, 0, 75, 45, 944, '#f43f5e', 4);
|
|
1788
|
+
sCircle(82, 45, 8, 945, '#f43f5e', 2, '#f43f5e');
|
|
1665
1789
|
}
|
|
1666
1790
|
ctx.restore();
|
|
1667
1791
|
}
|
|
1668
1792
|
|
|
1669
1793
|
} else if (COMP_THEME === 'doodle_general_human') {
|
|
1670
1794
|
// General Expressive Doodled Human Narrator
|
|
1671
|
-
sLine(cx - W * 0.4, cy + 220, cx + W * 0.4, cy + 220, 10,
|
|
1795
|
+
sLine(cx - W * 0.4, cy + 220, cx + W * 0.4, cy + 220, 10, AMBIENT_LINE, 2);
|
|
1672
1796
|
drawDoodleCharacter({
|
|
1673
1797
|
x: cx,
|
|
1674
1798
|
y: cy + 140,
|
package/src/workflows/assets.js
CHANGED
|
@@ -116,7 +116,8 @@ async function runAssets({
|
|
|
116
116
|
camera: scene.camera_hint || scene.camera || 'static',
|
|
117
117
|
focalPoint: scene.focal_point || null,
|
|
118
118
|
narration: scene.narration || '',
|
|
119
|
-
|
|
119
|
+
prompt: scene.prompt || '',
|
|
120
|
+
visualDescription: scene.visual_description || scene.prompt || scene.narration || '',
|
|
120
121
|
topic: scene.topic || topic || '',
|
|
121
122
|
style: scene.style || style || '',
|
|
122
123
|
theme: scene.theme || theme || '',
|
package/src/workflows/ingest.js
CHANGED
|
@@ -167,8 +167,11 @@ function normaliseIngestScene(raw, index, baseDir = process.cwd()) {
|
|
|
167
167
|
schema_version: '1.0.0',
|
|
168
168
|
narration,
|
|
169
169
|
caption,
|
|
170
|
+
prompt: String(raw.prompt || '').trim(),
|
|
171
|
+
style: String(raw.style || '').trim(),
|
|
172
|
+
theme: String(raw.theme || '').trim(),
|
|
170
173
|
// visual_description used by AI image generation — blank means no AI image needed
|
|
171
|
-
visual_description: String(raw.visual_description || raw.description || narration || '').trim(),
|
|
174
|
+
visual_description: String(raw.visual_description || raw.description || raw.prompt || narration || '').trim(),
|
|
172
175
|
video_path: videoPath,
|
|
173
176
|
image_path: imagePath,
|
|
174
177
|
audio_path: audioPath,
|