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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Using the SDK Video Preview Component
|
|
3
|
+
*
|
|
4
|
+
* This demonstrates how to use previewVideo() and <VideoPreview>
|
|
5
|
+
* to display video templates in React applications.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTemplate, previewVideo, VideoPreview, type PreviewFrames } from 'loopwind/sdk';
|
|
9
|
+
import { useState, useEffect } from 'react';
|
|
10
|
+
|
|
11
|
+
// Define a video template
|
|
12
|
+
const introTemplate = defineTemplate({
|
|
13
|
+
name: 'intro-animation',
|
|
14
|
+
type: 'video',
|
|
15
|
+
size: { width: 1920, height: 1080 },
|
|
16
|
+
video: { fps: 30, duration: 3 },
|
|
17
|
+
render: ({ tw, progress, title = 'Hello World' }) => (
|
|
18
|
+
<div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-r from-blue-500 to-purple-600')}>
|
|
19
|
+
<h1
|
|
20
|
+
style={{
|
|
21
|
+
...tw('text-8xl font-bold text-white enter-fade-in/0/1000'),
|
|
22
|
+
opacity: progress
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
{title}
|
|
26
|
+
</h1>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Example 1: Pre-render at build time or on server
|
|
33
|
+
* (Next.js getStaticProps, getServerSideProps, etc.)
|
|
34
|
+
*/
|
|
35
|
+
export async function getStaticProps() {
|
|
36
|
+
// Pre-render all frames on the server
|
|
37
|
+
const frames = await previewVideo(introTemplate, {
|
|
38
|
+
title: 'Welcome!'
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
props: {
|
|
43
|
+
frames // Pass serialized frames to client
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Example 2: Client component that displays pre-rendered frames
|
|
50
|
+
*/
|
|
51
|
+
export function IntroPreview({ frames }: { frames: PreviewFrames }) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="container">
|
|
54
|
+
<h2>Video Preview</h2>
|
|
55
|
+
<VideoPreview
|
|
56
|
+
frames={frames}
|
|
57
|
+
autoPlay={true}
|
|
58
|
+
loop={true}
|
|
59
|
+
controls={true}
|
|
60
|
+
onFrameChange={(frame) => console.log(`Current frame: ${frame}`)}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Example 3: Loading frames on demand (client-side)
|
|
68
|
+
* Note: This only works if you can run Node.js code (e.g., Electron, desktop apps)
|
|
69
|
+
*/
|
|
70
|
+
export function DynamicPreview() {
|
|
71
|
+
const [frames, setFrames] = useState<PreviewFrames | null>(null);
|
|
72
|
+
const [loading, setLoading] = useState(true);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
// Load frames (only works in Node.js environment)
|
|
76
|
+
previewVideo(introTemplate, { title: 'Dynamic!' })
|
|
77
|
+
.then(setFrames)
|
|
78
|
+
.finally(() => setLoading(false));
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
if (loading) return <div>Rendering preview...</div>;
|
|
82
|
+
if (!frames) return <div>Failed to load preview</div>;
|
|
83
|
+
|
|
84
|
+
return <VideoPreview frames={frames} autoPlay loop />;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Example 4: Minimal preview without controls
|
|
89
|
+
*/
|
|
90
|
+
export function MinimalPreview({ frames }: { frames: PreviewFrames }) {
|
|
91
|
+
return (
|
|
92
|
+
<VideoPreview
|
|
93
|
+
frames={frames}
|
|
94
|
+
controls={false}
|
|
95
|
+
autoPlay={true}
|
|
96
|
+
loop={true}
|
|
97
|
+
style={{ border: '2px solid #7c3aed', borderRadius: '8px' }}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Example 5: Next.js API Route for on-demand preview generation
|
|
104
|
+
*/
|
|
105
|
+
// pages/api/preview/[template].ts
|
|
106
|
+
export async function handler(req, res) {
|
|
107
|
+
const { template } = req.query;
|
|
108
|
+
const props = req.body;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Generate preview frames on-demand
|
|
112
|
+
const frames = await previewVideo(introTemplate, props, {
|
|
113
|
+
format: 'data-url' // Use data URLs for easy JSON serialization
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
res.json(frames);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
res.status(500).json({ error: error.message });
|
|
119
|
+
}
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { defineTemplate, renderVideo } from './dist/sdk/index.js';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
const { createElement: h } = React;
|
|
6
|
+
|
|
7
|
+
function createTemplateModule(meta, render) {
|
|
8
|
+
return { meta, default: render };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const templates = [
|
|
12
|
+
// 1. Bouncing ball with spring
|
|
13
|
+
createTemplateModule(
|
|
14
|
+
{
|
|
15
|
+
name: 'bouncing-ball',
|
|
16
|
+
description: 'Bouncing ball with spring easing animation',
|
|
17
|
+
type: 'video',
|
|
18
|
+
size: { width: 600, height: 400 },
|
|
19
|
+
video: { fps: 60, duration: 4 },
|
|
20
|
+
},
|
|
21
|
+
({ tw }) =>
|
|
22
|
+
h('div', { style: tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-900 to-purple-900') },
|
|
23
|
+
h('div', { style: tw('w-48 h-48 rounded-full bg-gradient-to-br from-yellow-400 to-orange-500 shadow-2xl spring loop-translate-y-16/1000') })
|
|
24
|
+
)
|
|
25
|
+
),
|
|
26
|
+
|
|
27
|
+
// 2. Linear easing
|
|
28
|
+
createTemplateModule(
|
|
29
|
+
{
|
|
30
|
+
name: 'easing-linear',
|
|
31
|
+
description: 'Bouncing ball with linear easing (constant speed)',
|
|
32
|
+
type: 'video',
|
|
33
|
+
size: { width: 600, height: 400 },
|
|
34
|
+
video: { fps: 60, duration: 4 },
|
|
35
|
+
},
|
|
36
|
+
({ tw }) =>
|
|
37
|
+
h('div', { style: tw('flex flex-col items-center justify-center gap-6 w-full h-full bg-gradient-to-br from-slate-900 to-slate-800') },
|
|
38
|
+
h('div', { style: tw('w-32 h-32 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 shadow-2xl linear loop-translate-y-12/1000') }),
|
|
39
|
+
h('div', { style: tw('text-white text-2xl font-bold') }, 'Linear Easing'),
|
|
40
|
+
h('div', { style: tw('text-white/60 text-sm text-center max-w-md px-4') }, 'Constant speed')
|
|
41
|
+
)
|
|
42
|
+
),
|
|
43
|
+
|
|
44
|
+
// 3. Ease-in
|
|
45
|
+
createTemplateModule(
|
|
46
|
+
{
|
|
47
|
+
name: 'easing-ease-in',
|
|
48
|
+
description: 'Bouncing ball with ease-in (accelerating)',
|
|
49
|
+
type: 'video',
|
|
50
|
+
size: { width: 600, height: 400 },
|
|
51
|
+
video: { fps: 60, duration: 4 },
|
|
52
|
+
},
|
|
53
|
+
({ tw }) =>
|
|
54
|
+
h('div', { style: tw('flex flex-col items-center justify-center gap-6 w-full h-full bg-gradient-to-br from-emerald-900 to-teal-800') },
|
|
55
|
+
h('div', { style: tw('w-32 h-32 rounded-full bg-gradient-to-br from-emerald-400 to-green-500 shadow-2xl ease-in loop-translate-y-12/1000') }),
|
|
56
|
+
h('div', { style: tw('text-white text-2xl font-bold') }, 'Ease In'),
|
|
57
|
+
h('div', { style: tw('text-white/60 text-sm text-center max-w-md px-4') }, 'Slow start, fast end')
|
|
58
|
+
)
|
|
59
|
+
),
|
|
60
|
+
|
|
61
|
+
// 4. Ease-out
|
|
62
|
+
createTemplateModule(
|
|
63
|
+
{
|
|
64
|
+
name: 'easing-ease-out',
|
|
65
|
+
description: 'Bouncing ball with ease-out (decelerating)',
|
|
66
|
+
type: 'video',
|
|
67
|
+
size: { width: 600, height: 400 },
|
|
68
|
+
video: { fps: 60, duration: 4 },
|
|
69
|
+
},
|
|
70
|
+
({ tw }) =>
|
|
71
|
+
h('div', { style: tw('flex flex-col items-center justify-center gap-6 w-full h-full bg-gradient-to-br from-rose-900 to-pink-800') },
|
|
72
|
+
h('div', { style: tw('w-32 h-32 rounded-full bg-gradient-to-br from-rose-400 to-pink-500 shadow-2xl ease-out loop-translate-y-12/1000') }),
|
|
73
|
+
h('div', { style: tw('text-white text-2xl font-bold') }, 'Ease Out'),
|
|
74
|
+
h('div', { style: tw('text-white/60 text-sm text-center max-w-md px-4') }, 'Fast start, slow end')
|
|
75
|
+
)
|
|
76
|
+
),
|
|
77
|
+
|
|
78
|
+
// 5. Easing comparison (3 balls)
|
|
79
|
+
createTemplateModule(
|
|
80
|
+
{
|
|
81
|
+
name: 'easing-comparison',
|
|
82
|
+
description: 'Three balls with different easings side-by-side',
|
|
83
|
+
type: 'video',
|
|
84
|
+
size: { width: 600, height: 400 },
|
|
85
|
+
video: { fps: 60, duration: 4 },
|
|
86
|
+
},
|
|
87
|
+
({ tw }) =>
|
|
88
|
+
h('div', { style: tw('flex items-center justify-center gap-8 w-full h-full bg-gradient-to-br from-slate-900 to-gray-900 px-8') },
|
|
89
|
+
h('div', { style: tw('flex flex-col items-center gap-4') },
|
|
90
|
+
h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 shadow-2xl linear loop-translate-y-10/800') }),
|
|
91
|
+
h('div', { style: tw('text-white text-sm font-bold') }, 'Linear'),
|
|
92
|
+
h('div', { style: tw('text-white/50 text-xs') }, '800ms')
|
|
93
|
+
),
|
|
94
|
+
h('div', { style: tw('flex flex-col items-center gap-4') },
|
|
95
|
+
h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-pink-400 to-rose-500 shadow-2xl ease-out loop-translate-y-10/1000') }),
|
|
96
|
+
h('div', { style: tw('text-white text-sm font-bold') }, 'Ease Out'),
|
|
97
|
+
h('div', { style: tw('text-white/50 text-xs') }, '1000ms')
|
|
98
|
+
),
|
|
99
|
+
h('div', { style: tw('flex flex-col items-center gap-4') },
|
|
100
|
+
h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-yellow-400 to-orange-500 shadow-2xl spring loop-translate-y-10/1200') }),
|
|
101
|
+
h('div', { style: tw('text-white text-sm font-bold') }, 'Spring'),
|
|
102
|
+
h('div', { style: tw('text-white/50 text-xs') }, '1200ms')
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
),
|
|
106
|
+
|
|
107
|
+
// 6. Spring variants
|
|
108
|
+
createTemplateModule(
|
|
109
|
+
{
|
|
110
|
+
name: 'spring-variants',
|
|
111
|
+
description: 'Different spring configurations with varying bounce',
|
|
112
|
+
type: 'video',
|
|
113
|
+
size: { width: 600, height: 400 },
|
|
114
|
+
video: { fps: 60, duration: 4 },
|
|
115
|
+
},
|
|
116
|
+
({ tw }) =>
|
|
117
|
+
h('div', { style: tw('flex items-center justify-center gap-6 w-full h-full bg-gradient-to-br from-indigo-900 to-purple-900 px-6') },
|
|
118
|
+
h('div', { style: tw('flex flex-col items-center gap-3') },
|
|
119
|
+
h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-blue-400 to-cyan-500 shadow-2xl spring/1/100/10 loop-translate-y-10/1000') }),
|
|
120
|
+
h('div', { style: tw('text-white text-xs font-bold') }, 'Gentle'),
|
|
121
|
+
h('div', { style: tw('text-white/50 text-[10px]') }, '1/100/10')
|
|
122
|
+
),
|
|
123
|
+
h('div', { style: tw('flex flex-col items-center gap-3') },
|
|
124
|
+
h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-pink-400 to-rose-500 shadow-2xl spring/1/170/8 loop-translate-y-10/1000') }),
|
|
125
|
+
h('div', { style: tw('text-white text-xs font-bold') }, 'Bouncy'),
|
|
126
|
+
h('div', { style: tw('text-white/50 text-[10px]') }, '1/170/8')
|
|
127
|
+
),
|
|
128
|
+
h('div', { style: tw('flex flex-col items-center gap-3') },
|
|
129
|
+
h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-emerald-400 to-green-500 shadow-2xl spring/1/200/15 loop-translate-y-10/1000') }),
|
|
130
|
+
h('div', { style: tw('text-white text-xs font-bold') }, 'Snappy'),
|
|
131
|
+
h('div', { style: tw('text-white/50 text-[10px]') }, '1/200/15')
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
async function renderAll() {
|
|
138
|
+
console.log('Rendering all examples at 600x400...\n');
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < templates.length; i++) {
|
|
141
|
+
const templateModule = templates[i];
|
|
142
|
+
const template = defineTemplate(templateModule);
|
|
143
|
+
const outputName = `example-${templateModule.meta.name}.mp4`;
|
|
144
|
+
const outputPath = `output/${outputName}`;
|
|
145
|
+
|
|
146
|
+
console.log(`📹 Rendering ${outputName}...`);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const buffer = await renderVideo(template, {}, { quality: 20 });
|
|
150
|
+
await fs.writeFile(outputPath, buffer);
|
|
151
|
+
console.log(` ✔ ${outputName} (${(buffer.length / 1024).toFixed(1)}KB)\n`);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.log(` ✖ Failed: ${err.message}\n`);
|
|
154
|
+
console.error(err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log('✅ All examples rendered at 600x400!');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
renderAll().catch(console.error);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineTemplate, renderVideo } from './dist/sdk/index.js';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
const { createElement: h } = React;
|
|
6
|
+
|
|
7
|
+
function createTemplateModule(meta, render) {
|
|
8
|
+
return { meta, default: render };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Spring variants with enter animations to show the bounce better
|
|
12
|
+
const springVariantsTemplate = createTemplateModule(
|
|
13
|
+
{
|
|
14
|
+
name: 'spring-variants',
|
|
15
|
+
description: 'Different spring configurations with varying bounce',
|
|
16
|
+
type: 'video',
|
|
17
|
+
size: { width: 600, height: 400 },
|
|
18
|
+
video: { fps: 60, duration: 3 },
|
|
19
|
+
},
|
|
20
|
+
({ tw }) =>
|
|
21
|
+
h('div', { style: tw('flex items-center justify-center gap-6 w-full h-full bg-gradient-to-br from-indigo-900 to-purple-900 px-6') },
|
|
22
|
+
h('div', { style: tw('flex flex-col items-center gap-3') },
|
|
23
|
+
h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-blue-400 to-cyan-500 shadow-2xl opacity-0 enter-ease-spring/1/100/10 enter-fade-in/0/1500 enter--translate-y-32/0/1500') }),
|
|
24
|
+
h('div', { style: tw('text-white text-xs font-bold opacity-0 enter-fade-in/0/800') }, 'Gentle'),
|
|
25
|
+
h('div', { style: tw('text-white/50 text-[10px] opacity-0 enter-fade-in/0/800') }, '1/100/10')
|
|
26
|
+
),
|
|
27
|
+
h('div', { style: tw('flex flex-col items-center gap-3') },
|
|
28
|
+
h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-pink-400 to-rose-500 shadow-2xl opacity-0 enter-ease-spring/1/170/8 enter-fade-in/0/1500 enter--translate-y-32/0/1500') }),
|
|
29
|
+
h('div', { style: tw('text-white text-xs font-bold opacity-0 enter-fade-in/200/800') }, 'Bouncy'),
|
|
30
|
+
h('div', { style: tw('text-white/50 text-[10px] opacity-0 enter-fade-in/200/800') }, '1/170/8')
|
|
31
|
+
),
|
|
32
|
+
h('div', { style: tw('flex flex-col items-center gap-3') },
|
|
33
|
+
h('div', { style: tw('w-24 h-24 rounded-full bg-gradient-to-br from-emerald-400 to-green-500 shadow-2xl opacity-0 enter-ease-spring/1/200/15 enter-fade-in/0/1500 enter--translate-y-32/0/1500') }),
|
|
34
|
+
h('div', { style: tw('text-white text-xs font-bold opacity-0 enter-fade-in/400/800') }, 'Snappy'),
|
|
35
|
+
h('div', { style: tw('text-white/50 text-[10px] opacity-0 enter-fade-in/400/800') }, '1/200/15')
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
async function render() {
|
|
41
|
+
console.log('Rendering spring-variants with enter animations...\n');
|
|
42
|
+
|
|
43
|
+
const template = defineTemplate(springVariantsTemplate);
|
|
44
|
+
const outputPath = 'output/example-spring-variants.mp4';
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const buffer = await renderVideo(template, {}, { quality: 20 });
|
|
48
|
+
await fs.writeFile(outputPath, buffer);
|
|
49
|
+
console.log(`✔ example-spring-variants.mp4 (${(buffer.length / 1024).toFixed(1)}KB)\n`);
|
|
50
|
+
console.log('✅ Now you can see the spring differences!');
|
|
51
|
+
console.log(' - Gentle: smooth drop with slight overshoot bounce');
|
|
52
|
+
console.log(' - Bouncy: exaggerated drop with lots of bounce');
|
|
53
|
+
console.log(' - Snappy: fast drop with minimal/no bounce');
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.log(`✖ Failed: ${err.message}\n`);
|
|
56
|
+
console.error(err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
render().catch(console.error);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineTemplate, renderVideo } from './dist/sdk/index.js';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
const { createElement: h } = React;
|
|
6
|
+
|
|
7
|
+
function createTemplateModule(meta, render) {
|
|
8
|
+
return { meta, default: render };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const staggeredTextTemplate = createTemplateModule(
|
|
12
|
+
{
|
|
13
|
+
name: 'staggered-text',
|
|
14
|
+
description: 'Staggered text animation with spring easing',
|
|
15
|
+
type: 'video',
|
|
16
|
+
size: { width: 600, height: 400 },
|
|
17
|
+
video: { fps: 60, duration: 3 },
|
|
18
|
+
},
|
|
19
|
+
({ tw }) => {
|
|
20
|
+
const text = 'loopwind';
|
|
21
|
+
const letters = text.split('');
|
|
22
|
+
const staggerDelay = 80; // ms between each letter
|
|
23
|
+
|
|
24
|
+
return h('div', { style: tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-violet-900 via-purple-900 to-fuchsia-900') },
|
|
25
|
+
h('div', { style: tw('flex items-center justify-center gap-1') },
|
|
26
|
+
...letters.map((letter, i) =>
|
|
27
|
+
h('span', {
|
|
28
|
+
key: i,
|
|
29
|
+
style: tw(`text-8xl font-black bg-clip-text text-transparent bg-gradient-to-br from-cyan-400 via-blue-400 to-purple-400 opacity-0 enter-ease-spring enter-fade-in/${i * staggerDelay}/600 enter--translate-y-12/${i * staggerDelay}/600`)
|
|
30
|
+
}, letter)
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
async function render() {
|
|
38
|
+
console.log('Rendering staggered-text animation...\n');
|
|
39
|
+
|
|
40
|
+
const template = defineTemplate(staggeredTextTemplate);
|
|
41
|
+
const outputPath = 'output/example-staggered-text.mp4';
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const buffer = await renderVideo(template, {}, { quality: 20 });
|
|
45
|
+
await fs.writeFile(outputPath, buffer);
|
|
46
|
+
console.log(`✔ example-staggered-text.mp4 (${(buffer.length / 1024).toFixed(1)}KB)\n`);
|
|
47
|
+
console.log('✅ Staggered text animation complete!');
|
|
48
|
+
console.log(' Each letter animates in with spring easing');
|
|
49
|
+
console.log(' 80ms delay between each letter');
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.log(`✖ Failed: ${err.message}\n`);
|
|
52
|
+
console.error(err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render().catch(console.error);
|