@amg555/frameforge-mcp 2.3.3 → 2.3.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 +44 -0
- package/README.md +3 -2
- package/out/frames/f000000.jpg +0 -0
- package/out/frames/f000001.jpg +0 -0
- package/out/frames/f000002.jpg +0 -0
- package/out/frames/f000003.jpg +0 -0
- package/out/frames/f000004.jpg +0 -0
- package/out/frames/f000005.jpg +0 -0
- package/out/frames/f000006.jpg +0 -0
- package/out/frames/f000007.jpg +0 -0
- package/out/frames/f000008.jpg +0 -0
- package/out/frames/f000009.jpg +0 -0
- package/out/frames/f000010.jpg +0 -0
- package/out/frames/f000011.jpg +0 -0
- package/out/frames/f000012.jpg +0 -0
- package/out/frames/f000013.jpg +0 -0
- package/out/frames/f000014.jpg +0 -0
- package/out/frames/f000015.jpg +0 -0
- package/out/frames/f000016.jpg +0 -0
- package/out/frames/f000017.jpg +0 -0
- package/out/frames/f000018.jpg +0 -0
- package/out/frames/f000019.jpg +0 -0
- package/out/frames/f000020.jpg +0 -0
- package/out/frames/f000021.jpg +0 -0
- package/out/frames/f000022.jpg +0 -0
- package/out/frames/f000023.jpg +0 -0
- package/out/frames/f000024.jpg +0 -0
- package/out/frames/f000025.jpg +0 -0
- package/out/frames/f000026.jpg +0 -0
- package/out/frames/f000027.jpg +0 -0
- package/out/frames/f000028.jpg +0 -0
- package/out/frames/f000029.jpg +0 -0
- package/out/frames/f000030.jpg +0 -0
- package/out/frames/f000031.jpg +0 -0
- package/out/frames/f000032.jpg +0 -0
- package/out/frames/f000033.jpg +0 -0
- package/out/frames/f000034.jpg +0 -0
- package/out/frames/f000035.jpg +0 -0
- package/out/frames/f000036.jpg +0 -0
- package/out/frames/f000037.jpg +0 -0
- package/out/frames/f000038.jpg +0 -0
- package/out/frames/f000039.jpg +0 -0
- package/out/frames/f000040.jpg +0 -0
- package/out/frames/f000041.jpg +0 -0
- package/out/frames/f000042.jpg +0 -0
- package/out/frames/f000043.jpg +0 -0
- package/out/frames/f000044.jpg +0 -0
- package/out/frames/f000045.jpg +0 -0
- package/out/frames/f000046.jpg +0 -0
- package/out/frames/f000047.jpg +0 -0
- package/package.json +2 -2
- package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +1 -1
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.css +2 -2
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +1 -1
- package/plugins/video-pipeline/skills/video-pipeline/references/providers.md +11 -0
- package/scripts/deep-validate.js +2 -2
- package/scripts/html-renderer/new-project.mjs +2 -2
- package/src/cli/doctor.js +12 -5
- package/src/harness/bin/harness.mjs +20 -7
- package/src/mcp-server/server.js +1 -1
- package/src/video-renderer/renderer.js +222 -60
- package/src/video-renderer/scene-animator.js +459 -0
- package/src/workflows/assets.js +4 -0
- package/src/workflows/render.js +12 -7
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Scene Animator — Code-Driven HTML/Canvas Scene Generator
|
|
4
|
+
*
|
|
5
|
+
* Generates self-contained HTML/Canvas animation scenes conforming to the
|
|
6
|
+
* FrameForge frame-purity contract:
|
|
7
|
+
* - window.DURATION = seconds
|
|
8
|
+
* - window.seek = async (t) => draws frame at time t
|
|
9
|
+
* - Every frame is a pure function of t (no Date.now, no Math.random without seed)
|
|
10
|
+
* - Multilingual typography support: Malayalam, Indic, CJK, Arabic, Latin
|
|
11
|
+
*
|
|
12
|
+
* Supported component themes:
|
|
13
|
+
* - 'code': IDE/terminal window with typing code and cursor
|
|
14
|
+
* - 'chart': Animated bar chart, progress ring, and metric counter
|
|
15
|
+
* - 'nodes': Pulsing network nodes with traveling energy packets
|
|
16
|
+
* - 'diagram': Flowchart steps with directional energy arrows
|
|
17
|
+
* - 'image': Floating Ken-Burns card embedding user-provided image
|
|
18
|
+
* - 'kinetic': Audio spectrum visualizer + isometric geometry
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// Multilingual font family stack prioritizing Indic/Malayalam system fonts
|
|
25
|
+
const FONT_STACK = `'Nirmala UI', 'Noto Sans Malayalam', 'Malayalam Sangam MN', 'Kartika', 'Meera', 'Manjari', -apple-system, 'Segoe UI', system-ui, sans-serif`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect the best visual theme from visual description, caption, and media.
|
|
29
|
+
*/
|
|
30
|
+
function detectTheme({ visualDescription = '', caption = '', imagePath = null } = {}) {
|
|
31
|
+
if (imagePath) return 'image';
|
|
32
|
+
const text = `${visualDescription} ${caption}`.toLowerCase();
|
|
33
|
+
if (/\b(code|function|syntax|api|javascript|python|typescript|json|sql|query|endpoint|algorithm|struct|cli|terminal|bash|shell|npm|git)\b/.test(text)) {
|
|
34
|
+
return 'code';
|
|
35
|
+
}
|
|
36
|
+
if (/\b(chart|data|metric|analytics|growth|graph|statistic|stats|revenue|percent|count|number)\b/.test(text)) {
|
|
37
|
+
return 'chart';
|
|
38
|
+
}
|
|
39
|
+
if (/\b(network|node|ai|model|neural|pipeline|architecture|cloud|server|system|mesh|connect)\b/.test(text)) {
|
|
40
|
+
return 'nodes';
|
|
41
|
+
}
|
|
42
|
+
if (/\b(diagram|flow|step|process|workflow|sequence|lifecycle|cycle)\b/.test(text)) {
|
|
43
|
+
return 'diagram';
|
|
44
|
+
}
|
|
45
|
+
return 'kinetic';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate a complete self-contained HTML scene string.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} opts
|
|
52
|
+
* @param {string} opts.caption
|
|
53
|
+
* @param {string} [opts.narration]
|
|
54
|
+
* @param {string} [opts.visualDescription]
|
|
55
|
+
* @param {string} [opts.imagePath]
|
|
56
|
+
* @param {number} opts.duration
|
|
57
|
+
* @param {number} [opts.width]
|
|
58
|
+
* @param {number} [opts.height]
|
|
59
|
+
* @param {number} [opts.sceneIndex]
|
|
60
|
+
* @param {string} [opts.musicMood]
|
|
61
|
+
* @returns {string} HTML document
|
|
62
|
+
*/
|
|
63
|
+
function generateSceneHtml({
|
|
64
|
+
caption = '',
|
|
65
|
+
narration = '',
|
|
66
|
+
visualDescription = '',
|
|
67
|
+
imagePath = null,
|
|
68
|
+
duration = 3,
|
|
69
|
+
width = 1920,
|
|
70
|
+
height = 1080,
|
|
71
|
+
sceneIndex = 0,
|
|
72
|
+
musicMood = 'neutral',
|
|
73
|
+
} = {}) {
|
|
74
|
+
const theme = detectTheme({ visualDescription, caption, imagePath });
|
|
75
|
+
const clampedDur = Math.max(1, Number(duration) || 3);
|
|
76
|
+
|
|
77
|
+
// Convert imagePath to data URL if local file exists
|
|
78
|
+
let imageDataUri = '';
|
|
79
|
+
if (imagePath && fs.existsSync(imagePath)) {
|
|
80
|
+
try {
|
|
81
|
+
const ext = path.extname(imagePath).toLowerCase().slice(1) || 'jpeg';
|
|
82
|
+
const mime = ext === 'png' ? 'image/png' : ext === 'webp' ? 'image/webp' : 'image/jpeg';
|
|
83
|
+
const b64 = fs.readFileSync(imagePath).toString('base64');
|
|
84
|
+
imageDataUri = `data:${mime};base64,${b64}`;
|
|
85
|
+
} catch (_) {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Color palettes based on scene index and music mood
|
|
89
|
+
const PALETTES = [
|
|
90
|
+
{ bg0: '#142036', bg1: '#0b111c', bg2: '#05080f', accent: '#38bdf8', accent2: '#818cf8', tag: 'CYBER TECH' },
|
|
91
|
+
{ bg0: '#231436', bg1: '#110a1c', bg2: '#07040d', accent: '#c084fc', accent2: '#f43f5e', tag: 'DEEP DYNAMICS' },
|
|
92
|
+
{ bg0: '#0e261f', bg1: '#071511', bg2: '#030a08', accent: '#34d399', accent2: '#38bdf8', tag: 'SYSTEM ACTIVE' },
|
|
93
|
+
{ bg0: '#291d0e', bg1: '#150f07', bg2: '#0a0703', accent: '#fbbf24', accent2: '#f97316', tag: 'HIGH IMPACT' },
|
|
94
|
+
{ bg0: '#161d2b', bg1: '#0d131d', bg2: '#06090e', accent: '#60a5fa', accent2: '#a78bfa', tag: 'INTELLIGENCE' },
|
|
95
|
+
];
|
|
96
|
+
const pal = PALETTES[sceneIndex % PALETTES.length];
|
|
97
|
+
|
|
98
|
+
return `<!doctype html>
|
|
99
|
+
<html lang="en">
|
|
100
|
+
<head>
|
|
101
|
+
<meta charset="utf-8">
|
|
102
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
103
|
+
<style>
|
|
104
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
105
|
+
html, body {
|
|
106
|
+
width: 100%;
|
|
107
|
+
height: 100%;
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
background: ${pal.bg2};
|
|
110
|
+
font-family: ${FONT_STACK};
|
|
111
|
+
-webkit-font-smoothing: antialiased;
|
|
112
|
+
}
|
|
113
|
+
canvas {
|
|
114
|
+
width: 100%;
|
|
115
|
+
height: 100%;
|
|
116
|
+
display: block;
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
<canvas id="stage" width="${width}" height="${height}"></canvas>
|
|
122
|
+
<script>
|
|
123
|
+
window.DURATION = ${clampedDur};
|
|
124
|
+
const canvas = document.getElementById('stage');
|
|
125
|
+
const ctx = canvas.getContext('2d');
|
|
126
|
+
const W = ${width};
|
|
127
|
+
const H = ${height};
|
|
128
|
+
|
|
129
|
+
// Seeded PRNG for frame purity (same t -> exact same frame)
|
|
130
|
+
function sfc32(a, b, c, d) {
|
|
131
|
+
return function() {
|
|
132
|
+
a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
|
|
133
|
+
let t = (a + b | 0) + d | 0;
|
|
134
|
+
d = d + 1 | 0;
|
|
135
|
+
a = b ^ b >>> 9;
|
|
136
|
+
b = c + (c << 3) | 0;
|
|
137
|
+
c = c << 21 | c >>> 11;
|
|
138
|
+
c = c + t | 0;
|
|
139
|
+
return (t >>> 0) / 4294967296;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const rng = sfc32(${42 + sceneIndex * 17}, ${1337 + sceneIndex * 31}, ${7 + sceneIndex * 11}, 99);
|
|
144
|
+
const PARTICLES = Array.from({ length: 54 }, () => ({
|
|
145
|
+
x: rng() * W,
|
|
146
|
+
y: rng() * H,
|
|
147
|
+
vx: (rng() - 0.5) * 50,
|
|
148
|
+
vy: (rng() - 0.5) * 35 - 15,
|
|
149
|
+
r: 1.5 + rng() * 3.5,
|
|
150
|
+
alpha: 0.3 + rng() * 0.5
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
// Spring dynamics function
|
|
154
|
+
function spring(t, delay = 0, tension = 160, friction = 14) {
|
|
155
|
+
const dt = t - delay;
|
|
156
|
+
if (dt <= 0) return 0;
|
|
157
|
+
const omega = Math.sqrt(tension);
|
|
158
|
+
const zeta = friction / (2 * omega);
|
|
159
|
+
if (zeta < 1) {
|
|
160
|
+
const omegaD = omega * Math.sqrt(1 - zeta * zeta);
|
|
161
|
+
return 1 - Math.exp(-zeta * omega * dt) * (Math.cos(omegaD * dt) + (zeta * omega / omegaD) * Math.sin(omegaD * dt));
|
|
162
|
+
}
|
|
163
|
+
return 1 - Math.exp(-omega * dt);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const THEME = ${JSON.stringify(theme)};
|
|
167
|
+
const CAPTION = ${JSON.stringify(caption)};
|
|
168
|
+
const NARRATION = ${JSON.stringify(narration)};
|
|
169
|
+
const TAG = ${JSON.stringify(pal.tag)};
|
|
170
|
+
const ACCENT = ${JSON.stringify(pal.accent)};
|
|
171
|
+
const ACCENT2 = ${JSON.stringify(pal.accent2)};
|
|
172
|
+
const BG0 = ${JSON.stringify(pal.bg0)};
|
|
173
|
+
const BG1 = ${JSON.stringify(pal.bg1)};
|
|
174
|
+
const BG2 = ${JSON.stringify(pal.bg2)};
|
|
175
|
+
const IMAGE_SRC = ${JSON.stringify(imageDataUri)};
|
|
176
|
+
|
|
177
|
+
let cachedImage = null;
|
|
178
|
+
if (IMAGE_SRC) {
|
|
179
|
+
cachedImage = new Image();
|
|
180
|
+
cachedImage.src = IMAGE_SRC;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
window.seek = async (t) => {
|
|
184
|
+
// ── 1. Dynamic Moving Gradient Background ────────────────────────────────
|
|
185
|
+
const cx = W / 2 + Math.sin(t * 0.7) * 160;
|
|
186
|
+
const cy = H / 2 + Math.cos(t * 0.5) * 90;
|
|
187
|
+
const bgGrad = ctx.createRadialGradient(cx, cy, 60, W / 2, H / 2, W * 0.85);
|
|
188
|
+
bgGrad.addColorStop(0, BG0);
|
|
189
|
+
bgGrad.addColorStop(0.55, BG1);
|
|
190
|
+
bgGrad.addColorStop(1, BG2);
|
|
191
|
+
ctx.fillStyle = bgGrad;
|
|
192
|
+
ctx.fillRect(0, 0, W, H);
|
|
193
|
+
|
|
194
|
+
// ── 2. Subtle Moving Perspective Grid ───────────────────────────────────
|
|
195
|
+
ctx.save();
|
|
196
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.04)';
|
|
197
|
+
ctx.lineWidth = 1;
|
|
198
|
+
const gridShift = (t * 30) % 60;
|
|
199
|
+
for (let x = 0; x <= W; x += 60) {
|
|
200
|
+
ctx.beginPath();
|
|
201
|
+
ctx.moveTo(x, 0);
|
|
202
|
+
ctx.lineTo(x, H);
|
|
203
|
+
ctx.stroke();
|
|
204
|
+
}
|
|
205
|
+
for (let y = gridShift; y <= H; y += 60) {
|
|
206
|
+
ctx.beginPath();
|
|
207
|
+
ctx.moveTo(0, y);
|
|
208
|
+
ctx.lineTo(W, y);
|
|
209
|
+
ctx.stroke();
|
|
210
|
+
}
|
|
211
|
+
ctx.restore();
|
|
212
|
+
|
|
213
|
+
// ── 3. Floating Dust & Energy Particles ──────────────────────────────────
|
|
214
|
+
ctx.save();
|
|
215
|
+
for (let i = 0; i < PARTICLES.length; i++) {
|
|
216
|
+
const p = PARTICLES[i];
|
|
217
|
+
const px = (p.x + p.vx * t + W * 10) % W;
|
|
218
|
+
const py = (p.y + p.vy * t + H * 10) % H;
|
|
219
|
+
const pulse = 0.4 + 0.6 * Math.sin(t * 3.2 + i * 0.5);
|
|
220
|
+
ctx.fillStyle = ACCENT;
|
|
221
|
+
ctx.globalAlpha = p.alpha * pulse * 0.7;
|
|
222
|
+
ctx.beginPath();
|
|
223
|
+
ctx.arc(px, py, p.r, 0, Math.PI * 2);
|
|
224
|
+
ctx.fill();
|
|
225
|
+
}
|
|
226
|
+
ctx.restore();
|
|
227
|
+
|
|
228
|
+
// ── 4. Central Graphic Card matching Theme ──────────────────────────────
|
|
229
|
+
ctx.save();
|
|
230
|
+
const cardSpring = spring(t, 0.08, 140, 13);
|
|
231
|
+
const cardScale = Math.min(1.04, Math.max(0, cardSpring));
|
|
232
|
+
const cardY = H * 0.40 + (1 - cardSpring) * 50;
|
|
233
|
+
ctx.translate(W / 2, cardY);
|
|
234
|
+
ctx.scale(cardScale, cardScale);
|
|
235
|
+
|
|
236
|
+
const cardW = Math.round(W * 0.55);
|
|
237
|
+
const cardH = Math.round(H * 0.38);
|
|
238
|
+
|
|
239
|
+
// Card backdrop with glassmorphic border
|
|
240
|
+
ctx.fillStyle = 'rgba(11, 17, 28, 0.82)';
|
|
241
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
|
|
242
|
+
ctx.lineWidth = 2;
|
|
243
|
+
ctx.beginPath();
|
|
244
|
+
ctx.roundRect(-cardW / 2, -cardH / 2, cardW, cardH, 20);
|
|
245
|
+
ctx.fill();
|
|
246
|
+
ctx.stroke();
|
|
247
|
+
|
|
248
|
+
// Accent edge highlight
|
|
249
|
+
ctx.strokeStyle = ACCENT;
|
|
250
|
+
ctx.lineWidth = 2;
|
|
251
|
+
ctx.beginPath();
|
|
252
|
+
ctx.moveTo(-cardW / 2 + 30, -cardH / 2);
|
|
253
|
+
ctx.lineTo(cardW / 2 - 30, -cardH / 2);
|
|
254
|
+
ctx.stroke();
|
|
255
|
+
|
|
256
|
+
if (THEME === 'code') {
|
|
257
|
+
// IDE Titlebar
|
|
258
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.05)';
|
|
259
|
+
ctx.beginPath();
|
|
260
|
+
ctx.roundRect(-cardW / 2, -cardH / 2, cardW, 40, [20, 20, 0, 0]);
|
|
261
|
+
ctx.fill();
|
|
262
|
+
// Window dots
|
|
263
|
+
ctx.fillStyle = '#ff5f56'; ctx.beginPath(); ctx.arc(-cardW / 2 + 25, -cardH / 2 + 20, 6, 0, Math.PI * 2); ctx.fill();
|
|
264
|
+
ctx.fillStyle = '#ffbd2e'; ctx.beginPath(); ctx.arc(-cardW / 2 + 45, -cardH / 2 + 20, 6, 0, Math.PI * 2); ctx.fill();
|
|
265
|
+
ctx.fillStyle = '#27c93f'; ctx.beginPath(); ctx.arc(-cardW / 2 + 65, -cardH / 2 + 20, 6, 0, Math.PI * 2); ctx.fill();
|
|
266
|
+
// Tab title
|
|
267
|
+
ctx.font = '14px monospace';
|
|
268
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
|
|
269
|
+
ctx.textAlign = 'left';
|
|
270
|
+
ctx.fillText('scene-pipeline.js', -cardW / 2 + 90, -cardH / 2 + 25);
|
|
271
|
+
|
|
272
|
+
// Code lines with typing effect
|
|
273
|
+
const codeLines = [
|
|
274
|
+
'import { FrameForge } from "@amg555/frameforge";',
|
|
275
|
+
'const engine = new FrameForge.AnimationEngine({ t });',
|
|
276
|
+
'await engine.renderScene({',
|
|
277
|
+
' status: "ACTIVE_24FPS",',
|
|
278
|
+
' fidelity: "BROADCAST_BT709"',
|
|
279
|
+
'});'
|
|
280
|
+
];
|
|
281
|
+
ctx.font = '18px "Fira Code", monospace';
|
|
282
|
+
const typeProg = Math.min(1, t / (window.DURATION * 0.75));
|
|
283
|
+
const totalChars = codeLines.join('\\n').length;
|
|
284
|
+
let charsToShow = Math.floor(totalChars * typeProg);
|
|
285
|
+
|
|
286
|
+
let lineY = -cardH / 2 + 80;
|
|
287
|
+
for (let l = 0; l < codeLines.length; l++) {
|
|
288
|
+
const line = codeLines[l];
|
|
289
|
+
const sliceLen = Math.min(line.length, charsToShow);
|
|
290
|
+
const textToDraw = line.slice(0, sliceLen);
|
|
291
|
+
charsToShow = Math.max(0, charsToShow - line.length - 1);
|
|
292
|
+
|
|
293
|
+
// Line number
|
|
294
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
|
|
295
|
+
ctx.fillText(String(l + 1).padStart(2, '0'), -cardW / 2 + 30, lineY);
|
|
296
|
+
// Syntax text
|
|
297
|
+
ctx.fillStyle = l === 0 ? ACCENT : l === 1 ? ACCENT2 : '#e2e8f0';
|
|
298
|
+
ctx.fillText(textToDraw, -cardW / 2 + 75, lineY);
|
|
299
|
+
lineY += 32;
|
|
300
|
+
}
|
|
301
|
+
} else if (THEME === 'chart') {
|
|
302
|
+
// Animated bar chart + metrics
|
|
303
|
+
const bars = 8;
|
|
304
|
+
const barW = (cardW - 140) / bars;
|
|
305
|
+
const startX = -cardW / 2 + 70;
|
|
306
|
+
const baseLine = cardH / 2 - 50;
|
|
307
|
+
|
|
308
|
+
for (let b = 0; b < bars; b++) {
|
|
309
|
+
const barSpring = spring(t, 0.15 + b * 0.05, 120, 12);
|
|
310
|
+
const targetH = 50 + ((b * 37) % 140);
|
|
311
|
+
const curH = targetH * Math.min(1.05, Math.max(0, barSpring));
|
|
312
|
+
const bx = startX + b * barW;
|
|
313
|
+
const by = baseLine - curH;
|
|
314
|
+
|
|
315
|
+
const barGrad = ctx.createLinearGradient(0, by, 0, baseLine);
|
|
316
|
+
barGrad.addColorStop(0, ACCENT);
|
|
317
|
+
barGrad.addColorStop(1, ACCENT2);
|
|
318
|
+
ctx.fillStyle = barGrad;
|
|
319
|
+
ctx.beginPath();
|
|
320
|
+
ctx.roundRect(bx, by, barW - 12, curH, 6);
|
|
321
|
+
ctx.fill();
|
|
322
|
+
}
|
|
323
|
+
// Animated Counter
|
|
324
|
+
const counterVal = Math.round(spring(t, 0.2, 100, 10) * 98.4);
|
|
325
|
+
ctx.font = 'bold 36px monospace';
|
|
326
|
+
ctx.fillStyle = '#ffffff';
|
|
327
|
+
ctx.textAlign = 'right';
|
|
328
|
+
ctx.fillText(counterVal + '% METRIC GAIN', cardW / 2 - 40, -cardH / 2 + 50);
|
|
329
|
+
} else if (THEME === 'nodes') {
|
|
330
|
+
// Interconnected pulsing network nodes
|
|
331
|
+
const nodeCoords = [
|
|
332
|
+
[-cardW * 0.35, -30], [-cardW * 0.15, 40], [0, -50],
|
|
333
|
+
[cardW * 0.15, 30], [cardW * 0.35, -20]
|
|
334
|
+
];
|
|
335
|
+
// Connections
|
|
336
|
+
ctx.strokeStyle = 'rgba(56, 189, 248, 0.35)';
|
|
337
|
+
ctx.lineWidth = 2;
|
|
338
|
+
for (let i = 0; i < nodeCoords.length - 1; i++) {
|
|
339
|
+
ctx.beginPath();
|
|
340
|
+
ctx.moveTo(nodeCoords[i][0], nodeCoords[i][1]);
|
|
341
|
+
ctx.lineTo(nodeCoords[i + 1][0], nodeCoords[i + 1][1]);
|
|
342
|
+
ctx.stroke();
|
|
343
|
+
|
|
344
|
+
// Energy packet along connection
|
|
345
|
+
const packetPos = (t * 0.8 + i * 0.25) % 1;
|
|
346
|
+
const ex = nodeCoords[i][0] + (nodeCoords[i + 1][0] - nodeCoords[i][0]) * packetPos;
|
|
347
|
+
const ey = nodeCoords[i][1] + (nodeCoords[i + 1][1] - nodeCoords[i][1]) * packetPos;
|
|
348
|
+
ctx.fillStyle = ACCENT;
|
|
349
|
+
ctx.beginPath();
|
|
350
|
+
ctx.arc(ex, ey, 5, 0, Math.PI * 2);
|
|
351
|
+
ctx.fill();
|
|
352
|
+
}
|
|
353
|
+
// Nodes
|
|
354
|
+
for (let i = 0; i < nodeCoords.length; i++) {
|
|
355
|
+
const [nx, ny] = nodeCoords[i];
|
|
356
|
+
const pulse = 1 + 0.25 * Math.sin(t * 4 + i);
|
|
357
|
+
ctx.fillStyle = ACCENT2;
|
|
358
|
+
ctx.beginPath();
|
|
359
|
+
ctx.arc(nx, ny, 14 * pulse, 0, Math.PI * 2);
|
|
360
|
+
ctx.fill();
|
|
361
|
+
ctx.fillStyle = '#ffffff';
|
|
362
|
+
ctx.beginPath();
|
|
363
|
+
ctx.arc(nx, ny, 6, 0, Math.PI * 2);
|
|
364
|
+
ctx.fill();
|
|
365
|
+
}
|
|
366
|
+
} else if (THEME === 'image' && cachedImage && cachedImage.complete && cachedImage.naturalWidth > 0) {
|
|
367
|
+
// Ken-Burns floating image
|
|
368
|
+
const zoom = 1.0 + (t / window.DURATION) * 0.07;
|
|
369
|
+
ctx.save();
|
|
370
|
+
ctx.beginPath();
|
|
371
|
+
ctx.roundRect(-cardW / 2 + 12, -cardH / 2 + 12, cardW - 24, cardH - 24, 14);
|
|
372
|
+
ctx.clip();
|
|
373
|
+
ctx.scale(zoom, zoom);
|
|
374
|
+
ctx.drawImage(cachedImage, -cardW / 2 + 12, -cardH / 2 + 12, cardW - 24, cardH - 24);
|
|
375
|
+
ctx.restore();
|
|
376
|
+
} else {
|
|
377
|
+
// Kinetic Audio Spectrum Bars
|
|
378
|
+
const bars = 28;
|
|
379
|
+
const barW = (cardW - 80) / bars;
|
|
380
|
+
const startX = -cardW / 2 + 40;
|
|
381
|
+
for (let b = 0; b < bars; b++) {
|
|
382
|
+
const phase = t * 4.5 + b * 0.3;
|
|
383
|
+
const hVal = 25 + 95 * Math.abs(Math.sin(phase) * Math.cos(phase * 0.8));
|
|
384
|
+
const bx = startX + b * barW;
|
|
385
|
+
const by = 20 - hVal / 2;
|
|
386
|
+
|
|
387
|
+
const barGrad = ctx.createLinearGradient(0, by, 0, by + hVal);
|
|
388
|
+
barGrad.addColorStop(0, ACCENT);
|
|
389
|
+
barGrad.addColorStop(1, ACCENT2);
|
|
390
|
+
ctx.fillStyle = barGrad;
|
|
391
|
+
ctx.beginPath();
|
|
392
|
+
ctx.roundRect(bx, by, barW - 6, hVal, 4);
|
|
393
|
+
ctx.fill();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Tag Badge
|
|
398
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
|
|
399
|
+
ctx.beginPath();
|
|
400
|
+
ctx.roundRect(-cardW / 2 + 25, -cardH / 2 + 20, 140, 30, 15);
|
|
401
|
+
ctx.fill();
|
|
402
|
+
ctx.font = 'bold 13px system-ui, sans-serif';
|
|
403
|
+
ctx.fillStyle = ACCENT;
|
|
404
|
+
ctx.textAlign = 'center';
|
|
405
|
+
ctx.fillText(TAG, -cardW / 2 + 95, -cardH / 2 + 40);
|
|
406
|
+
|
|
407
|
+
ctx.restore();
|
|
408
|
+
|
|
409
|
+
// ── 5. Kinetic Typography with Multilingual / Malayalam Support ─────────
|
|
410
|
+
ctx.save();
|
|
411
|
+
const textSpring = spring(t, 0.22, 150, 13);
|
|
412
|
+
const textY = H * 0.77 + (1 - textSpring) * 45;
|
|
413
|
+
const textAlpha = Math.min(1, Math.max(0, textSpring));
|
|
414
|
+
|
|
415
|
+
ctx.globalAlpha = textAlpha;
|
|
416
|
+
ctx.textAlign = 'center';
|
|
417
|
+
|
|
418
|
+
// Primary Caption / Headline
|
|
419
|
+
ctx.font = 'bold 54px ' + ${JSON.stringify(FONT_STACK)};
|
|
420
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
|
|
421
|
+
ctx.shadowBlur = 20;
|
|
422
|
+
ctx.shadowOffsetY = 6;
|
|
423
|
+
ctx.fillStyle = '#ffffff';
|
|
424
|
+
ctx.fillText(CAPTION, W / 2, textY);
|
|
425
|
+
|
|
426
|
+
// Subtitle / Narration preview
|
|
427
|
+
if (NARRATION) {
|
|
428
|
+
ctx.font = '400 28px ' + ${JSON.stringify(FONT_STACK)};
|
|
429
|
+
ctx.fillStyle = 'rgba(226, 232, 240, 0.88)';
|
|
430
|
+
ctx.shadowBlur = 12;
|
|
431
|
+
const cleanNarr = NARRATION.length > 95 ? NARRATION.slice(0, 95) + '...' : NARRATION;
|
|
432
|
+
ctx.fillText(cleanNarr, W / 2, textY + 62);
|
|
433
|
+
}
|
|
434
|
+
ctx.restore();
|
|
435
|
+
|
|
436
|
+
// ── 6. Progress Scrubber ────────────────────────────────────────────────
|
|
437
|
+
ctx.save();
|
|
438
|
+
const progress = Math.min(1, Math.max(0, t / window.DURATION));
|
|
439
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
|
|
440
|
+
ctx.fillRect(0, H - 8, W, 8);
|
|
441
|
+
|
|
442
|
+
const progGrad = ctx.createLinearGradient(0, 0, W, 0);
|
|
443
|
+
progGrad.addColorStop(0, ACCENT);
|
|
444
|
+
progGrad.addColorStop(1, ACCENT2);
|
|
445
|
+
ctx.fillStyle = progGrad;
|
|
446
|
+
ctx.fillRect(0, H - 8, W * progress, 8);
|
|
447
|
+
ctx.restore();
|
|
448
|
+
};
|
|
449
|
+
window.ready = true;
|
|
450
|
+
</script>
|
|
451
|
+
</body>
|
|
452
|
+
</html>`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
module.exports = {
|
|
456
|
+
generateSceneHtml,
|
|
457
|
+
detectTheme,
|
|
458
|
+
FONT_STACK,
|
|
459
|
+
};
|
package/src/workflows/assets.js
CHANGED
|
@@ -88,6 +88,10 @@ async function runAssets({ productionId, scenes, parallelism = 1, skipTts = fals
|
|
|
88
88
|
caption: scene.caption,
|
|
89
89
|
camera: scene.camera_hint || scene.camera || 'static',
|
|
90
90
|
focalPoint: scene.focal_point || null,
|
|
91
|
+
narration: scene.narration || '',
|
|
92
|
+
visualDescription: scene.visual_description || scene.narration || '',
|
|
93
|
+
musicMood: scene.music_mood || 'neutral',
|
|
94
|
+
language: scene.language || '',
|
|
91
95
|
};
|
|
92
96
|
}));
|
|
93
97
|
results.push(...batchResults);
|
package/src/workflows/render.js
CHANGED
|
@@ -42,13 +42,18 @@ async function runRender({ productionId, sceneAssets, withMusic = true, musicVol
|
|
|
42
42
|
const idx = asset.sceneId - 1;
|
|
43
43
|
try {
|
|
44
44
|
const scenePath = renderer.renderScene({
|
|
45
|
-
sceneIndex:
|
|
46
|
-
imagePath:
|
|
47
|
-
videoPath:
|
|
48
|
-
voicePath:
|
|
49
|
-
caption:
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
sceneIndex: idx,
|
|
46
|
+
imagePath: asset.imagePath,
|
|
47
|
+
videoPath: asset.videoPath,
|
|
48
|
+
voicePath: asset.voicePath,
|
|
49
|
+
caption: asset.caption || '',
|
|
50
|
+
narration: asset.narration || '',
|
|
51
|
+
visualDescription: asset.visualDescription || '',
|
|
52
|
+
visualType: asset.visualType || 'code_animation',
|
|
53
|
+
musicMood: asset.musicMood || 'neutral',
|
|
54
|
+
language: asset.language || '',
|
|
55
|
+
camera: asset.camera || 'static',
|
|
56
|
+
focalPoint: asset.focalPoint || null,
|
|
52
57
|
renderWidth,
|
|
53
58
|
renderHeight,
|
|
54
59
|
});
|