loopwind 0.19.0 → 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/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/website/test-playground.mjs +45 -0
- package/output/sdk-static.jpg +0 -0
|
@@ -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);
|
package/test-sdk-config.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { defineTemplate, renderImage, renderVideo } from './dist/sdk/index.js';
|
|
7
|
+
import * as configTestTemplate from './test-templates/config-test.mjs';
|
|
7
8
|
import React from 'react';
|
|
8
9
|
import fs from 'fs/promises';
|
|
9
10
|
import path from 'path';
|
|
@@ -12,6 +13,11 @@ import { fileURLToPath } from 'url';
|
|
|
12
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const { createElement: h } = React;
|
|
14
15
|
|
|
16
|
+
// Helper to create inline template modules for testing
|
|
17
|
+
function createTemplateModule(meta, render) {
|
|
18
|
+
return { meta, default: render };
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
// Test utilities
|
|
16
22
|
let testsPassed = 0;
|
|
17
23
|
let testsFailed = 0;
|
|
@@ -52,15 +58,7 @@ await test('Config in defineTemplate - Image', async () => {
|
|
|
52
58
|
},
|
|
53
59
|
};
|
|
54
60
|
|
|
55
|
-
const template = defineTemplate({
|
|
56
|
-
name: 'test-config-template',
|
|
57
|
-
size: { width: 400, height: 400 },
|
|
58
|
-
config,
|
|
59
|
-
render: ({ tw, title }) =>
|
|
60
|
-
h('div', { style: tw('flex items-center justify-center w-full h-full bg-background') },
|
|
61
|
-
h('h1', { style: tw('text-4xl font-bold text-primary') }, title)
|
|
62
|
-
),
|
|
63
|
-
});
|
|
61
|
+
const template = defineTemplate(configTestTemplate, { config });
|
|
64
62
|
|
|
65
63
|
assert(template.config, 'Template should have config');
|
|
66
64
|
assert(template.config.colors.primary === '#ff0000', 'Config colors should match');
|
|
@@ -74,14 +72,20 @@ await test('Config in defineTemplate - Image', async () => {
|
|
|
74
72
|
|
|
75
73
|
// Test 2: Config in renderImage options
|
|
76
74
|
await test('Config in renderImage options', async () => {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
const templateModule = createTemplateModule(
|
|
76
|
+
{
|
|
77
|
+
name: 'test-render-config',
|
|
78
|
+
type: 'image',
|
|
79
|
+
size: { width: 400, height: 400 },
|
|
80
|
+
props: { title: 'string' },
|
|
81
|
+
},
|
|
82
|
+
({ tw, title }) =>
|
|
81
83
|
h('div', { style: tw('flex items-center justify-center w-full h-full bg-secondary') },
|
|
82
84
|
h('h1', { style: tw('text-4xl font-bold text-accent') }, title)
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const template = defineTemplate(templateModule);
|
|
85
89
|
|
|
86
90
|
const svg = await renderImage(
|
|
87
91
|
template,
|
|
@@ -104,18 +108,25 @@ await test('Config in renderImage options', async () => {
|
|
|
104
108
|
|
|
105
109
|
// Test 3: Config override (options override template)
|
|
106
110
|
await test('Config override - Options override template', async () => {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
const templateModule = createTemplateModule(
|
|
112
|
+
{
|
|
113
|
+
name: 'test-override',
|
|
114
|
+
type: 'image',
|
|
115
|
+
size: { width: 400, height: 400 },
|
|
116
|
+
props: {},
|
|
117
|
+
},
|
|
118
|
+
({ tw }) =>
|
|
119
|
+
h('div', { style: tw('flex items-center justify-center w-full h-full bg-brand') },
|
|
120
|
+
h('h1', { style: tw('text-4xl font-bold') }, 'Override Test')
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const template = defineTemplate(templateModule, {
|
|
110
125
|
config: {
|
|
111
126
|
colors: {
|
|
112
127
|
brand: '#111111',
|
|
113
128
|
},
|
|
114
129
|
},
|
|
115
|
-
render: ({ tw }) =>
|
|
116
|
-
h('div', { style: tw('flex items-center justify-center w-full h-full bg-brand') },
|
|
117
|
-
h('h1', { style: tw('text-4xl font-bold') }, 'Override Test')
|
|
118
|
-
),
|
|
119
130
|
});
|
|
120
131
|
|
|
121
132
|
const svg = await renderImage(
|
|
@@ -138,14 +149,20 @@ await test('Config override - Options override template', async () => {
|
|
|
138
149
|
|
|
139
150
|
// Test 4: PNG format with config
|
|
140
151
|
await test('PNG format with config', async () => {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
152
|
+
const templateModule = createTemplateModule(
|
|
153
|
+
{
|
|
154
|
+
name: 'test-png',
|
|
155
|
+
type: 'image',
|
|
156
|
+
size: { width: 200, height: 200 },
|
|
157
|
+
props: {},
|
|
158
|
+
},
|
|
159
|
+
({ tw }) =>
|
|
145
160
|
h('div', { style: tw('flex items-center justify-center w-full h-full bg-primary') },
|
|
146
161
|
h('h1', { style: tw('text-2xl font-bold text-white') }, 'PNG')
|
|
147
|
-
)
|
|
148
|
-
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const template = defineTemplate(templateModule);
|
|
149
166
|
|
|
150
167
|
const png = await renderImage(
|
|
151
168
|
template,
|
|
@@ -168,21 +185,27 @@ await test('PNG format with config', async () => {
|
|
|
168
185
|
|
|
169
186
|
// Test 5: Video with config
|
|
170
187
|
await test('Video with config', async () => {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
primary: '#ff6600',
|
|
179
|
-
},
|
|
188
|
+
const templateModule = createTemplateModule(
|
|
189
|
+
{
|
|
190
|
+
name: 'test-video',
|
|
191
|
+
type: 'video',
|
|
192
|
+
size: { width: 400, height: 400 },
|
|
193
|
+
video: { fps: 30, duration: 0.5 },
|
|
194
|
+
props: {},
|
|
180
195
|
},
|
|
181
|
-
|
|
196
|
+
({ tw, progress }) => {
|
|
182
197
|
const opacity = progress;
|
|
183
198
|
return h('div', { style: tw('flex items-center justify-center w-full h-full bg-white') },
|
|
184
199
|
h('h1', { style: { ...tw('text-4xl font-bold text-primary'), opacity } }, 'Video')
|
|
185
200
|
);
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const template = defineTemplate(templateModule, {
|
|
205
|
+
config: {
|
|
206
|
+
colors: {
|
|
207
|
+
primary: '#ff6600',
|
|
208
|
+
},
|
|
186
209
|
},
|
|
187
210
|
});
|
|
188
211
|
|
|
@@ -197,20 +220,26 @@ await test('Video with config', async () => {
|
|
|
197
220
|
|
|
198
221
|
// Test 6: Video with config override
|
|
199
222
|
await test('Video with config override', async () => {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
223
|
+
const templateModule = createTemplateModule(
|
|
224
|
+
{
|
|
225
|
+
name: 'test-video-override',
|
|
226
|
+
type: 'video',
|
|
227
|
+
size: { width: 400, height: 400 },
|
|
228
|
+
video: { fps: 30, duration: 0.5 },
|
|
229
|
+
props: {},
|
|
230
|
+
},
|
|
231
|
+
({ tw, progress }) =>
|
|
232
|
+
h('div', { style: tw('flex items-center justify-center w-full h-full bg-brand') },
|
|
233
|
+
h('h1', { style: { ...tw('text-4xl font-bold'), opacity: progress } }, 'Video')
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const template = defineTemplate(templateModule, {
|
|
205
238
|
config: {
|
|
206
239
|
colors: {
|
|
207
240
|
brand: '#333333',
|
|
208
241
|
},
|
|
209
242
|
},
|
|
210
|
-
render: ({ tw, progress }) =>
|
|
211
|
-
h('div', { style: tw('flex items-center justify-center w-full h-full bg-brand') },
|
|
212
|
-
h('h1', { style: { ...tw('text-4xl font-bold'), opacity: progress } }, 'Video')
|
|
213
|
-
),
|
|
214
243
|
});
|
|
215
244
|
|
|
216
245
|
const mp4 = await renderVideo(template, {}, {
|
|
@@ -237,18 +266,23 @@ await test('Multiple custom colors', async () => {
|
|
|
237
266
|
},
|
|
238
267
|
};
|
|
239
268
|
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
269
|
+
const templateModule = createTemplateModule(
|
|
270
|
+
{
|
|
271
|
+
name: 'test-multi-colors',
|
|
272
|
+
type: 'image',
|
|
273
|
+
size: { width: 600, height: 400 },
|
|
274
|
+
props: {},
|
|
275
|
+
},
|
|
276
|
+
({ tw }) =>
|
|
245
277
|
h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-brand-light p-8') },
|
|
246
278
|
h('h1', { style: tw('text-4xl font-bold text-brand') }, 'Brand'),
|
|
247
279
|
h('p', { style: tw('text-2xl text-brand-dark') }, 'Brand Dark'),
|
|
248
280
|
h('p', { style: tw('text-xl text-accent') }, 'Accent'),
|
|
249
281
|
h('p', { style: tw('text-lg text-success') }, 'Success')
|
|
250
|
-
)
|
|
251
|
-
|
|
282
|
+
)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const template = defineTemplate(templateModule, { config });
|
|
252
286
|
|
|
253
287
|
const svg = await renderImage(template, {}, { format: 'svg' });
|
|
254
288
|
const svgString = svg.toString();
|
|
@@ -272,16 +306,21 @@ await test('Fonts in config', async () => {
|
|
|
272
306
|
},
|
|
273
307
|
};
|
|
274
308
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
309
|
+
const templateModule = createTemplateModule(
|
|
310
|
+
{
|
|
311
|
+
name: 'test-fonts',
|
|
312
|
+
type: 'image',
|
|
313
|
+
size: { width: 400, height: 400 },
|
|
314
|
+
props: {},
|
|
315
|
+
},
|
|
316
|
+
({ tw }) =>
|
|
280
317
|
h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-white') },
|
|
281
318
|
h('h1', { style: tw('font-sans text-4xl text-text') }, 'Sans Font'),
|
|
282
319
|
h('p', { style: tw('font-mono text-xl text-text') }, 'Mono Font')
|
|
283
|
-
)
|
|
284
|
-
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const template = defineTemplate(templateModule, { config });
|
|
285
324
|
|
|
286
325
|
const svg = await renderImage(template, {}, { format: 'svg' });
|
|
287
326
|
const svgString = svg.toString();
|
|
@@ -292,14 +331,20 @@ await test('Fonts in config', async () => {
|
|
|
292
331
|
|
|
293
332
|
// Test 9: Empty config (should work with defaults)
|
|
294
333
|
await test('Empty config - should work with defaults', async () => {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
334
|
+
const templateModule = createTemplateModule(
|
|
335
|
+
{
|
|
336
|
+
name: 'test-no-config',
|
|
337
|
+
type: 'image',
|
|
338
|
+
size: { width: 400, height: 400 },
|
|
339
|
+
props: {},
|
|
340
|
+
},
|
|
341
|
+
({ tw }) =>
|
|
299
342
|
h('div', { style: tw('flex items-center justify-center w-full h-full bg-white') },
|
|
300
343
|
h('h1', { style: tw('text-4xl font-bold text-black') }, 'No Config')
|
|
301
|
-
)
|
|
302
|
-
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const template = defineTemplate(templateModule);
|
|
303
348
|
|
|
304
349
|
const svg = await renderImage(template, {}, { format: 'svg' });
|
|
305
350
|
assert(svg.length > 0, 'SVG should be generated without config');
|
|
@@ -307,19 +352,26 @@ await test('Empty config - should work with defaults', async () => {
|
|
|
307
352
|
|
|
308
353
|
// Test 10: Partial config merge
|
|
309
354
|
await test('Partial config merge', async () => {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
355
|
+
const templateModule = createTemplateModule(
|
|
356
|
+
{
|
|
357
|
+
name: 'test-partial-merge',
|
|
358
|
+
type: 'image',
|
|
359
|
+
size: { width: 400, height: 400 },
|
|
360
|
+
props: {},
|
|
361
|
+
},
|
|
362
|
+
({ tw }) =>
|
|
363
|
+
h('div', { style: tw('flex items-center justify-center w-full h-full bg-secondary') },
|
|
364
|
+
h('h1', { style: tw('text-4xl font-bold text-primary') }, 'Merge')
|
|
365
|
+
)
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const template = defineTemplate(templateModule, {
|
|
313
369
|
config: {
|
|
314
370
|
colors: {
|
|
315
371
|
primary: '#111111',
|
|
316
372
|
secondary: '#222222',
|
|
317
373
|
},
|
|
318
374
|
},
|
|
319
|
-
render: ({ tw }) =>
|
|
320
|
-
h('div', { style: tw('flex items-center justify-center w-full h-full bg-secondary') },
|
|
321
|
-
h('h1', { style: tw('text-4xl font-bold text-primary') }, 'Merge')
|
|
322
|
-
),
|
|
323
375
|
});
|
|
324
376
|
|
|
325
377
|
const svg = await renderImage(
|
|
@@ -363,16 +415,21 @@ await test('Font files from URLs', async () => {
|
|
|
363
415
|
},
|
|
364
416
|
};
|
|
365
417
|
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
418
|
+
const templateModule = createTemplateModule(
|
|
419
|
+
{
|
|
420
|
+
name: 'test-font-files',
|
|
421
|
+
type: 'image',
|
|
422
|
+
size: { width: 600, height: 400 },
|
|
423
|
+
props: {},
|
|
424
|
+
},
|
|
425
|
+
({ tw }) =>
|
|
371
426
|
h('div', { style: tw('flex flex-col items-center justify-center w-full h-full bg-white p-8') },
|
|
372
427
|
h('h1', { style: tw('font-sans text-6xl font-bold text-text') }, 'Bold Text'),
|
|
373
428
|
h('p', { style: tw('font-sans text-3xl font-normal text-text') }, 'Normal Text')
|
|
374
|
-
)
|
|
375
|
-
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
const template = defineTemplate(templateModule, { config });
|
|
376
433
|
|
|
377
434
|
const svg = await renderImage(template, {}, { format: 'svg' });
|
|
378
435
|
assert(svg.length > 0, 'SVG should be generated with font files from URLs');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export const meta = {
|
|
4
|
+
name: 'test-static-debug',
|
|
5
|
+
description: 'Simple static image for debug testing',
|
|
6
|
+
type: 'image' as const,
|
|
7
|
+
size: { width: 600, height: 400 },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function TestStatic({ tw }: { tw: any }) {
|
|
11
|
+
return (
|
|
12
|
+
<div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-500 to-purple-600')}>
|
|
13
|
+
<div style={tw('flex flex-col items-center gap-4 p-8 bg-white rounded-lg')}>
|
|
14
|
+
<h1 style={tw('text-4xl font-bold text-gray-900')}>Debug Test</h1>
|
|
15
|
+
<p style={tw('text-lg text-gray-600')}>Testing Satori debug mode</p>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|