@amg555/frameforge-mcp 2.4.7 → 2.4.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amg555/frameforge-mcp",
3
- "version": "2.4.7",
3
+ "version": "2.4.8",
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"
@@ -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.7', code.includes("version: '2.4.7'"));
23
+ check('SERVER_INFO version 2.4.8', code.includes("version: '2.4.8'"));
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.7', cfWorker.includes("'2.4.7'"));
302
+ check('cf-worker version 2.4.8', cfWorker.includes("'2.4.8'"));
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 ────────────────────────────────────────────────
@@ -23,6 +23,20 @@ export const VIRTUAL_CLOCK_INJECTION = `
23
23
 
24
24
  // 1. Deterministic clocks
25
25
  performance.now = () => _now;
26
+ const OriginalDate = window.Date;
27
+ class VirtualDate extends OriginalDate {
28
+ constructor(...args) {
29
+ if (args.length === 0) {
30
+ super(_startDate + _now);
31
+ } else {
32
+ super(...args);
33
+ }
34
+ }
35
+ static now() {
36
+ return Math.floor(_startDate + _now);
37
+ }
38
+ }
39
+ window.Date = VirtualDate;
26
40
  Date.now = () => _startDate + _now;
27
41
 
28
42
  // 2. Deterministic requestAnimationFrame
@@ -60,6 +74,9 @@ export const VIRTUAL_CLOCK_INJECTION = `
60
74
 
61
75
  // 4. Deterministic Seeded PRNG fallback for unseeded Math.random()
62
76
  let _seed = 0x2F6E2B1;
