loopwind 0.18.1 → 0.20.1
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/README.md +83 -0
- package/dist/commands/preview.d.ts.map +1 -1
- package/dist/commands/preview.js +2 -1
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/render.d.ts.map +1 -1
- package/dist/commands/render.js +2 -0
- package/dist/commands/render.js.map +1 -1
- package/dist/default-templates/AGENTS.md +54 -0
- package/dist/lib/renderer.d.ts.map +1 -1
- package/dist/lib/renderer.js +3 -0
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/tailwind-browser.d.ts.map +1 -1
- package/dist/lib/tailwind-browser.js +169 -6
- package/dist/lib/tailwind-browser.js.map +1 -1
- package/dist/lib/tailwind.d.ts.map +1 -1
- package/dist/lib/tailwind.js +178 -7
- package/dist/lib/tailwind.js.map +1 -1
- package/dist/lib/video-preview.d.ts +1 -1
- package/dist/lib/video-preview.d.ts.map +1 -1
- package/dist/lib/video-preview.js +266 -249
- package/dist/lib/video-preview.js.map +1 -1
- package/dist/lib/video-renderer.d.ts +2 -0
- package/dist/lib/video-renderer.d.ts.map +1 -1
- package/dist/lib/video-renderer.js +4 -4
- package/dist/lib/video-renderer.js.map +1 -1
- package/dist/sdk/compiler.d.ts +94 -0
- package/dist/sdk/compiler.d.ts.map +1 -0
- package/dist/sdk/compiler.js +122 -0
- package/dist/sdk/compiler.js.map +1 -0
- package/dist/sdk/index.d.ts +3 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +2 -1
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/preview.d.ts +65 -0
- package/dist/sdk/preview.d.ts.map +1 -0
- package/dist/sdk/preview.js +262 -0
- package/dist/sdk/preview.js.map +1 -0
- package/dist/sdk/template.d.ts +47 -24
- package/dist/sdk/template.d.ts.map +1 -1
- package/dist/sdk/template.js +53 -93
- package/dist/sdk/template.js.map +1 -1
- package/examples/nextjs-template-import.ts +2 -2
- package/examples/sdk-video-preview.tsx +120 -0
- package/examples/template-compiler-workflow.ts +251 -0
- package/package.json +6 -2
- package/render-examples-600x400.mjs +161 -0
- package/render-spring-variants-fixed.mjs +60 -0
- package/render-staggered-text.mjs +56 -0
- package/test-jsx-support.mjs +32 -6
- package/test-sdk-config.mjs +138 -81
- package/test-sdk-source-config.mjs +427 -0
- package/test-static-debug.tsx +19 -0
- package/test-templates/config-test.mjs +17 -0
- package/test-templates/test-sdk.mjs +46 -22
- package/test-video-props.json +3 -0
- package/website/DEPLOYMENT.md +1 -0
- package/website/OG_IMAGES.md +1 -0
- package/website/astro.config.mjs +18 -2
- package/website/dist/.gitkeep +1 -0
- package/website/dist/_worker.js/index.js +1 -1
- package/website/dist/_worker.js/{manifest_BAAoOzaU.mjs → manifest_CT_D-YDe.mjs} +1 -1
- package/website/dist/llm.txt +1 -1
- package/website/dist/sdk/index.html +405 -102
- package/website/dist/sitemap.xml +12 -12
- package/website/package-lock.json +2866 -7080
- package/website/package.json +1 -2
- package/website/public/.gitkeep +1 -0
- package/website/templates/og-image.tsx +20 -21
- package/website/test-playground.mjs +45 -0
|
@@ -1,168 +1,92 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import { createServer as createViteServer } from 'vite';
|
|
3
2
|
import path from 'path';
|
|
4
|
-
import fs from 'fs/promises';
|
|
5
3
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
4
|
+
import { renderToSVG, clearTemplateCache } from './renderer.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
9
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
8
|
const __dirname = path.dirname(__filename);
|
|
11
9
|
export async function startVideoPreview(options) {
|
|
12
|
-
const { templateName, templatePath, props, meta, port, open } = options;
|
|
10
|
+
const { templateName, templatePath, props, meta, port, open, debug } = options;
|
|
13
11
|
const app = express();
|
|
14
|
-
|
|
15
|
-
let loopwindConfig = null;
|
|
12
|
+
const loopwindConfig = await loadConfig();
|
|
16
13
|
const templateDir = path.dirname(templatePath);
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const loopwindContent = await fs.readFile(cwdLoopwindPath, 'utf-8');
|
|
36
|
-
loopwindConfig = JSON.parse(loopwindContent);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
// loopwind.json doesn't exist or couldn't be read - that's fine
|
|
14
|
+
// Pre-render all frames as SVG
|
|
15
|
+
console.log(chalk.dim('Pre-rendering video frames...'));
|
|
16
|
+
const { fps, duration } = meta.video;
|
|
17
|
+
const totalFrames = Math.floor(fps * duration);
|
|
18
|
+
const frames = [];
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
for (let frame = 0; frame < totalFrames; frame++) {
|
|
21
|
+
const progress = frame / totalFrames;
|
|
22
|
+
const svg = await renderToSVG(templateName, {
|
|
23
|
+
...props,
|
|
24
|
+
frame,
|
|
25
|
+
progress,
|
|
26
|
+
}, { config: loopwindConfig, debug });
|
|
27
|
+
frames.push(svg);
|
|
28
|
+
// Show progress every 30 frames
|
|
29
|
+
if (frame % 30 === 0 || frame === totalFrames - 1) {
|
|
30
|
+
const percent = Math.round((frame / totalFrames) * 100);
|
|
31
|
+
process.stdout.write(`\r${chalk.dim(` Rendering frames... ${percent}% (${frame + 1}/${totalFrames})`)}`);
|
|
40
32
|
}
|
|
41
33
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
colors: tailwindConfig.theme.colors,
|
|
51
|
-
spacing: tailwindConfig.theme.spacing,
|
|
52
|
-
fontSize: tailwindConfig.theme.fontSize,
|
|
53
|
-
borderRadius: tailwindConfig.theme.borderRadius,
|
|
54
|
-
extend: tailwindConfig.theme.extend ? {
|
|
55
|
-
colors: tailwindConfig.theme.extend.colors,
|
|
56
|
-
spacing: tailwindConfig.theme.extend.spacing,
|
|
57
|
-
fontSize: tailwindConfig.theme.extend.fontSize,
|
|
58
|
-
borderRadius: tailwindConfig.theme.extend.borderRadius,
|
|
59
|
-
} : undefined
|
|
60
|
-
}
|
|
61
|
-
};
|
|
34
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
35
|
+
console.log(`\n${chalk.green('✓')} ${chalk.dim(`Rendered ${totalFrames} frames in ${elapsed}s`)}\n`);
|
|
36
|
+
// Serve individual frame as SVG
|
|
37
|
+
app.get('/api/frame/:frameNumber', (req, res) => {
|
|
38
|
+
const frameNum = parseInt(req.params.frameNumber);
|
|
39
|
+
if (frameNum < 0 || frameNum >= frames.length) {
|
|
40
|
+
res.status(404).send('Frame not found');
|
|
41
|
+
return;
|
|
62
42
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
'.woff2': 'woff2',
|
|
90
|
-
'.woff': 'woff',
|
|
91
|
-
'.ttf': 'truetype',
|
|
92
|
-
'.otf': 'opentype',
|
|
93
|
-
};
|
|
94
|
-
const format = formatMap[fontExt] || 'truetype';
|
|
95
|
-
// Store path for serving
|
|
96
|
-
fontFilePaths[fontFileName] = fontPath;
|
|
97
|
-
fontFaceCSS += `
|
|
98
|
-
@font-face {
|
|
99
|
-
font-family: '${fontName}';
|
|
100
|
-
src: url('/fonts/${fontFileName}') format('${format}');
|
|
101
|
-
font-weight: ${fileDef.weight || 400};
|
|
102
|
-
font-style: ${fileDef.style || 'normal'};
|
|
103
|
-
}`;
|
|
43
|
+
res.setHeader('Content-Type', 'image/svg+xml');
|
|
44
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
45
|
+
res.send(frames[frameNum]);
|
|
46
|
+
});
|
|
47
|
+
// Serve template directory for local assets
|
|
48
|
+
app.use('/template', express.static(templateDir));
|
|
49
|
+
// Regenerate frames endpoint (for HMR-like behavior)
|
|
50
|
+
app.get('/api/regenerate', async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
console.log(chalk.blue('\n📝 Regenerating frames...'));
|
|
53
|
+
// Clear caches
|
|
54
|
+
clearTemplateCache();
|
|
55
|
+
frames.length = 0;
|
|
56
|
+
// Re-render all frames
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
for (let frame = 0; frame < totalFrames; frame++) {
|
|
59
|
+
const progress = frame / totalFrames;
|
|
60
|
+
const svg = await renderToSVG(templateName, {
|
|
61
|
+
...props,
|
|
62
|
+
frame,
|
|
63
|
+
progress,
|
|
64
|
+
}, { config: loopwindConfig, debug });
|
|
65
|
+
frames.push(svg);
|
|
66
|
+
if (frame % 30 === 0 || frame === totalFrames - 1) {
|
|
67
|
+
const percent = Math.round((frame / totalFrames) * 100);
|
|
68
|
+
process.stdout.write(`\r${chalk.dim(` Rendering frames... ${percent}% (${frame + 1}/${totalFrames})`)}`);
|
|
104
69
|
}
|
|
105
70
|
}
|
|
71
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
72
|
+
console.log(`\n${chalk.green('✓')} ${chalk.dim(`Regenerated ${totalFrames} frames in ${elapsed}s`)}\n`);
|
|
73
|
+
res.json({ success: true, totalFrames });
|
|
106
74
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// Generate the entry file
|
|
116
|
-
const entryContent = `
|
|
117
|
-
import React from 'react';
|
|
118
|
-
import { createRoot } from 'react-dom/client';
|
|
119
|
-
import { VideoPlayer } from '${distLibDir}/video-player.js';
|
|
120
|
-
import { tw } from '${distLibDir}/tailwind-browser.js';
|
|
121
|
-
import Template, { meta } from '${templatePath}';
|
|
122
|
-
|
|
123
|
-
const props = ${JSON.stringify(props)};
|
|
124
|
-
|
|
125
|
-
// Tailwind config theme (loaded server-side and serialized)
|
|
126
|
-
const tailwindConfig = ${JSON.stringify(tailwindTheme)};
|
|
127
|
-
|
|
128
|
-
// loopwind.json config (loaded server-side and embedded)
|
|
129
|
-
const loopwindConfig = ${JSON.stringify(loopwindConfig)};
|
|
130
|
-
|
|
131
|
-
function App() {
|
|
132
|
-
return (
|
|
133
|
-
<VideoPlayer
|
|
134
|
-
Template={Template}
|
|
135
|
-
meta={meta}
|
|
136
|
-
props={props}
|
|
137
|
-
tailwindConfig={tailwindConfig}
|
|
138
|
-
loopwindConfig={loopwindConfig}
|
|
139
|
-
/>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const container = document.getElementById('root');
|
|
144
|
-
const root = createRoot(container);
|
|
145
|
-
root.render(<App />);
|
|
146
|
-
|
|
147
|
-
// Hot module replacement
|
|
148
|
-
if (import.meta.hot) {
|
|
149
|
-
import.meta.hot.accept();
|
|
150
|
-
}
|
|
151
|
-
`;
|
|
152
|
-
const entryPath = path.join(tempDir, 'entry.tsx');
|
|
153
|
-
await fs.writeFile(entryPath, entryContent);
|
|
154
|
-
// Generate index.html
|
|
155
|
-
const htmlContent = `
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error(chalk.red('✖ Regeneration error:'), error.message);
|
|
77
|
+
res.status(500).json({ error: error.message });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// Serve index.html for root
|
|
81
|
+
app.get('/', (req, res) => {
|
|
82
|
+
const html = `
|
|
156
83
|
<!DOCTYPE html>
|
|
157
84
|
<html lang="en">
|
|
158
85
|
<head>
|
|
159
86
|
<meta charset="UTF-8">
|
|
160
87
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
161
88
|
<title>Video Preview: ${templateName}</title>
|
|
162
|
-
<style
|
|
163
|
-
html {
|
|
164
|
-
font-size: 16px; /* Match Satori's default rem base */
|
|
165
|
-
}
|
|
89
|
+
<style>
|
|
166
90
|
* {
|
|
167
91
|
margin: 0;
|
|
168
92
|
padding: 0;
|
|
@@ -170,7 +94,7 @@ if (import.meta.hot) {
|
|
|
170
94
|
}
|
|
171
95
|
body {
|
|
172
96
|
overflow: hidden;
|
|
173
|
-
font-family:
|
|
97
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
174
98
|
}
|
|
175
99
|
/* Custom range slider styling */
|
|
176
100
|
input[type="range"] {
|
|
@@ -208,105 +132,206 @@ if (import.meta.hot) {
|
|
|
208
132
|
</head>
|
|
209
133
|
<body>
|
|
210
134
|
<div id="root"></div>
|
|
211
|
-
<script type="module"
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
135
|
+
<script type="module">
|
|
136
|
+
const meta = ${JSON.stringify(meta)};
|
|
137
|
+
const totalFrames = ${totalFrames};
|
|
138
|
+
|
|
139
|
+
// Mount the simplified video player
|
|
140
|
+
const root = document.getElementById('root');
|
|
141
|
+
|
|
142
|
+
let frame = 0;
|
|
143
|
+
let isPlaying = true;
|
|
144
|
+
let isLooping = true;
|
|
145
|
+
let animationId = null;
|
|
146
|
+
let lastTime = performance.now();
|
|
147
|
+
|
|
148
|
+
const { fps, duration } = meta.video;
|
|
149
|
+
const durationMs = duration * 1000;
|
|
150
|
+
|
|
151
|
+
// Create UI
|
|
152
|
+
root.innerHTML = \`
|
|
153
|
+
<div style="display: flex; flex-direction: column; height: 100vh; background: #0a0a0a; color: #ffffff;">
|
|
154
|
+
<!-- Header -->
|
|
155
|
+
<div style="background: #18181b; border-bottom: 1px solid #27272a; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center;">
|
|
156
|
+
<h1 style="font-size: 1.25rem; font-weight: 600; margin: 0;">loopwind preview: ${templateName}</h1>
|
|
157
|
+
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
|
158
|
+
<button id="regenerate-btn" style="background: #3b82f6; border: none; color: white; cursor: pointer; padding: 0.5rem 1rem; border-radius: 0.375rem; font-size: 0.875rem; font-weight: 500;">
|
|
159
|
+
Regenerate
|
|
160
|
+
</button>
|
|
161
|
+
<span style="background: #7c3aed; color: white; padding: 0.25rem 0.75rem; border-radius: 0.375rem; font-size: 0.875rem; font-weight: 500;">VIDEO</span>
|
|
162
|
+
<span style="background: #27272a; color: #a1a1aa; padding: 0.25rem 0.75rem; border-radius: 0.375rem; font-size: 0.75rem; font-family: monospace;">
|
|
163
|
+
\${meta.size.width}×\${meta.size.height} @ \${fps}fps
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<!-- Canvas -->
|
|
169
|
+
<div id="canvas-container" style="flex: 1; display: flex; align-items: center; justify-content: center; padding: 2rem; overflow: hidden;">
|
|
170
|
+
<div id="video-wrapper" style="box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5); border-radius: 0.5rem; overflow: hidden; border: 1px solid #3f3f46; max-width: 100%; max-height: 100%;">
|
|
171
|
+
<img id="frame-img" style="display: block; width: 100%; height: 100%; object-fit: contain;" width="\${meta.size.width}" height="\${meta.size.height}" />
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<!-- Controls -->
|
|
176
|
+
<div style="background: #18181b; border-top: 1px solid #27272a; padding: 1rem 2rem;">
|
|
177
|
+
<div style="margin-bottom: 0.75rem;">
|
|
178
|
+
<input type="range" id="scrubber" min="0" max="100" step="0.1" value="0" style="width: 100%; height: 6px; border-radius: 3px; cursor: pointer; background: linear-gradient(to right, #7c3aed 0%, #3f3f46 0%);" />
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
182
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
183
|
+
<button id="restart-btn" style="background: transparent; border: none; color: #a1a1aa; cursor: pointer; padding: 0.5rem; font-size: 1rem;">⏮</button>
|
|
184
|
+
<button id="step-back-btn" style="background: transparent; border: none; color: #a1a1aa; cursor: pointer; padding: 0.5rem; font-size: 1rem;">⏪</button>
|
|
185
|
+
<button id="play-pause-btn" style="background: #7c3aed; border: none; color: white; cursor: pointer; padding: 0.75rem 1rem; border-radius: 0.375rem; font-size: 1rem; min-width: 3rem;">⏸</button>
|
|
186
|
+
<button id="step-forward-btn" style="background: transparent; border: none; color: #a1a1aa; cursor: pointer; padding: 0.5rem; font-size: 1rem;">⏩</button>
|
|
187
|
+
<button id="loop-btn" style="background: #3f3f46; border: 1px solid #3f3f46; color: #ffffff; cursor: pointer; padding: 0.5rem 0.75rem; border-radius: 0.375rem; font-size: 0.75rem; margin-left: 0.5rem;">🔁</button>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div style="font-family: monospace; font-size: 0.875rem; color: #a1a1aa;">
|
|
191
|
+
<span id="current-time" style="color: #ffffff;">0ms</span>
|
|
192
|
+
/
|
|
193
|
+
<span>\${Math.floor(durationMs)}ms</span>
|
|
194
|
+
<span style="margin-left: 1rem; color: #71717a;">Frame <span id="current-frame">0</span> / \${totalFrames}</span>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
\`;
|
|
200
|
+
|
|
201
|
+
// Get elements
|
|
202
|
+
const frameImg = document.getElementById('frame-img');
|
|
203
|
+
const scrubber = document.getElementById('scrubber');
|
|
204
|
+
const playPauseBtn = document.getElementById('play-pause-btn');
|
|
205
|
+
const restartBtn = document.getElementById('restart-btn');
|
|
206
|
+
const stepBackBtn = document.getElementById('step-back-btn');
|
|
207
|
+
const stepForwardBtn = document.getElementById('step-forward-btn');
|
|
208
|
+
const loopBtn = document.getElementById('loop-btn');
|
|
209
|
+
const regenerateBtn = document.getElementById('regenerate-btn');
|
|
210
|
+
const currentTimeEl = document.getElementById('current-time');
|
|
211
|
+
const currentFrameEl = document.getElementById('current-frame');
|
|
212
|
+
|
|
213
|
+
// Update frame
|
|
214
|
+
function updateFrame() {
|
|
215
|
+
const frameNum = Math.floor(frame);
|
|
216
|
+
frameImg.src = \`/api/frame/\${frameNum}?t=\${Date.now()}\`;
|
|
217
|
+
|
|
218
|
+
const currentMs = (frame / totalFrames) * durationMs;
|
|
219
|
+
const progress = (frame / totalFrames) * 100;
|
|
220
|
+
|
|
221
|
+
currentTimeEl.textContent = \`\${Math.floor(currentMs)}ms\`;
|
|
222
|
+
currentFrameEl.textContent = frameNum;
|
|
223
|
+
scrubber.value = progress;
|
|
224
|
+
scrubber.style.background = \`linear-gradient(to right, #7c3aed \${progress}%, #3f3f46 \${progress}%)\`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Animation loop
|
|
228
|
+
function animate(time) {
|
|
229
|
+
const delta = time - lastTime;
|
|
230
|
+
lastTime = time;
|
|
231
|
+
|
|
232
|
+
frame += (delta / 1000) * fps;
|
|
233
|
+
|
|
234
|
+
if (frame >= totalFrames) {
|
|
235
|
+
if (isLooping) {
|
|
236
|
+
frame = frame % totalFrames;
|
|
237
|
+
} else {
|
|
238
|
+
frame = totalFrames - 1;
|
|
239
|
+
isPlaying = false;
|
|
240
|
+
playPauseBtn.textContent = '▶';
|
|
246
241
|
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
updateFrame();
|
|
245
|
+
|
|
246
|
+
if (isPlaying) {
|
|
247
|
+
animationId = requestAnimationFrame(animate);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Start animation
|
|
252
|
+
updateFrame();
|
|
253
|
+
animationId = requestAnimationFrame(animate);
|
|
254
|
+
|
|
255
|
+
// Controls
|
|
256
|
+
scrubber.addEventListener('input', (e) => {
|
|
257
|
+
const value = parseFloat(e.target.value);
|
|
258
|
+
frame = (value / 100) * totalFrames;
|
|
259
|
+
updateFrame();
|
|
247
260
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const mimeTypes = {
|
|
262
|
-
'.woff2': 'font/woff2',
|
|
263
|
-
'.woff': 'font/woff',
|
|
264
|
-
'.ttf': 'font/ttf',
|
|
265
|
-
'.otf': 'font/otf',
|
|
266
|
-
};
|
|
267
|
-
res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
|
|
268
|
-
res.send(fontData);
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
res.status(404).send('Font not found');
|
|
272
|
-
}
|
|
261
|
+
|
|
262
|
+
playPauseBtn.addEventListener('click', () => {
|
|
263
|
+
if (!isPlaying && frame >= totalFrames - 1) {
|
|
264
|
+
frame = 0;
|
|
265
|
+
}
|
|
266
|
+
isPlaying = !isPlaying;
|
|
267
|
+
playPauseBtn.textContent = isPlaying ? '⏸' : '▶';
|
|
268
|
+
if (isPlaying) {
|
|
269
|
+
lastTime = performance.now();
|
|
270
|
+
animationId = requestAnimationFrame(animate);
|
|
271
|
+
} else if (animationId) {
|
|
272
|
+
cancelAnimationFrame(animationId);
|
|
273
|
+
}
|
|
273
274
|
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Read the rendered video
|
|
285
|
-
const videoBuffer = await fs.readFile(outputPath);
|
|
286
|
-
// Clean up temp file
|
|
287
|
-
await fs.unlink(outputPath).catch(() => { });
|
|
288
|
-
res.setHeader('Content-Type', 'video/mp4');
|
|
289
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
290
|
-
res.send(videoBuffer);
|
|
291
|
-
}
|
|
292
|
-
catch (error) {
|
|
293
|
-
console.error('Render error:', error.message);
|
|
294
|
-
res.status(500).json({ error: error.message });
|
|
295
|
-
}
|
|
275
|
+
|
|
276
|
+
restartBtn.addEventListener('click', () => {
|
|
277
|
+
frame = 0;
|
|
278
|
+
isPlaying = true;
|
|
279
|
+
playPauseBtn.textContent = '⏸';
|
|
280
|
+
lastTime = performance.now();
|
|
281
|
+
updateFrame();
|
|
282
|
+
if (!animationId) {
|
|
283
|
+
animationId = requestAnimationFrame(animate);
|
|
284
|
+
}
|
|
296
285
|
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
286
|
+
|
|
287
|
+
stepBackBtn.addEventListener('click', () => {
|
|
288
|
+
isPlaying = false;
|
|
289
|
+
playPauseBtn.textContent = '▶';
|
|
290
|
+
if (animationId) cancelAnimationFrame(animationId);
|
|
291
|
+
frame = Math.max(0, frame - 1);
|
|
292
|
+
updateFrame();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
stepForwardBtn.addEventListener('click', () => {
|
|
296
|
+
isPlaying = false;
|
|
297
|
+
playPauseBtn.textContent = '▶';
|
|
298
|
+
if (animationId) cancelAnimationFrame(animationId);
|
|
299
|
+
frame = Math.min(totalFrames - 1, frame + 1);
|
|
300
|
+
updateFrame();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
loopBtn.addEventListener('click', () => {
|
|
304
|
+
isLooping = !isLooping;
|
|
305
|
+
loopBtn.style.background = isLooping ? '#3f3f46' : 'transparent';
|
|
306
|
+
loopBtn.style.color = isLooping ? '#ffffff' : '#71717a';
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
regenerateBtn.addEventListener('click', async () => {
|
|
310
|
+
regenerateBtn.disabled = true;
|
|
311
|
+
regenerateBtn.textContent = 'Regenerating...';
|
|
312
|
+
regenerateBtn.style.opacity = '0.5';
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const response = await fetch('/api/regenerate');
|
|
316
|
+
if (response.ok) {
|
|
317
|
+
// Reload the page to get fresh frames
|
|
318
|
+
window.location.reload();
|
|
319
|
+
} else {
|
|
320
|
+
alert('Regeneration failed');
|
|
309
321
|
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
alert('Regeneration failed: ' + error.message);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
regenerateBtn.disabled = false;
|
|
327
|
+
regenerateBtn.textContent = 'Regenerate';
|
|
328
|
+
regenerateBtn.style.opacity = '1';
|
|
329
|
+
});
|
|
330
|
+
</script>
|
|
331
|
+
</body>
|
|
332
|
+
</html>
|
|
333
|
+
`;
|
|
334
|
+
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
|
|
310
335
|
});
|
|
311
336
|
// Start server
|
|
312
337
|
const server = app.listen(port, () => {
|
|
@@ -321,16 +346,8 @@ if (import.meta.hot) {
|
|
|
321
346
|
});
|
|
322
347
|
// Cleanup function
|
|
323
348
|
const cleanup = async () => {
|
|
324
|
-
await vite.close();
|
|
325
349
|
server.close();
|
|
326
|
-
// Clean up temp files
|
|
327
|
-
try {
|
|
328
|
-
await fs.rm(tempDir, { recursive: true });
|
|
329
|
-
}
|
|
330
|
-
catch {
|
|
331
|
-
// Ignore cleanup errors
|
|
332
|
-
}
|
|
333
350
|
};
|
|
334
|
-
return { server,
|
|
351
|
+
return { server, cleanup };
|
|
335
352
|
}
|
|
336
353
|
//# sourceMappingURL=video-preview.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-preview.js","sourceRoot":"","sources":["../../src/lib/video-preview.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,
|
|
1
|
+
{"version":3,"file":"video-preview.js","sourceRoot":"","sources":["../../src/lib/video-preview.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,6 +30,7 @@ export declare function renderVideo(templateName: string, props: TemplateProps,
|
|
|
30
30
|
quality?: number;
|
|
31
31
|
onFrameProgress?: (frame: number, total: number, phase?: 'svg' | 'encode') => void;
|
|
32
32
|
config?: any;
|
|
33
|
+
debug?: boolean;
|
|
33
34
|
}): Promise<void>;
|
|
34
35
|
/**
|
|
35
36
|
* Render a video template to GIF using gifenc (pure JS, no ffmpeg required)
|
|
@@ -37,5 +38,6 @@ export declare function renderVideo(templateName: string, props: TemplateProps,
|
|
|
37
38
|
export declare function renderVideoToGif(templateName: string, props: TemplateProps, outputPath: string, options?: {
|
|
38
39
|
onFrameProgress?: (frame: number, total: number, phase?: 'svg' | 'encode') => void;
|
|
39
40
|
config?: any;
|
|
41
|
+
debug?: boolean;
|
|
40
42
|
}): Promise<void>;
|
|
41
43
|
//# sourceMappingURL=video-renderer.d.ts.map
|