loopwind 0.19.0 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +4 -0
- package/dist/lib/renderer.d.ts.map +1 -1
- package/dist/lib/renderer.js +7 -2
- 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 +275 -248
- 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 +14 -4
- package/dist/lib/video-renderer.js.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +1 -0
- 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/examples/nextjs-template-import.ts +2 -2
- package/examples/sdk-video-preview.tsx +120 -0
- package/package.json +1 -1
- package/render-examples-600x400.mjs +161 -0
- package/render-spring-variants-fixed.mjs +60 -0
- package/render-staggered-text.mjs +56 -0
- package/test-sdk-config.mjs +138 -81
- package/test-static-debug.tsx +19 -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/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/output/sdk-static.jpg +0 -0
|
@@ -1,168 +1,102 @@
|
|
|
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
|
-
|
|
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
|
+
// Create shared caches to avoid re-fetching images/QR codes for each frame
|
|
20
|
+
const sharedCache = {
|
|
21
|
+
qr: new Map(),
|
|
22
|
+
image: new Map()
|
|
23
|
+
};
|
|
24
|
+
const startTime = Date.now();
|
|
25
|
+
for (let frame = 0; frame < totalFrames; frame++) {
|
|
26
|
+
const progress = frame / totalFrames;
|
|
27
|
+
const svg = await renderToSVG(templateName, {
|
|
28
|
+
...props,
|
|
29
|
+
frame,
|
|
30
|
+
progress,
|
|
31
|
+
}, { config: loopwindConfig, debug, sharedCache });
|
|
32
|
+
frames.push(svg);
|
|
33
|
+
// Show progress every 30 frames
|
|
34
|
+
if (frame % 30 === 0 || frame === totalFrames - 1) {
|
|
35
|
+
const percent = Math.round((frame / totalFrames) * 100);
|
|
36
|
+
process.stdout.write(`\r${chalk.dim(` Rendering frames... ${percent}% (${frame + 1}/${totalFrames})`)}`);
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// loopwind.json doesn't exist or couldn't be read - that's fine
|
|
39
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
40
|
+
console.log(`\n${chalk.green('✓')} ${chalk.dim(`Rendered ${totalFrames} frames in ${elapsed}s`)}\n`);
|
|
41
|
+
// Serve individual frame as SVG
|
|
42
|
+
app.get('/api/frame/:frameNumber', (req, res) => {
|
|
43
|
+
const frameNum = parseInt(req.params.frameNumber);
|
|
44
|
+
if (frameNum < 0 || frameNum >= frames.length) {
|
|
45
|
+
res.status(404).send('Frame not found');
|
|
46
|
+
return;
|
|
40
47
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
borderRadius: tailwindConfig.theme.extend.borderRadius,
|
|
59
|
-
} : undefined
|
|
60
|
-
}
|
|
48
|
+
res.setHeader('Content-Type', 'image/svg+xml');
|
|
49
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
50
|
+
res.send(frames[frameNum]);
|
|
51
|
+
});
|
|
52
|
+
// Serve template directory for local assets
|
|
53
|
+
app.use('/template', express.static(templateDir));
|
|
54
|
+
// Regenerate frames endpoint (for HMR-like behavior)
|
|
55
|
+
app.get('/api/regenerate', async (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
console.log(chalk.blue('\n📝 Regenerating frames...'));
|
|
58
|
+
// Clear caches
|
|
59
|
+
clearTemplateCache();
|
|
60
|
+
frames.length = 0;
|
|
61
|
+
// Create fresh shared caches for regeneration
|
|
62
|
+
const regenerateCache = {
|
|
63
|
+
qr: new Map(),
|
|
64
|
+
image: new Map()
|
|
61
65
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const fontDef = value;
|
|
76
|
-
if (fontDef.files && fontDef.files.length > 0 && fontDef.family && fontDef.family[0]) {
|
|
77
|
-
const fontName = fontDef.family[0];
|
|
78
|
-
// Use the sans font as the default body font
|
|
79
|
-
if (key === 'sans') {
|
|
80
|
-
defaultFontFamily = fontDef.family.map(f => f.includes(' ') ? `'${f}'` : f).join(', ');
|
|
81
|
-
}
|
|
82
|
-
for (const fileDef of fontDef.files) {
|
|
83
|
-
// Resolve font path relative to CWD
|
|
84
|
-
const fontPath = path.resolve(fileDef.path);
|
|
85
|
-
const fontFileName = path.basename(fontPath);
|
|
86
|
-
const fontExt = path.extname(fontPath).toLowerCase();
|
|
87
|
-
// Map extension to format
|
|
88
|
-
const formatMap = {
|
|
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
|
-
}`;
|
|
66
|
+
// Re-render all frames
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
for (let frame = 0; frame < totalFrames; frame++) {
|
|
69
|
+
const progress = frame / totalFrames;
|
|
70
|
+
const svg = await renderToSVG(templateName, {
|
|
71
|
+
...props,
|
|
72
|
+
frame,
|
|
73
|
+
progress,
|
|
74
|
+
}, { config: loopwindConfig, debug, sharedCache: regenerateCache });
|
|
75
|
+
frames.push(svg);
|
|
76
|
+
if (frame % 30 === 0 || frame === totalFrames - 1) {
|
|
77
|
+
const percent = Math.round((frame / totalFrames) * 100);
|
|
78
|
+
process.stdout.write(`\r${chalk.dim(` Rendering frames... ${percent}% (${frame + 1}/${totalFrames})`)}`);
|
|
104
79
|
}
|
|
105
80
|
}
|
|
81
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
82
|
+
console.log(`\n${chalk.green('✓')} ${chalk.dim(`Regenerated ${totalFrames} frames in ${elapsed}s`)}\n`);
|
|
83
|
+
res.json({ success: true, totalFrames });
|
|
106
84
|
}
|
|
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 = `
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error(chalk.red('✖ Regeneration error:'), error.message);
|
|
87
|
+
res.status(500).json({ error: error.message });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Serve index.html for root
|
|
91
|
+
app.get('/', (req, res) => {
|
|
92
|
+
const html = `
|
|
156
93
|
<!DOCTYPE html>
|
|
157
94
|
<html lang="en">
|
|
158
95
|
<head>
|
|
159
96
|
<meta charset="UTF-8">
|
|
160
97
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
161
98
|
<title>Video Preview: ${templateName}</title>
|
|
162
|
-
<style
|
|
163
|
-
html {
|
|
164
|
-
font-size: 16px; /* Match Satori's default rem base */
|
|
165
|
-
}
|
|
99
|
+
<style>
|
|
166
100
|
* {
|
|
167
101
|
margin: 0;
|
|
168
102
|
padding: 0;
|
|
@@ -170,7 +104,7 @@ if (import.meta.hot) {
|
|
|
170
104
|
}
|
|
171
105
|
body {
|
|
172
106
|
overflow: hidden;
|
|
173
|
-
font-family:
|
|
107
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
174
108
|
}
|
|
175
109
|
/* Custom range slider styling */
|
|
176
110
|
input[type="range"] {
|
|
@@ -208,105 +142,206 @@ if (import.meta.hot) {
|
|
|
208
142
|
</head>
|
|
209
143
|
<body>
|
|
210
144
|
<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
|
-
|
|
145
|
+
<script type="module">
|
|
146
|
+
const meta = ${JSON.stringify(meta)};
|
|
147
|
+
const totalFrames = ${totalFrames};
|
|
148
|
+
|
|
149
|
+
// Mount the simplified video player
|
|
150
|
+
const root = document.getElementById('root');
|
|
151
|
+
|
|
152
|
+
let frame = 0;
|
|
153
|
+
let isPlaying = true;
|
|
154
|
+
let isLooping = true;
|
|
155
|
+
let animationId = null;
|
|
156
|
+
let lastTime = performance.now();
|
|
157
|
+
|
|
158
|
+
const { fps, duration } = meta.video;
|
|
159
|
+
const durationMs = duration * 1000;
|
|
160
|
+
|
|
161
|
+
// Create UI
|
|
162
|
+
root.innerHTML = \`
|
|
163
|
+
<div style="display: flex; flex-direction: column; height: 100vh; background: #0a0a0a; color: #ffffff;">
|
|
164
|
+
<!-- Header -->
|
|
165
|
+
<div style="background: #18181b; border-bottom: 1px solid #27272a; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center;">
|
|
166
|
+
<h1 style="font-size: 1.25rem; font-weight: 600; margin: 0;">loopwind preview: ${templateName}</h1>
|
|
167
|
+
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
|
168
|
+
<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;">
|
|
169
|
+
Regenerate
|
|
170
|
+
</button>
|
|
171
|
+
<span style="background: #7c3aed; color: white; padding: 0.25rem 0.75rem; border-radius: 0.375rem; font-size: 0.875rem; font-weight: 500;">VIDEO</span>
|
|
172
|
+
<span style="background: #27272a; color: #a1a1aa; padding: 0.25rem 0.75rem; border-radius: 0.375rem; font-size: 0.75rem; font-family: monospace;">
|
|
173
|
+
\${meta.size.width}×\${meta.size.height} @ \${fps}fps
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Canvas -->
|
|
179
|
+
<div id="canvas-container" style="flex: 1; display: flex; align-items: center; justify-content: center; padding: 2rem; overflow: hidden;">
|
|
180
|
+
<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%;">
|
|
181
|
+
<img id="frame-img" style="display: block; width: 100%; height: 100%; object-fit: contain;" width="\${meta.size.width}" height="\${meta.size.height}" />
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- Controls -->
|
|
186
|
+
<div style="background: #18181b; border-top: 1px solid #27272a; padding: 1rem 2rem;">
|
|
187
|
+
<div style="margin-bottom: 0.75rem;">
|
|
188
|
+
<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%);" />
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
192
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
193
|
+
<button id="restart-btn" style="background: transparent; border: none; color: #a1a1aa; cursor: pointer; padding: 0.5rem; font-size: 1rem;">⏮</button>
|
|
194
|
+
<button id="step-back-btn" style="background: transparent; border: none; color: #a1a1aa; cursor: pointer; padding: 0.5rem; font-size: 1rem;">⏪</button>
|
|
195
|
+
<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>
|
|
196
|
+
<button id="step-forward-btn" style="background: transparent; border: none; color: #a1a1aa; cursor: pointer; padding: 0.5rem; font-size: 1rem;">⏩</button>
|
|
197
|
+
<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>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div style="font-family: monospace; font-size: 0.875rem; color: #a1a1aa;">
|
|
201
|
+
<span id="current-time" style="color: #ffffff;">0ms</span>
|
|
202
|
+
/
|
|
203
|
+
<span>\${Math.floor(durationMs)}ms</span>
|
|
204
|
+
<span style="margin-left: 1rem; color: #71717a;">Frame <span id="current-frame">0</span> / \${totalFrames}</span>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
\`;
|
|
210
|
+
|
|
211
|
+
// Get elements
|
|
212
|
+
const frameImg = document.getElementById('frame-img');
|
|
213
|
+
const scrubber = document.getElementById('scrubber');
|
|
214
|
+
const playPauseBtn = document.getElementById('play-pause-btn');
|
|
215
|
+
const restartBtn = document.getElementById('restart-btn');
|
|
216
|
+
const stepBackBtn = document.getElementById('step-back-btn');
|
|
217
|
+
const stepForwardBtn = document.getElementById('step-forward-btn');
|
|
218
|
+
const loopBtn = document.getElementById('loop-btn');
|
|
219
|
+
const regenerateBtn = document.getElementById('regenerate-btn');
|
|
220
|
+
const currentTimeEl = document.getElementById('current-time');
|
|
221
|
+
const currentFrameEl = document.getElementById('current-frame');
|
|
222
|
+
|
|
223
|
+
// Update frame
|
|
224
|
+
function updateFrame() {
|
|
225
|
+
const frameNum = Math.floor(frame);
|
|
226
|
+
frameImg.src = \`/api/frame/\${frameNum}?t=\${Date.now()}\`;
|
|
227
|
+
|
|
228
|
+
const currentMs = (frame / totalFrames) * durationMs;
|
|
229
|
+
const progress = (frame / totalFrames) * 100;
|
|
230
|
+
|
|
231
|
+
currentTimeEl.textContent = \`\${Math.floor(currentMs)}ms\`;
|
|
232
|
+
currentFrameEl.textContent = frameNum;
|
|
233
|
+
scrubber.value = progress;
|
|
234
|
+
scrubber.style.background = \`linear-gradient(to right, #7c3aed \${progress}%, #3f3f46 \${progress}%)\`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Animation loop
|
|
238
|
+
function animate(time) {
|
|
239
|
+
const delta = time - lastTime;
|
|
240
|
+
lastTime = time;
|
|
241
|
+
|
|
242
|
+
frame += (delta / 1000) * fps;
|
|
243
|
+
|
|
244
|
+
if (frame >= totalFrames) {
|
|
245
|
+
if (isLooping) {
|
|
246
|
+
frame = frame % totalFrames;
|
|
247
|
+
} else {
|
|
248
|
+
frame = totalFrames - 1;
|
|
249
|
+
isPlaying = false;
|
|
250
|
+
playPauseBtn.textContent = '▶';
|
|
246
251
|
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
updateFrame();
|
|
255
|
+
|
|
256
|
+
if (isPlaying) {
|
|
257
|
+
animationId = requestAnimationFrame(animate);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Start animation
|
|
262
|
+
updateFrame();
|
|
263
|
+
animationId = requestAnimationFrame(animate);
|
|
264
|
+
|
|
265
|
+
// Controls
|
|
266
|
+
scrubber.addEventListener('input', (e) => {
|
|
267
|
+
const value = parseFloat(e.target.value);
|
|
268
|
+
frame = (value / 100) * totalFrames;
|
|
269
|
+
updateFrame();
|
|
247
270
|
});
|
|
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
|
-
}
|
|
271
|
+
|
|
272
|
+
playPauseBtn.addEventListener('click', () => {
|
|
273
|
+
if (!isPlaying && frame >= totalFrames - 1) {
|
|
274
|
+
frame = 0;
|
|
275
|
+
}
|
|
276
|
+
isPlaying = !isPlaying;
|
|
277
|
+
playPauseBtn.textContent = isPlaying ? '⏸' : '▶';
|
|
278
|
+
if (isPlaying) {
|
|
279
|
+
lastTime = performance.now();
|
|
280
|
+
animationId = requestAnimationFrame(animate);
|
|
281
|
+
} else if (animationId) {
|
|
282
|
+
cancelAnimationFrame(animationId);
|
|
283
|
+
}
|
|
273
284
|
});
|
|
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
|
-
}
|
|
285
|
+
|
|
286
|
+
restartBtn.addEventListener('click', () => {
|
|
287
|
+
frame = 0;
|
|
288
|
+
isPlaying = true;
|
|
289
|
+
playPauseBtn.textContent = '⏸';
|
|
290
|
+
lastTime = performance.now();
|
|
291
|
+
updateFrame();
|
|
292
|
+
if (!animationId) {
|
|
293
|
+
animationId = requestAnimationFrame(animate);
|
|
294
|
+
}
|
|
296
295
|
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
296
|
+
|
|
297
|
+
stepBackBtn.addEventListener('click', () => {
|
|
298
|
+
isPlaying = false;
|
|
299
|
+
playPauseBtn.textContent = '▶';
|
|
300
|
+
if (animationId) cancelAnimationFrame(animationId);
|
|
301
|
+
frame = Math.max(0, frame - 1);
|
|
302
|
+
updateFrame();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
stepForwardBtn.addEventListener('click', () => {
|
|
306
|
+
isPlaying = false;
|
|
307
|
+
playPauseBtn.textContent = '▶';
|
|
308
|
+
if (animationId) cancelAnimationFrame(animationId);
|
|
309
|
+
frame = Math.min(totalFrames - 1, frame + 1);
|
|
310
|
+
updateFrame();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
loopBtn.addEventListener('click', () => {
|
|
314
|
+
isLooping = !isLooping;
|
|
315
|
+
loopBtn.style.background = isLooping ? '#3f3f46' : 'transparent';
|
|
316
|
+
loopBtn.style.color = isLooping ? '#ffffff' : '#71717a';
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
regenerateBtn.addEventListener('click', async () => {
|
|
320
|
+
regenerateBtn.disabled = true;
|
|
321
|
+
regenerateBtn.textContent = 'Regenerating...';
|
|
322
|
+
regenerateBtn.style.opacity = '0.5';
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const response = await fetch('/api/regenerate');
|
|
326
|
+
if (response.ok) {
|
|
327
|
+
// Reload the page to get fresh frames
|
|
328
|
+
window.location.reload();
|
|
329
|
+
} else {
|
|
330
|
+
alert('Regeneration failed');
|
|
309
331
|
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
alert('Regeneration failed: ' + error.message);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
regenerateBtn.disabled = false;
|
|
337
|
+
regenerateBtn.textContent = 'Regenerate';
|
|
338
|
+
regenerateBtn.style.opacity = '1';
|
|
339
|
+
});
|
|
340
|
+
</script>
|
|
341
|
+
</body>
|
|
342
|
+
</html>
|
|
343
|
+
`;
|
|
344
|
+
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
|
|
310
345
|
});
|
|
311
346
|
// Start server
|
|
312
347
|
const server = app.listen(port, () => {
|
|
@@ -321,16 +356,8 @@ if (import.meta.hot) {
|
|
|
321
356
|
});
|
|
322
357
|
// Cleanup function
|
|
323
358
|
const cleanup = async () => {
|
|
324
|
-
await vite.close();
|
|
325
359
|
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
360
|
};
|
|
334
|
-
return { server,
|
|
361
|
+
return { server, cleanup };
|
|
335
362
|
}
|
|
336
363
|
//# 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
|