77
+ const resetSeed = (tSec) => {
78
+ _seed = (0x2F6E2B1 ^ Math.imul(Math.floor(tSec * 1000), 0x9E3779B9)) | 0;
79
+ };
63
80
  window.Math.random = () => {
64
81
  _seed = (_seed + 0x6D2B79F5) | 0;
65
82
  let t = Math.imul(_seed ^ (_seed >>> 15), 1 | _seed);
@@ -84,16 +101,30 @@ export const VIRTUAL_CLOCK_INJECTION = `
84
101
  currentRafs.forEach(cb => { try { cb(_now); } catch (_) {} });
85
102
 
86
103
  try {
87
- document.getAnimations().forEach(a => {
88
- if (a.playState !== 'paused') a.pause();
89
- a.currentTime = _now;
90
- });
104
+ if (document.getAnimations) {
105
+ document.getAnimations().forEach(a => {
106
+ if (a.playState !== 'paused') a.pause();
107
+ a.currentTime = _now;
108
+ });
109
+ }
91
110
  } catch (_) {}
92
111
  };
93
112
 
94
113
  // 6. Absolute virtual seek
95
114
  window.__seek = async (t) => {
96
115
  _now = t * 1000;
116
+ resetSeed(t);
117
+
118
+ // Pause & seek CSS animations
119
+ try {
120
+ if (document.getAnimations) {
121
+ document.getAnimations().forEach(a => {
122
+ if (a.playState !== 'paused') a.pause();
123
+ a.currentTime = _now;
124
+ });
125
+ }
126
+ } catch (_) {}
127
+
97
128
  if (window.renderAt) {
98
129
  await window.renderAt(t);
99
130
  } else if (window.seek) {
@@ -1641,7 +1641,7 @@ Allows the agent in Claude Code, Cursor, or any IDE to inspect the visual output
1641
1641
 
1642
1642
  const SERVER_INFO = {
1643
1643
  name: 'video-pipeline',
1644
- version: '2.4.7',
1644
+ version: '2.4.8',
1645
1645
  };
1646
1646
 
1647
1647
  const CAPABILITIES = {
@@ -148,6 +148,7 @@ class VideoRenderer {
148
148
  voicePath,
149
149
  caption = '',
150
150
  narration = '',
151
+ prompt = '',
151
152
  visualDescription = '',
152
153
  visualType = 'code_animation',
153
154
  topic = '',
@@ -277,6 +278,7 @@ class VideoRenderer {
277
278
  voicePath,
278
279
  caption,
279
280
  narration,
281
+ prompt,
280
282
  visualDescription,
281
283
  visualType,
282
284
  topic,
@@ -362,6 +364,7 @@ class VideoRenderer {
362
364
  voicePath,
363
365
  caption,
364
366
  narration,
367
+ prompt = '',
365
368
  visualDescription,
366
369
  visualType = 'code_animation',
367
370
  topic = '',
@@ -403,6 +406,7 @@ class VideoRenderer {
403
406
  htmlContent = generateSceneHtml({
404
407
  caption,
405
408
  narration,
409
+ prompt,
406
410
  visualDescription,
407
411
  imagePath,
408
412
  topic,
@@ -445,6 +449,19 @@ class VideoRenderer {
445
449
  throw new Error(`Harness frame rendering failed: ${(res.stderr || res.stdout || '').slice(-400)}`);
446
450
  }
447
451
 
452
+ // Closed-loop visual frame inspection (catches blank/blown frames before encoding)
453
+ try {
454
+ const { analyzeImageFile } = require('../harness/lib/pixels.mjs');
455
+ const midFrameIdx = Math.floor(lastFrame / 2);
456
+ const midFrameFile = path.join(framesDir, `f${String(midFrameIdx).padStart(6, '0')}.jpg`);
457
+ if (fs.existsSync(midFrameFile)) {
458
+ const analysis = analyzeImageFile(midFrameFile, renderWidth, renderHeight);
459
+ if (analysis.isBlank) {
460
+ console.warn(`[Renderer] ⚠️ Scene ${sceneIndex} visual warning: blank frame at t=${(midFrameIdx / fps).toFixed(2)}s`);
461
+ }
462
+ }
463
+ } catch (_) {}
464
+
448
465
  const framesPattern = path.join(framesDir, 'f%06d.jpg');
449
466
  const encResult = spawnSync('ffmpeg', [
450
467
  '-nostdin', '-y', '-threads', '2',
@@ -343,7 +343,25 @@ function detectComponentTheme({ visualDescription = '', visual_description = '',
343
343
  return 'doodle_dressing';
344
344
  }
345
345
 
346
- // 10. General Human Character
346
+ // 10. Map / Migration / Ancient World / Out of Africa (Scene 10)
347
+ if (/(?:^|[^\w])(migration|migrating|migrate|map with africa|africa highlighted|colder regions|climate illustrations|out of africa|geographic migration|migration arrows|continent map|world map)(?:[^\w]|$)/i.test(text)
348
+ || /കുടിയേറുന്നതിന്|കുടിയേറ്റം|ആഫ്രിക്ക|ഭൂപടത്തിൽ ആഫ്രിക്ക|മൈഗ്രേഷൻ|തണുപ്പുള്ള പ്രദേശങ്ങളിലേക്ക് കുടിയേറാൻ|ഭൂപടം|ഭൂഖണ്ഡം/.test(text)) {
349
+ return 'doodle_map_migration';
350
+ }
351
+
352
+ // 11. Royal Robes / Social Status / Ornaments / Crown / Wealth & Authority (Scene 16)
353
+ if (/(?:^|[^\w])(royal robe|royal robes|crown|sceptre|ornaments|status and power|hierarchy|wealth authority|noble clothing|decorated garments)(?:[^\w]|$)/i.test(text)
354
+ || /സമ്പത്തിന്റെയും അധികാരത്തിന്റെയും|രാജകീയ|കിരീടം|സാമൂഹിക സ്ഥാനം|അധികാരത്തിന്റെ അടയാളം/.test(text)) {
355
+ return 'doodle_royal_status';
356
+ }
357
+
358
+ // 12. Outro / Subscribe / Notification Bell / Channel Engagement (Scene 25)
359
+ if (/(?:^|[^\w])(subscribe|notification bell|youtube outro|channel subscribe|outro|subscribe button|like and subscribe)(?:[^\w]|$)/i.test(text)
360
+ || /സബ്സ്ക്രൈബ്|ചാനൽ സബ്സ്ക്രൈബ്|സബ്സ്ക്രൈബ് ചെയ്യൂ|ബെൽ ഐക്കൺ|നന്ദി/.test(text)) {
361
+ return 'doodle_outro_subscribe';
362
+ }
363
+
364
+ // 13. General Human Character
347
365
  if (/(?:^|[^\w])(human|person|man|woman|people|child|guy|doodle|character|actor)(?:[^\w]|$)/i.test(text)
348
366
  || /മനുഷ്യൻ|ആളുകൾ|വ്യക്തി/.test(text)) {
349
367
  return 'doodle_general_human';
@@ -575,6 +593,7 @@ function generateSceneHtml({
575
593
  const aesthetic = AESTHETICS[aestheticKey] || AESTHETICS.cyber_tech;
576
594
  const compTheme = detectComponentTheme({
577
595
  visualDescription,
596
+ prompt,
578
597
  caption,
579
598
  narration,
580
599
  topic,
@@ -2510,6 +2529,338 @@ function drawMainScene(t) {
2510
2529
  showShadow: true,
2511
2530
  seed: 110,
2512
2531
  });
2532
+ } else if (COMP_THEME === 'doodle_map_migration') {
2533
+ // Scene 10: Hand-Drawn Migration Map / Out of Africa / Ancient World Routes
2534
+ const mapSpring = spring(t, 0.05, 170, 14);
2535
+ const arrowSpring = spring(t, window.DURATION * 0.35, 180, 15);
2536
+ const walkProg = Math.min(1, Math.max(0, (t - window.DURATION * 0.2) / (window.DURATION * 0.75)));
2537
+
2538
+ // Antique parchment background wash
2539
+ ctx.save();
2540
+ ctx.fillStyle = IS_LIGHT ? '#faf5ec' : '#1c1917';
2541
+ ctx.fillRect(0, 0, W, H);
2542
+
2543
+ // Decorative map grid lines (latitude/longitude in subtle ink)
2544
+ ctx.strokeStyle = IS_LIGHT ? 'rgba(180, 83, 9, 0.12)' : 'rgba(255, 255, 255, 0.1)';
2545
+ ctx.lineWidth = 1.5;
2546
+ ctx.setLineDash([6, 6]);
2547
+ for (let gx = 100; gx < W; gx += 200) {
2548
+ ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, H); ctx.stroke();
2549
+ }
2550
+ for (let gy = 80; gy < H; gy += 160) {
2551
+ ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(W, gy); ctx.stroke();
2552
+ }
2553
+ ctx.setLineDash([]);
2554
+
2555
+ // Hand-drawn stylized continent outlines (Africa, Europe, Asia)
2556
+ const mapCenter = cx - 40;
2557
+ const mapY = cy - 20;
2558
+
2559
+ // 1. Africa Outline (Highlighted in warm terracotta wash)
2560
+ ctx.save();
2561
+ ctx.fillStyle = 'rgba(217, 119, 6, 0.20)';
2562
+ ctx.beginPath();
2563
+ ctx.moveTo(mapCenter - 140, mapY - 60); // West bulge
2564
+ ctx.bezierCurveTo(mapCenter - 160, mapY + 20, mapCenter - 80, mapY + 140, mapCenter - 40, mapY + 220); // South tip (Good Hope)
2565
+ ctx.bezierCurveTo(mapCenter + 10, mapY + 160, mapCenter + 40, mapY + 60, mapCenter + 60, mapY - 20); // Horn of Africa
2566
+ ctx.bezierCurveTo(mapCenter + 30, mapY - 60, mapCenter - 20, mapY - 80, mapCenter - 140, mapY - 60); // North Coast
2567
+ ctx.closePath();
2568
+ ctx.fill();
2569
+
2570
+ // Outline stroke with ink wobble
2571
+ sLine(mapCenter - 140, mapY - 60, mapCenter - 160, mapY + 20, 710, '#c2410c', 3);
2572
+ sLine(mapCenter - 160, mapY + 20, mapCenter - 40, mapY + 220, 711, '#c2410c', 3);
2573
+ sLine(mapCenter - 40, mapY + 220, mapCenter + 60, mapY - 20, 712, '#c2410c', 3);
2574
+ sLine(mapCenter + 60, mapY - 20, mapCenter - 140, mapY - 60, 713, '#c2410c', 3);
2575
+
2576
+ // Africa label badge
2577
+ ctx.fillStyle = '#b45309';
2578
+ ctx.font = '700 22px ' + FONT_DISPLAY;
2579
+ ctx.textAlign = 'center';
2580
+ ctx.fillText('AFRICA', mapCenter - 40, mapY + 60);
2581
+
2582
+ // Sun icon over Africa
2583
+ sCircle(mapCenter - 90, mapY + 40, 18, 714, '#d97706', 2.5, '#fef08a');
2584
+ for (let r = 0; r < 6; r++) {
2585
+ const ra = (r / 6) * Math.PI * 2 + t * 0.5;
2586
+ const rx1 = mapCenter - 90 + Math.cos(ra) * 22;
2587
+ const ry1 = mapY + 40 + Math.sin(ra) * 22;
2588
+ const rx2 = mapCenter - 90 + Math.cos(ra) * 32;
2589
+ const ry2 = mapY + 40 + Math.sin(ra) * 32;
2590
+ sLine(rx1, ry1, rx2, ry2, 715 + r, '#d97706', 2);
2591
+ }
2592
+
2593
+ // 2. Eurasia / Northern Territory Outline (Colder destination)
2594
+ ctx.fillStyle = 'rgba(56, 189, 248, 0.12)';
2595
+ ctx.beginPath();
2596
+ ctx.moveTo(mapCenter - 130, mapY - 90);
2597
+ ctx.bezierCurveTo(mapCenter - 120, mapY - 200, mapCenter + 60, mapY - 220, mapCenter + 220, mapY - 170); // Northern Eurasia
2598
+ ctx.bezierCurveTo(mapCenter + 320, mapY - 130, mapCenter + 360, mapY - 40, mapCenter + 260, mapY + 10); // East Asia
2599
+ ctx.bezierCurveTo(mapCenter + 140, mapY - 30, mapCenter + 40, mapY - 70, mapCenter - 130, mapY - 90);
2600
+ ctx.closePath();
2601
+ ctx.fill();
2602
+
2603
+ sLine(mapCenter - 130, mapY - 90, mapCenter + 60, mapY - 210, 725, '#0284c7', 3);
2604
+ sLine(mapCenter + 60, mapY - 210, mapCenter + 280, mapY - 160, 726, '#0284c7', 3);
2605
+ sLine(mapCenter + 280, mapY - 160, mapCenter + 240, mapY + 10, 727, '#0284c7', 3);
2606
+
2607
+ // Snowflake climate icons over Eurasia
2608
+ for (let sf = 0; sf < 3; sf++) {
2609
+ const sfx = mapCenter + 60 + sf * 110;
2610
+ const sfy = mapY - 160 + (sf % 2) * 35;
2611
+ sLine(sfx - 14, sfy, sfx + 14, sfy, 730 + sf, '#0284c7', 2);
2612
+ sLine(sfx, sfy - 14, sfx, sfy + 14, 733 + sf, '#0284c7', 2);
2613
+ sLine(sfx - 10, sfy - 10, sfx + 10, sfy + 10, 736 + sf, '#0284c7', 2);
2614
+ sLine(sfx - 10, sfy + 10, sfx + 10, sfy - 10, 739 + sf, '#0284c7', 2);
2615
+ }
2616
+ ctx.restore();
2617
+
2618
+ // 3. Dynamic Migration Route Curves with Pulsing Arrowheads
2619
+ if (arrowSpring > 0.05) {
2620
+ ctx.save();
2621
+ ctx.strokeStyle = '#c2410c';
2622
+ ctx.lineWidth = 4;
2623
+ ctx.setLineDash([10, 8]);
2624
+
2625
+ // Route 1: Out of Africa -> Europe
2626
+ ctx.beginPath();
2627
+ ctx.moveTo(mapCenter + 20, mapY - 40);
2628
+ ctx.quadraticCurveTo(mapCenter - 20, mapY - 120, mapCenter - 60, mapY - 160);
2629
+ ctx.stroke();
2630
+
2631
+ // Route 2: Out of Africa -> Asia
2632
+ ctx.beginPath();
2633
+ ctx.moveTo(mapCenter + 40, mapY - 35);
2634
+ ctx.quadraticCurveTo(mapCenter + 120, mapY - 80, mapCenter + 220, mapY - 120);
2635
+ ctx.stroke();
2636
+ ctx.setLineDash([]);
2637
+
2638
+ // Pulsing migration markers
2639
+ const pulseR = 6 + Math.sin(t * 6) * 3;
2640
+ sCircle(mapCenter + 30, mapY - 40, pulseR, 750, '#ea580c', 2, '#fbbf24');
2641
+ sCircle(mapCenter - 60, mapY - 160, pulseR, 751, '#0284c7', 2, '#38bdf8');
2642
+ sCircle(mapCenter + 220, mapY - 120, pulseR, 752, '#0284c7', 2, '#38bdf8');
2643
+ ctx.restore();
2644
+ }
2645
+
2646
+ // 4. Walking Doodle Explorer on the Migration Trail
2647
+ const trailStartX = mapCenter + 10;
2648
+ const trailStartY = mapY + 20;
2649
+ const trailEndX = mapCenter + 190;
2650
+ const trailEndY = mapY - 100;
2651
+ const curNomadX = trailStartX + (trailEndX - trailStartX) * walkProg;
2652
+ const curNomadY = trailStartY + (trailEndY - trailStartY) * walkProg;
2653
+
2654
+ drawDoodleCharacter({
2655
+ x: curNomadX,
2656
+ y: curNomadY + 120,
2657
+ scale: 1.15,
2658
+ hairStyle: 'wild',
2659
+ expression: 'focused',
2660
+ clothing: walkProg > 0.5 ? 'pelt' : 'none',
2661
+ armPose: 'hold_spear',
2662
+ showShadow: true,
2663
+ seed: 760,
2664
+ });
2665
+
2666
+ } else if (COMP_THEME === 'doodle_royal_status') {
2667
+ // Scene 16: Evolution of Clothing into Royal Mantles, Crowns & Social Status
2668
+ const crownSpring = spring(t, window.DURATION * 0.32, 190, 15);
2669
+ const arrowRise = spring(t, window.DURATION * 0.55, 170, 14);
2670
+
2671
+ // Warm dais platform
2672
+ ctx.fillStyle = IS_LIGHT ? '#faf7f2' : '#1c1917';
2673
+ ctx.fillRect(0, 0, W, H);
2674
+
2675
+ ctx.fillStyle = '#451a03';
2676
+ ctx.fillRect(0, cy + 140, W, H - (cy + 140));
2677
+ sLine(0, cy + 140, W, cy + 140, 770, '#d97706', 3.5);
2678
+
2679
+ // Prehistoric Human on Left (Simple pelt, admiring transformation)
2680
+ drawDoodleCharacter({
2681
+ x: cx - 260,
2682
+ y: cy + 140,
2683
+ scale: 1.3,
2684
+ hairStyle: 'wild',
2685
+ expression: 'curious',
2686
+ clothing: 'pelt',
2687
+ armPose: 'thinker',
2688
+ showShadow: true,
2689
+ seed: 771,
2690
+ });
2691
+
2692
+ // Royal Monarch on Right (Majestic regal cape / robe)
2693
+ const kingX = cx + 220;
2694
+ drawDoodleCharacter({
2695
+ x: kingX,
2696
+ y: cy + 140,
2697
+ scale: 1.35,
2698
+ hairStyle: 'modern',
2699
+ expression: 'smiling',
2700
+ clothing: 'cloak',
2701
+ armPose: 'peace_gesture',
2702
+ showShadow: true,
2703
+ seed: 772,
2704
+ });
2705
+
2706
+ // Ornate Golden Crown with Ruby jewels appearing above Monarch
2707
+ if (crownSpring > 0.05) {
2708
+ ctx.save();
2709
+ ctx.translate(kingX, cy - 20);
2710
+ ctx.scale(crownSpring, crownSpring);
2711
+
2712
+ // Golden crown base & peaks
2713
+ ctx.fillStyle = '#f59e0b';
2714
+ ctx.beginPath();
2715
+ ctx.moveTo(-36, 0);
2716
+ ctx.lineTo(-40, -32);
2717
+ ctx.lineTo(-20, -18);
2718
+ ctx.lineTo(0, -42);
2719
+ ctx.lineTo(20, -18);
2720
+ ctx.lineTo(40, -32);
2721
+ ctx.lineTo(36, 0);
2722
+ ctx.closePath();
2723
+ ctx.fill();
2724
+ ctx.strokeStyle = '#b45309';
2725
+ ctx.lineWidth = 2.5;
2726
+ ctx.stroke();
2727
+
2728
+ // Jewels on crown peaks
2729
+ sCircle(-40, -32, 4, 780, '#b91c1c', 1.5, '#ef4444');
2730
+ sCircle(0, -42, 5, 781, '#b91c1c', 1.5, '#ef4444');
2731
+ sCircle(40, -32, 4, 782, '#b91c1c', 1.5, '#ef4444');
2732
+
2733
+ // Golden Sceptre in hand
2734
+ sLine(42, 20, 52, 130, 785, '#d97706', 4.5);
2735
+ sCircle(42, 16, 10, 786, '#d97706', 2, '#fbbf24');
2736
+ ctx.restore();
2737
+ }
2738
+
2739
+ // Status & Power indicator arrows rising centrally
2740
+ if (arrowRise > 0.05) {
2741
+ ctx.save();
2742
+ ctx.globalAlpha = Math.min(1, arrowRise);
2743
+ for (let arr = 0; arr < 3; arr++) {
2744
+ const arrX = cx - 30 + arr * 40;
2745
+ const arrY = cy + 40 - arr * 35 - Math.sin(t * 4 + arr) * 10;
2746
+ sLine(arrX, arrY + 40, arrX, arrY, 790 + arr, '#d97706', 3.5);
2747
+ sLine(arrX - 12, arrY + 14, arrX, arrY, 793 + arr, '#d97706', 3.5);
2748
+ sLine(arrX + 12, arrY + 14, arrX, arrY, 796 + arr, '#d97706', 3.5);
2749
+ }
2750
+
2751
+ ctx.fillStyle = '#b45309';
2752
+ ctx.font = '700 20px ' + FONT_DISPLAY;
2753
+ ctx.textAlign = 'center';
2754
+ ctx.fillText('STATUS & POWER', cx + 10, cy - 90);
2755
+ ctx.restore();
2756
+ }
2757
+
2758
+ } else if (COMP_THEME === 'doodle_outro_subscribe') {
2759
+ // Scene 25: YouTube Outro / Animated Question Mark / Subscribe & Bell Card
2760
+ const qSpring = spring(t, 0.05, 180, 14);
2761
+ const subSpring = spring(t, window.DURATION * 0.28, 200, 14);
2762
+ const bellSpring = spring(t, window.DURATION * 0.50, 190, 12);
2763
+
2764
+ ctx.fillStyle = IS_LIGHT ? '#faf7f2' : '#1c1917';
2765
+ ctx.fillRect(0, 0, W, H);
2766
+
2767
+ // Warm terrain ground
2768
+ ctx.fillStyle = '#451a03';
2769
+ ctx.fillRect(0, cy + 140, W, H - (cy + 140));
2770
+ sLine(0, cy + 140, W, cy + 140, 810, '#d97706', 3.5);
2771
+
2772
+ // Prehistoric character on left drawing with a reed/quill
2773
+ drawDoodleCharacter({
2774
+ x: cx - 280,
2775
+ y: cy + 140,
2776
+ scale: 1.35,
2777
+ hairStyle: 'wild',
2778
+ expression: 'smiling',
2779
+ clothing: 'pelt',
2780
+ armPose: 'peace_gesture',
2781
+ showShadow: true,
2782
+ seed: 811,
2783
+ });
2784
+
2785
+ // Big bold animated question mark drawn in ink
2786
+ if (qSpring > 0.05) {
2787
+ ctx.save();
2788
+ const qX = cx - 80;
2789
+ const qY = cy - 20;
2790
+ ctx.translate(qX, qY);
2791
+ ctx.scale(qSpring, qSpring);
2792
+
2793
+ ctx.strokeStyle = '#c2410c';
2794
+ ctx.lineWidth = 9;
2795
+ ctx.lineCap = 'round';
2796
+ ctx.beginPath();
2797
+ ctx.arc(0, -60, 44, Math.PI, 0, false);
2798
+ ctx.bezierCurveTo(44, -15, 0, 5, 0, 35);
2799
+ ctx.stroke();
2800
+
2801
+ sCircle(0, 72, 8, 815, '#c2410c', 3, '#c2410c');
2802
+ ctx.restore();
2803
+ }
2804
+
2805
+ // Interactive YouTube Subscribe Pill Button & Swinging Notification Bell
2806
+ if (subSpring > 0.05) {
2807
+ ctx.save();
2808
+ const btnX = cx + 220;
2809
+ const btnY = cy - 20;
2810
+ ctx.translate(btnX, btnY);
2811
+ ctx.scale(subSpring, subSpring);
2812
+
2813
+ // Red Subscribe Pill
2814
+ ctx.fillStyle = '#dc2626';
2815
+ ctx.beginPath();
2816
+ ctx.roundRect(-140, -32, 280, 64, 32);
2817
+ ctx.fill();
2818
+ ctx.strokeStyle = '#991b1b';
2819
+ ctx.lineWidth = 3;
2820
+ ctx.stroke();
2821
+
2822
+ // White Subscribe text
2823
+ ctx.fillStyle = '#ffffff';
2824
+ ctx.font = '700 24px ' + FONT_DISPLAY;
2825
+ ctx.textAlign = 'center';
2826
+ ctx.textBaseline = 'middle';
2827
+ ctx.fillText('SUBSCRIBE', -15, 0);
2828
+
2829
+ // Play icon triangle
2830
+ ctx.beginPath();
2831
+ ctx.moveTo(-90, -12);
2832
+ ctx.lineTo(-72, 0);
2833
+ ctx.lineTo(-90, 12);
2834
+ ctx.closePath();
2835
+ ctx.fill();
2836
+
2837
+ // Notification Bell swinging on spring
2838
+ if (bellSpring > 0.05) {
2839
+ ctx.save();
2840
+ const bellSwing = Math.sin(t * 9) * 0.25 * Math.exp(-(t % 2.5));
2841
+ ctx.translate(180, 0);
2842
+ ctx.rotate(bellSwing);
2843
+
2844
+ // Golden bell body
2845
+ ctx.fillStyle = '#f59e0b';
2846
+ ctx.beginPath();
2847
+ ctx.arc(0, -12, 14, Math.PI, 0);
2848
+ ctx.quadraticCurveTo(18, 12, 22, 16);
2849
+ ctx.lineTo(-22, 16);
2850
+ ctx.quadraticCurveTo(-18, 12, 0, -12);
2851
+ ctx.closePath();
2852
+ ctx.fill();
2853
+ ctx.strokeStyle = '#b45309';
2854
+ ctx.lineWidth = 2.5;
2855
+ ctx.stroke();
2856
+
2857
+ // Bell clapper
2858
+ sCircle(0, 20, 5, 820, '#b45309', 2, '#d97706');
2859
+ ctx.restore();
2860
+ }
2861
+ ctx.restore();
2862
+ }
2863
+
2513
2864
  } else if (COMP_THEME === 'celestial_orbit') {
2514
2865
  // 3D Celestial Body (Earth) with orbiting satellites & signal pulses
2515
2866
  const earthR = Math.min(W, H) * 0.22;
@@ -2530,7 +2881,7 @@ function drawMainScene(t) {
2530
2881
  ctx.beginPath(); ctx.arc(cx, cy, earthR, 0, Math.PI * 2); ctx.fill();
2531
2882
 
2532
2883
  // Rotating longitude meridians
2533
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.22)';
2884
+ ctx.strokeStyle = IS_LIGHT ? 'rgba(28, 25, 23, 0.35)' : 'rgba(255, 255, 255, 0.22)';
2534
2885
  ctx.lineWidth = 1.5;
2535
2886
  for (let m = 0; m < 4; m++) {
2536
2887
  const mAngle = t * 0.5 + (m * Math.PI) / 4;
@@ -2561,7 +2912,7 @@ function drawMainScene(t) {
2561
2912
  ctx.save();
2562
2913
  ctx.translate(cx, cy);
2563
2914
  ctx.rotate(tilt);
2564
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.14)';
2915
+ ctx.strokeStyle = IS_LIGHT ? 'rgba(28, 25, 23, 0.25)' : 'rgba(255, 255, 255, 0.14)';
2565
2916
  ctx.lineWidth = 1.5;
2566
2917
  ctx.beginPath(); ctx.ellipse(0, 0, a, b, 0, 0, Math.PI * 2); ctx.stroke();
2567
2918
 
@@ -2790,7 +3141,7 @@ function drawMainScene(t) {
2790
3141
  } else if (COMP_THEME === 'radar_globe') {
2791
3142
  // 3D Tactical Wireframe Rotating Radar Globe
2792
3143
  const globeR = 150;
2793
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
3144
+ ctx.strokeStyle = IS_LIGHT ? 'rgba(28, 25, 23, 0.35)' : 'rgba(255, 255, 255, 0.15)';
2794
3145
  ctx.lineWidth = 1.5;
2795
3146
 
2796
3147
  // Outer degree ring
@@ -61,6 +61,7 @@ async function runRender({
61
61
  voicePath: asset.voicePath,
62
62
  caption: asset.caption || '',
63
63
  narration: asset.narration || '',
64
+ prompt: asset.prompt || '',
64
65
  visualDescription: asset.visualDescription || '',
65
66
  visualType: asset.visualType || 'code_animation',
66
67
  topic: asset.topic || topic || '',