loopwind 0.21.0 → 0.23.0
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 +9 -2
- package/dist/cli.js +5 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/render.d.ts +1 -0
- package/dist/commands/render.d.ts.map +1 -1
- package/dist/commands/render.js +1 -0
- package/dist/commands/render.js.map +1 -1
- package/dist/lib/encode-worker.d.ts +2 -0
- package/dist/lib/encode-worker.d.ts.map +1 -0
- package/dist/lib/encode-worker.js +29 -0
- package/dist/lib/encode-worker.js.map +1 -0
- package/dist/lib/mjpeg-muxer.d.ts +46 -0
- package/dist/lib/mjpeg-muxer.d.ts.map +1 -0
- package/dist/lib/mjpeg-muxer.js +513 -0
- package/dist/lib/mjpeg-muxer.js.map +1 -0
- package/dist/lib/render-core.d.ts +63 -0
- package/dist/lib/render-core.d.ts.map +1 -0
- package/dist/lib/render-core.js +65 -0
- package/dist/lib/render-core.js.map +1 -0
- package/dist/lib/renderer.d.ts.map +1 -1
- package/dist/lib/renderer.js +10 -7
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/resvg-init.d.ts +15 -0
- package/dist/lib/resvg-init.d.ts.map +1 -0
- package/dist/lib/resvg-init.js +55 -0
- package/dist/lib/resvg-init.js.map +1 -0
- package/dist/lib/tailwind/colors.d.ts +8 -0
- package/dist/lib/tailwind/colors.d.ts.map +1 -0
- package/dist/lib/tailwind/colors.js +102 -0
- package/dist/lib/tailwind/colors.js.map +1 -0
- package/dist/lib/tailwind/index.d.ts +10 -0
- package/dist/lib/tailwind/index.d.ts.map +1 -0
- package/dist/lib/tailwind/index.js +9 -0
- package/dist/lib/tailwind/index.js.map +1 -0
- package/dist/lib/tailwind/resolvers.d.ts +28 -0
- package/dist/lib/tailwind/resolvers.d.ts.map +1 -0
- package/dist/lib/tailwind/resolvers.js +94 -0
- package/dist/lib/tailwind/resolvers.js.map +1 -0
- package/dist/lib/tailwind/types.d.ts +29 -0
- package/dist/lib/tailwind/types.d.ts.map +1 -0
- package/dist/lib/tailwind/types.js +8 -0
- package/dist/lib/tailwind/types.js.map +1 -0
- package/dist/lib/tailwind-config-loader.d.ts +8 -45
- package/dist/lib/tailwind-config-loader.d.ts.map +1 -1
- package/dist/lib/tailwind-config-loader.js +6 -429
- package/dist/lib/tailwind-config-loader.js.map +1 -1
- package/dist/lib/tailwind.d.ts +1 -1
- package/dist/lib/tailwind.d.ts.map +1 -1
- package/dist/lib/tailwind.js +1 -1
- package/dist/lib/tailwind.js.map +1 -1
- package/dist/lib/video-preview.d.ts.map +1 -1
- package/dist/lib/video-preview.js +28 -16
- package/dist/lib/video-preview.js.map +1 -1
- package/dist/lib/video-renderer.d.ts +19 -1
- package/dist/lib/video-renderer.d.ts.map +1 -1
- package/dist/lib/video-renderer.js +194 -70
- package/dist/lib/video-renderer.js.map +1 -1
- package/dist/sdk/edge/index.d.ts +91 -0
- package/dist/sdk/edge/index.d.ts.map +1 -0
- package/dist/sdk/edge/index.js +187 -0
- package/dist/sdk/edge/index.js.map +1 -0
- package/dist/sdk/workers/index.d.ts +135 -0
- package/dist/sdk/workers/index.d.ts.map +1 -0
- package/dist/sdk/workers/index.js +271 -0
- package/dist/sdk/workers/index.js.map +1 -0
- package/dist/sdk/workers/tailwind-config.d.ts +48 -0
- package/dist/sdk/workers/tailwind-config.d.ts.map +1 -0
- package/dist/sdk/workers/tailwind-config.js +187 -0
- package/dist/sdk/workers/tailwind-config.js.map +1 -0
- package/dist/sdk/workers/tailwind.d.ts +9 -0
- package/dist/sdk/workers/tailwind.d.ts.map +1 -0
- package/dist/sdk/workers/tailwind.js +8 -0
- package/dist/sdk/workers/tailwind.js.map +1 -0
- package/package.json +6 -2
- package/test-cloudflare-worker/README.md +64 -0
- package/test-cloudflare-worker/dist/README.md +1 -0
- package/test-cloudflare-worker/dist/index.js +23743 -0
- package/test-cloudflare-worker/dist/index.js.map +8 -0
- package/test-cloudflare-worker/package-lock.json +1773 -0
- package/test-cloudflare-worker/package.json +25 -0
- package/test-cloudflare-worker/test-sdk.mjs +75 -0
- package/test-cloudflare-worker/wrangler.toml +14 -0
- package/test-video-720p.mjs +96 -0
- package/test-video-breakdown.mjs +98 -0
- package/test-video-perf-1080.mjs +67 -0
- package/test-video-perf.mjs +56 -0
- package/test-worker-1080p.mjs +103 -0
- package/test-worker-viability.mjs +140 -0
- package/website/astro.config.mjs +4 -9
- package/website/dist/_astro/PlaygroundEditor.DzFavsm8.js +26 -0
- package/website/dist/_astro/VideoPreviewClient.BrajhYmh.js +1 -0
- package/website/dist/_astro/agents.CZXv4DCM.css +1 -0
- package/website/dist/_astro/client.BHSq4mdQ.js +33 -0
- package/website/dist/_astro/index.CTbGshLK.js +9 -0
- package/website/dist/_astro/jsx-runtime.BjG_zV1W.js +9 -0
- package/website/dist/_routes.json +1 -0
- package/website/dist/_worker.js/_@astrojs-ssr-adapter.mjs +4 -4
- package/website/dist/_worker.js/_astro-internal_middleware.mjs +2 -2
- package/website/dist/_worker.js/chunks/Logo_Cud5QvBJ.mjs +22 -0
- package/website/dist/_worker.js/chunks/_@astro-renderers_-YVK7NHa.mjs +15015 -0
- package/website/dist/_worker.js/chunks/astro/{server_Y5_QHO8v.mjs → server_CsUrSZgd.mjs} +113 -2
- package/website/dist/_worker.js/chunks/{astro-designed-error-pages_BNTLO-TA.mjs → astro-designed-error-pages_1ELXm5Tt.mjs} +1 -1
- package/website/dist/_worker.js/chunks/{index_C1UTDwYg.mjs → index_BDWR1Q-q.mjs} +2 -2
- package/website/dist/_worker.js/chunks/{noop-middleware_DlWGj5t5.mjs → noop-middleware_B8fH5jha.mjs} +1 -1
- package/website/dist/_worker.js/index.js +38 -30
- package/website/dist/_worker.js/manifest_Bk6136-u.mjs +98 -0
- package/website/dist/_worker.js/pages/_image.astro.mjs +1 -1
- package/website/dist/_worker.js/pages/api/playground/render.astro.mjs +25562 -0
- package/website/dist/_worker.js/pages/api/playground/templates.astro.mjs +92 -0
- package/website/dist/_worker.js/pages/api/raw-markdown/_---path_.astro.mjs +1 -1
- package/website/dist/_worker.js/pages/playground/_example_.astro.mjs +95 -0
- package/website/dist/_worker.js/pages/playground.astro.mjs +1 -0
- package/website/dist/_worker.js/renderers.mjs +1 -56
- package/website/dist/agents/index.html +4 -2
- package/website/dist/animation/index.html +629 -3
- package/website/dist/config/index.html +4 -2
- package/website/dist/fonts/index.html +4 -2
- package/website/dist/getting-started/index.html +4 -2
- package/website/dist/helpers/index.html +196 -10
- package/website/dist/images/index.html +4 -2
- package/website/dist/index.html +4 -3
- package/website/dist/llm.txt +870 -20
- package/website/dist/playground/index.html +6 -0
- package/website/dist/preview/index.html +4 -2
- package/website/dist/sdk/index.html +639 -127
- package/website/dist/sitemap.xml +12 -12
- package/website/dist/styling/index.html +4 -2
- package/website/dist/templates/index.html +4 -2
- package/website/dist/video/index.html +47 -12
- package/website/package-lock.json +11 -1
- package/website/package.json +3 -1
- package/website/wrangler.toml +9 -0
- package/_dsgn/templates/dashed-stroke-test/template.tsx +0 -73
- package/_dsgn/templates/path-follow-test/template.tsx +0 -176
- package/_dsgn/templates/path-simple-test/template.tsx +0 -98
- package/_dsgn/templates/stroke-dash-test/meta.json +0 -12
- package/_dsgn/templates/stroke-dash-test/template.tsx +0 -53
- package/website/dist/_astro/agents.Yx-L_igG.css +0 -1
- package/website/dist/_worker.js/manifest_CT_D-YDe.mjs +0 -98
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "test-cloudflare-worker",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy",
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"description": "",
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@cloudflare/workers-types": "^4.20251209.0",
|
|
17
|
+
"typescript": "^5.9.3",
|
|
18
|
+
"wrangler": "^4.53.0"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@resvg/resvg-wasm": "^2.6.2",
|
|
22
|
+
"react": "^19.2.1",
|
|
23
|
+
"satori": "^0.18.3"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple test script to verify the workers SDK works
|
|
3
|
+
* Run with: node test-sdk.mjs
|
|
4
|
+
*/
|
|
5
|
+
import { defineTemplate, renderSVG, renderImage, initWasm } from 'loopwind/sdk/workers';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
// Define a simple test template
|
|
9
|
+
const testTemplate = defineTemplate({
|
|
10
|
+
name: 'test-og-image',
|
|
11
|
+
size: { width: 1200, height: 630 },
|
|
12
|
+
render: ({ tw, title, subtitle }) => {
|
|
13
|
+
return React.createElement(
|
|
14
|
+
'div',
|
|
15
|
+
{
|
|
16
|
+
style: tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-blue-500 to-purple-600 p-12'),
|
|
17
|
+
},
|
|
18
|
+
React.createElement(
|
|
19
|
+
'h1',
|
|
20
|
+
{ style: tw('text-7xl font-bold text-white text-center') },
|
|
21
|
+
title
|
|
22
|
+
),
|
|
23
|
+
subtitle && React.createElement(
|
|
24
|
+
'p',
|
|
25
|
+
{ style: tw('text-3xl text-white mt-6') },
|
|
26
|
+
subtitle
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
async function main() {
|
|
33
|
+
console.log('Testing loopwind workers SDK...\n');
|
|
34
|
+
|
|
35
|
+
// Test 1: Template definition
|
|
36
|
+
console.log('1. Template defined successfully:', testTemplate.name);
|
|
37
|
+
console.log(' Size:', testTemplate.size);
|
|
38
|
+
|
|
39
|
+
// Test 2: SVG rendering (no WASM needed for satori in Node.js)
|
|
40
|
+
console.log('\n2. Testing SVG rendering...');
|
|
41
|
+
try {
|
|
42
|
+
const svg = await renderSVG(testTemplate, {
|
|
43
|
+
title: 'Hello World!',
|
|
44
|
+
subtitle: 'Testing the SDK'
|
|
45
|
+
});
|
|
46
|
+
console.log(' SVG rendered successfully!');
|
|
47
|
+
console.log(' SVG length:', svg.length, 'bytes');
|
|
48
|
+
console.log(' SVG starts with:', svg.substring(0, 100));
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error(' SVG rendering failed:', err.message);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Test 3: PNG rendering (requires WASM)
|
|
54
|
+
console.log('\n3. Testing PNG rendering...');
|
|
55
|
+
try {
|
|
56
|
+
await initWasm();
|
|
57
|
+
const png = await renderImage(testTemplate, {
|
|
58
|
+
title: 'Hello PNG!',
|
|
59
|
+
subtitle: 'WASM is working'
|
|
60
|
+
}, { format: 'png', scale: 1 });
|
|
61
|
+
console.log(' PNG rendered successfully!');
|
|
62
|
+
console.log(' PNG size:', png.length, 'bytes');
|
|
63
|
+
|
|
64
|
+
// Save PNG to file
|
|
65
|
+
const fs = await import('fs/promises');
|
|
66
|
+
await fs.writeFile('test-output.png', png);
|
|
67
|
+
console.log(' Saved to test-output.png');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(' PNG rendering failed:', err.message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('\nTests complete!');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name = "loopwind-worker-test"
|
|
2
|
+
main = "src/index.ts"
|
|
3
|
+
compatibility_date = "2024-12-01"
|
|
4
|
+
|
|
5
|
+
# Enable nodejs_compat for Node.js API support
|
|
6
|
+
# This is required for satori and resvg-wasm dependencies
|
|
7
|
+
compatibility_flags = ["nodejs_compat_v2"]
|
|
8
|
+
|
|
9
|
+
[observability]
|
|
10
|
+
enabled = true
|
|
11
|
+
|
|
12
|
+
# NOTE: Local development (wrangler dev) may fail due to WASM restrictions.
|
|
13
|
+
# This is a known limitation of the local workerd runtime.
|
|
14
|
+
# Deploy to production (wrangler deploy) for full functionality.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { renderSVG, initWasm, defineTemplate } from './dist/sdk/workers/index.js';
|
|
2
|
+
import { Resvg } from '@resvg/resvg-wasm';
|
|
3
|
+
import HME from 'h264-mp4-encoder';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Same complex template but at 720p
|
|
7
|
+
const template = defineTemplate({
|
|
8
|
+
name: 'perf-test-720',
|
|
9
|
+
type: 'video',
|
|
10
|
+
size: { width: 1280, height: 720 },
|
|
11
|
+
video: { fps: 30, duration: 5 },
|
|
12
|
+
render: ({ tw, progress, frame }) => React.createElement(
|
|
13
|
+
'div',
|
|
14
|
+
{ style: tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800') },
|
|
15
|
+
React.createElement('h1', {
|
|
16
|
+
style: tw('text-7xl font-black text-white ease-out-cubic enter-bounce-in-up/0/800')
|
|
17
|
+
}, 'LOOPWIND'),
|
|
18
|
+
React.createElement('p', {
|
|
19
|
+
style: tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/400/600')
|
|
20
|
+
}, 'Performance Test'),
|
|
21
|
+
React.createElement('div', { style: tw('flex gap-4 mt-8') },
|
|
22
|
+
React.createElement('div', {
|
|
23
|
+
style: tw('px-4 py-2 bg-white/10 rounded-full text-white ease-out enter-fade-in-up/900/400')
|
|
24
|
+
}, 'Box 1'),
|
|
25
|
+
React.createElement('div', {
|
|
26
|
+
style: tw('px-4 py-2 bg-white/10 rounded-full text-white ease-out enter-fade-in-up/1050/400')
|
|
27
|
+
}, 'Box 2'),
|
|
28
|
+
React.createElement('div', {
|
|
29
|
+
style: tw('px-4 py-2 bg-white/10 rounded-full text-white ease-out enter-fade-in-up/1200/400')
|
|
30
|
+
}, 'Box 3')
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function runTest() {
|
|
36
|
+
console.log('Initializing WASM...');
|
|
37
|
+
await initWasm();
|
|
38
|
+
|
|
39
|
+
const totalFrames = 150;
|
|
40
|
+
const width = 1280;
|
|
41
|
+
const height = 720;
|
|
42
|
+
|
|
43
|
+
// Phase 1: SVG generation
|
|
44
|
+
console.log('\n=== 720p Complex Template Test ===');
|
|
45
|
+
const svgStart = Date.now();
|
|
46
|
+
const svgs = [];
|
|
47
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
48
|
+
const progress = i / (totalFrames - 1);
|
|
49
|
+
const svg = await renderSVG(template, { progress, frame: i });
|
|
50
|
+
svgs.push(svg);
|
|
51
|
+
}
|
|
52
|
+
const svgTime = Date.now() - svgStart;
|
|
53
|
+
console.log('SVG generation: ' + svgTime + 'ms (' + (svgTime/totalFrames).toFixed(1) + 'ms/frame)');
|
|
54
|
+
|
|
55
|
+
// Phase 2: RGBA conversion (resvg)
|
|
56
|
+
const rgbaStart = Date.now();
|
|
57
|
+
const rgbaFrames = [];
|
|
58
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
59
|
+
const resvg = new Resvg(svgs[i], { fitTo: { mode: 'width', value: width } });
|
|
60
|
+
const rendered = resvg.render();
|
|
61
|
+
rgbaFrames.push(rendered.pixels);
|
|
62
|
+
}
|
|
63
|
+
const rgbaTime = Date.now() - rgbaStart;
|
|
64
|
+
console.log('RGBA conversion: ' + rgbaTime + 'ms (' + (rgbaTime/totalFrames).toFixed(1) + 'ms/frame)');
|
|
65
|
+
|
|
66
|
+
// Phase 3: H.264 encoding
|
|
67
|
+
const encodeStart = Date.now();
|
|
68
|
+
const encoder = await HME.createH264MP4Encoder();
|
|
69
|
+
encoder.width = width;
|
|
70
|
+
encoder.height = height;
|
|
71
|
+
encoder.frameRate = 30;
|
|
72
|
+
encoder.quantizationParameter = 23;
|
|
73
|
+
encoder.speed = 10;
|
|
74
|
+
encoder.groupOfPictures = 30;
|
|
75
|
+
encoder.initialize();
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < rgbaFrames.length; i++) {
|
|
78
|
+
encoder.addFrameRgba(rgbaFrames[i]);
|
|
79
|
+
}
|
|
80
|
+
encoder.finalize();
|
|
81
|
+
const output = encoder.FS.readFile(encoder.outputFilename);
|
|
82
|
+
encoder.delete();
|
|
83
|
+
const encodeTime = Date.now() - encodeStart;
|
|
84
|
+
console.log('H.264 encoding: ' + encodeTime + 'ms (' + (encodeTime/totalFrames).toFixed(1) + 'ms/frame)');
|
|
85
|
+
|
|
86
|
+
// Summary
|
|
87
|
+
const totalTime = svgTime + rgbaTime + encodeTime;
|
|
88
|
+
console.log('\n=== SUMMARY (720p) ===');
|
|
89
|
+
console.log('SVG generation: ' + svgTime + 'ms (' + ((svgTime/totalTime)*100).toFixed(0) + '%)');
|
|
90
|
+
console.log('RGBA conversion: ' + rgbaTime + 'ms (' + ((rgbaTime/totalTime)*100).toFixed(0) + '%)');
|
|
91
|
+
console.log('H.264 encoding: ' + encodeTime + 'ms (' + ((encodeTime/totalTime)*100).toFixed(0) + '%)');
|
|
92
|
+
console.log('TOTAL: ' + totalTime + 'ms');
|
|
93
|
+
console.log('Real-time ratio: ' + (totalTime/5000).toFixed(2) + 'x');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
runTest().catch(console.error);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { renderSVG, initWasm, defineTemplate } from './dist/sdk/workers/index.js';
|
|
2
|
+
import { Resvg } from '@resvg/resvg-wasm';
|
|
3
|
+
import HME from 'h264-mp4-encoder';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Same complex template
|
|
7
|
+
const template = defineTemplate({
|
|
8
|
+
name: 'perf-test-1080',
|
|
9
|
+
type: 'video',
|
|
10
|
+
size: { width: 1920, height: 1080 },
|
|
11
|
+
video: { fps: 30, duration: 5 },
|
|
12
|
+
render: ({ tw, progress, frame }) => React.createElement(
|
|
13
|
+
'div',
|
|
14
|
+
{ style: tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800') },
|
|
15
|
+
React.createElement('h1', {
|
|
16
|
+
style: tw('text-9xl font-black text-white ease-out-cubic enter-bounce-in-up/0/800')
|
|
17
|
+
}, 'LOOPWIND'),
|
|
18
|
+
React.createElement('p', {
|
|
19
|
+
style: tw('text-3xl text-white/80 mt-6 ease-out enter-fade-in-up/400/600')
|
|
20
|
+
}, 'Performance Test'),
|
|
21
|
+
React.createElement('div', { style: tw('flex gap-6 mt-12') },
|
|
22
|
+
React.createElement('div', {
|
|
23
|
+
style: tw('px-6 py-3 bg-white/10 rounded-full text-white text-lg ease-out enter-fade-in-up/900/400')
|
|
24
|
+
}, 'Box 1'),
|
|
25
|
+
React.createElement('div', {
|
|
26
|
+
style: tw('px-6 py-3 bg-white/10 rounded-full text-white text-lg ease-out enter-fade-in-up/1050/400')
|
|
27
|
+
}, 'Box 2'),
|
|
28
|
+
React.createElement('div', {
|
|
29
|
+
style: tw('px-6 py-3 bg-white/10 rounded-full text-white text-lg ease-out enter-fade-in-up/1200/400')
|
|
30
|
+
}, 'Box 3')
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function runTest() {
|
|
36
|
+
console.log('Initializing WASM...');
|
|
37
|
+
await initWasm();
|
|
38
|
+
|
|
39
|
+
const totalFrames = 150;
|
|
40
|
+
const width = 1920;
|
|
41
|
+
const height = 1080;
|
|
42
|
+
|
|
43
|
+
// Phase 1: SVG generation
|
|
44
|
+
console.log('\n=== Phase 1: SVG Generation ===');
|
|
45
|
+
const svgStart = Date.now();
|
|
46
|
+
const svgs = [];
|
|
47
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
48
|
+
const progress = i / (totalFrames - 1);
|
|
49
|
+
const svg = await renderSVG(template, { progress, frame: i });
|
|
50
|
+
svgs.push(svg);
|
|
51
|
+
}
|
|
52
|
+
const svgTime = Date.now() - svgStart;
|
|
53
|
+
console.log('SVG generation: ' + svgTime + 'ms (' + (svgTime/totalFrames).toFixed(1) + 'ms/frame)');
|
|
54
|
+
|
|
55
|
+
// Phase 2: RGBA conversion (resvg)
|
|
56
|
+
console.log('\n=== Phase 2: RGBA Conversion (resvg) ===');
|
|
57
|
+
const rgbaStart = Date.now();
|
|
58
|
+
const rgbaFrames = [];
|
|
59
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
60
|
+
const resvg = new Resvg(svgs[i], { fitTo: { mode: 'width', value: width } });
|
|
61
|
+
const rendered = resvg.render();
|
|
62
|
+
rgbaFrames.push(rendered.pixels);
|
|
63
|
+
}
|
|
64
|
+
const rgbaTime = Date.now() - rgbaStart;
|
|
65
|
+
console.log('RGBA conversion: ' + rgbaTime + 'ms (' + (rgbaTime/totalFrames).toFixed(1) + 'ms/frame)');
|
|
66
|
+
|
|
67
|
+
// Phase 3: H.264 encoding
|
|
68
|
+
console.log('\n=== Phase 3: H.264 Encoding ===');
|
|
69
|
+
const encodeStart = Date.now();
|
|
70
|
+
const encoder = await HME.createH264MP4Encoder();
|
|
71
|
+
encoder.width = width;
|
|
72
|
+
encoder.height = height;
|
|
73
|
+
encoder.frameRate = 30;
|
|
74
|
+
encoder.quantizationParameter = 23;
|
|
75
|
+
encoder.speed = 10;
|
|
76
|
+
encoder.groupOfPictures = 30;
|
|
77
|
+
encoder.initialize();
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < rgbaFrames.length; i++) {
|
|
80
|
+
encoder.addFrameRgba(rgbaFrames[i]);
|
|
81
|
+
}
|
|
82
|
+
encoder.finalize();
|
|
83
|
+
const output = encoder.FS.readFile(encoder.outputFilename);
|
|
84
|
+
encoder.delete();
|
|
85
|
+
const encodeTime = Date.now() - encodeStart;
|
|
86
|
+
console.log('H.264 encoding: ' + encodeTime + 'ms (' + (encodeTime/totalFrames).toFixed(1) + 'ms/frame)');
|
|
87
|
+
|
|
88
|
+
// Summary
|
|
89
|
+
const totalTime = svgTime + rgbaTime + encodeTime;
|
|
90
|
+
console.log('\n=== SUMMARY ===');
|
|
91
|
+
console.log('SVG generation: ' + svgTime + 'ms (' + ((svgTime/totalTime)*100).toFixed(0) + '%)');
|
|
92
|
+
console.log('RGBA conversion: ' + rgbaTime + 'ms (' + ((rgbaTime/totalTime)*100).toFixed(0) + '%)');
|
|
93
|
+
console.log('H.264 encoding: ' + encodeTime + 'ms (' + ((encodeTime/totalTime)*100).toFixed(0) + '%)');
|
|
94
|
+
console.log('TOTAL: ' + totalTime + 'ms');
|
|
95
|
+
console.log('Real-time ratio: ' + (totalTime/5000).toFixed(2) + 'x');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
runTest().catch(console.error);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { renderVideo, initWasm, defineTemplate } from './dist/sdk/workers/index.js';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
// More complex video template at 1080p
|
|
5
|
+
const template = defineTemplate({
|
|
6
|
+
name: 'perf-test-1080',
|
|
7
|
+
type: 'video',
|
|
8
|
+
size: { width: 1920, height: 1080 }, // 1080p
|
|
9
|
+
video: { fps: 30, duration: 5 },
|
|
10
|
+
render: ({ tw, progress, frame }) => React.createElement(
|
|
11
|
+
'div',
|
|
12
|
+
{ style: tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800') },
|
|
13
|
+
React.createElement('h1', {
|
|
14
|
+
style: tw('text-9xl font-black text-white ease-out-cubic enter-bounce-in-up/0/800')
|
|
15
|
+
}, 'LOOPWIND'),
|
|
16
|
+
React.createElement('p', {
|
|
17
|
+
style: tw('text-3xl text-white/80 mt-6 ease-out enter-fade-in-up/400/600')
|
|
18
|
+
}, 'Performance Test'),
|
|
19
|
+
React.createElement('div', { style: tw('flex gap-6 mt-12') },
|
|
20
|
+
React.createElement('div', {
|
|
21
|
+
style: tw('px-6 py-3 bg-white/10 rounded-full text-white text-lg ease-out enter-fade-in-up/900/400')
|
|
22
|
+
}, 'Box 1'),
|
|
23
|
+
React.createElement('div', {
|
|
24
|
+
style: tw('px-6 py-3 bg-white/10 rounded-full text-white text-lg ease-out enter-fade-in-up/1050/400')
|
|
25
|
+
}, 'Box 2'),
|
|
26
|
+
React.createElement('div', {
|
|
27
|
+
style: tw('px-6 py-3 bg-white/10 rounded-full text-white text-lg ease-out enter-fade-in-up/1200/400')
|
|
28
|
+
}, 'Box 3')
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
async function runTest() {
|
|
34
|
+
console.log('Initializing WASM...');
|
|
35
|
+
const wasmStart = Date.now();
|
|
36
|
+
await initWasm();
|
|
37
|
+
console.log('WASM init: ' + (Date.now() - wasmStart) + 'ms\n');
|
|
38
|
+
|
|
39
|
+
console.log('=== Video Render Performance Test (1080p, Complex) ===');
|
|
40
|
+
console.log('Resolution: 1920x1080 (1080p)');
|
|
41
|
+
console.log('Duration: 5 seconds');
|
|
42
|
+
console.log('FPS: 30');
|
|
43
|
+
console.log('Total frames: 150\n');
|
|
44
|
+
|
|
45
|
+
const start = Date.now();
|
|
46
|
+
|
|
47
|
+
const mp4 = await renderVideo(template, {}, {
|
|
48
|
+
onProgress: (frame, total) => {
|
|
49
|
+
if (frame % 30 === 0 || frame === total) {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const elapsed = now - start;
|
|
52
|
+
const fps = frame / (elapsed / 1000);
|
|
53
|
+
console.log('Frame ' + frame + '/' + total + ' - ' + elapsed + 'ms elapsed - ' + fps.toFixed(1) + ' frames/sec');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const totalTime = Date.now() - start;
|
|
59
|
+
console.log('\n=== Results ===');
|
|
60
|
+
console.log('Total time: ' + totalTime + 'ms (' + (totalTime/1000).toFixed(2) + 's)');
|
|
61
|
+
console.log('Frames: 150');
|
|
62
|
+
console.log('Avg per frame: ' + (totalTime/150).toFixed(1) + 'ms');
|
|
63
|
+
console.log('Real-time ratio: ' + (totalTime/5000).toFixed(2) + 'x');
|
|
64
|
+
console.log('Output size: ' + (mp4.length / 1024).toFixed(0) + 'KB');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
runTest().catch(console.error);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { renderVideo, initWasm, defineTemplate } from './dist/sdk/workers/index.js';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
// Simple video template
|
|
5
|
+
const template = defineTemplate({
|
|
6
|
+
name: 'perf-test',
|
|
7
|
+
type: 'video',
|
|
8
|
+
size: { width: 1280, height: 720 }, // 720p
|
|
9
|
+
video: { fps: 30, duration: 5 },
|
|
10
|
+
render: ({ tw, progress }) => React.createElement(
|
|
11
|
+
'div',
|
|
12
|
+
{ style: tw('flex items-center justify-center w-full h-full bg-blue-600') },
|
|
13
|
+
React.createElement('div', {
|
|
14
|
+
style: {
|
|
15
|
+
...tw('text-8xl font-bold text-white'),
|
|
16
|
+
opacity: progress
|
|
17
|
+
}
|
|
18
|
+
}, 'TEST')
|
|
19
|
+
)
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function runTest() {
|
|
23
|
+
console.log('Initializing WASM...');
|
|
24
|
+
const wasmStart = Date.now();
|
|
25
|
+
await initWasm();
|
|
26
|
+
console.log('WASM init: ' + (Date.now() - wasmStart) + 'ms\n');
|
|
27
|
+
|
|
28
|
+
console.log('=== Video Render Performance Test ===');
|
|
29
|
+
console.log('Resolution: 1280x720 (720p)');
|
|
30
|
+
console.log('Duration: 5 seconds');
|
|
31
|
+
console.log('FPS: 30');
|
|
32
|
+
console.log('Total frames: 150\n');
|
|
33
|
+
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
|
|
36
|
+
const mp4 = await renderVideo(template, {}, {
|
|
37
|
+
onProgress: (frame, total) => {
|
|
38
|
+
if (frame % 30 === 0 || frame === total) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const elapsed = now - start;
|
|
41
|
+
const fps = frame / (elapsed / 1000);
|
|
42
|
+
console.log('Frame ' + frame + '/' + total + ' - ' + elapsed + 'ms elapsed - ' + fps.toFixed(1) + ' frames/sec');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const totalTime = Date.now() - start;
|
|
48
|
+
console.log('\n=== Results ===');
|
|
49
|
+
console.log('Total time: ' + totalTime + 'ms (' + (totalTime/1000).toFixed(2) + 's)');
|
|
50
|
+
console.log('Frames: 150');
|
|
51
|
+
console.log('Avg per frame: ' + (totalTime/150).toFixed(1) + 'ms');
|
|
52
|
+
console.log('Real-time ratio: ' + (totalTime/5000).toFixed(2) + 'x');
|
|
53
|
+
console.log('Output size: ' + (mp4.length / 1024).toFixed(0) + 'KB');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
runTest().catch(console.error);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { renderSVG, initWasm, defineTemplate } from './dist/sdk/workers/index.js';
|
|
2
|
+
import { Resvg } from '@resvg/resvg-wasm';
|
|
3
|
+
import HME from 'h264-mp4-encoder';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Test 1080p limits
|
|
7
|
+
const template = defineTemplate({
|
|
8
|
+
name: 'worker-test-1080',
|
|
9
|
+
type: 'video',
|
|
10
|
+
size: { width: 1920, height: 1080 },
|
|
11
|
+
video: { fps: 30, duration: 10 },
|
|
12
|
+
render: ({ tw, progress, frame }) => React.createElement(
|
|
13
|
+
'div',
|
|
14
|
+
{ style: tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800') },
|
|
15
|
+
React.createElement('h1', {
|
|
16
|
+
style: tw('text-9xl font-black text-white ease-out-cubic enter-bounce-in-up/0/800')
|
|
17
|
+
}, 'LOOPWIND'),
|
|
18
|
+
React.createElement('p', {
|
|
19
|
+
style: tw('text-3xl text-white/80 mt-6 ease-out enter-fade-in-up/400/600')
|
|
20
|
+
}, 'Worker Test'),
|
|
21
|
+
React.createElement('div', { style: tw('flex gap-6 mt-12') },
|
|
22
|
+
React.createElement('div', {
|
|
23
|
+
style: tw('px-6 py-3 bg-white/10 rounded-full text-white text-lg ease-out enter-fade-in-up/900/400')
|
|
24
|
+
}, 'Feature 1'),
|
|
25
|
+
React.createElement('div', {
|
|
26
|
+
style: tw('px-6 py-3 bg-white/10 rounded-full text-white text-lg ease-out enter-fade-in-up/1050/400')
|
|
27
|
+
}, 'Feature 2'),
|
|
28
|
+
React.createElement('div', {
|
|
29
|
+
style: tw('px-6 py-3 bg-white/10 rounded-full text-white text-lg ease-out enter-fade-in-up/1200/400')
|
|
30
|
+
}, 'Feature 3')
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function runTests() {
|
|
36
|
+
console.log('=== 1080p Worker Limits Test ===\n');
|
|
37
|
+
await initWasm();
|
|
38
|
+
|
|
39
|
+
const configs = [
|
|
40
|
+
{ width: 1920, height: 1080, fps: 24, duration: 5, name: '1080p/24fps/5s' },
|
|
41
|
+
{ width: 1920, height: 1080, fps: 24, duration: 10, name: '1080p/24fps/10s' },
|
|
42
|
+
{ width: 1920, height: 1080, fps: 30, duration: 5, name: '1080p/30fps/5s' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const config of configs) {
|
|
46
|
+
const { width, height, fps, duration, name } = config;
|
|
47
|
+
const totalFrames = fps * duration;
|
|
48
|
+
|
|
49
|
+
console.log(`\nTesting ${name} (${totalFrames} frames)...`);
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
|
|
52
|
+
// SVG generation
|
|
53
|
+
const svgStart = Date.now();
|
|
54
|
+
const svgs = [];
|
|
55
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
56
|
+
const progress = i / (totalFrames - 1);
|
|
57
|
+
const svg = await renderSVG(template, { progress, frame: i });
|
|
58
|
+
svgs.push(svg);
|
|
59
|
+
}
|
|
60
|
+
const svgTime = Date.now() - svgStart;
|
|
61
|
+
|
|
62
|
+
// RGBA conversion
|
|
63
|
+
const rgbaStart = Date.now();
|
|
64
|
+
const rgbaFrames = [];
|
|
65
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
66
|
+
const resvg = new Resvg(svgs[i], { fitTo: { mode: 'width', value: width } });
|
|
67
|
+
const rendered = resvg.render();
|
|
68
|
+
rgbaFrames.push(rendered.pixels);
|
|
69
|
+
}
|
|
70
|
+
const rgbaTime = Date.now() - rgbaStart;
|
|
71
|
+
|
|
72
|
+
// H.264 encoding
|
|
73
|
+
const encodeStart = Date.now();
|
|
74
|
+
const encoder = await HME.createH264MP4Encoder();
|
|
75
|
+
encoder.width = width;
|
|
76
|
+
encoder.height = height;
|
|
77
|
+
encoder.frameRate = fps;
|
|
78
|
+
encoder.quantizationParameter = 23;
|
|
79
|
+
encoder.speed = 10;
|
|
80
|
+
encoder.groupOfPictures = fps;
|
|
81
|
+
encoder.initialize();
|
|
82
|
+
|
|
83
|
+
for (const frame of rgbaFrames) {
|
|
84
|
+
encoder.addFrameRgba(frame);
|
|
85
|
+
}
|
|
86
|
+
encoder.finalize();
|
|
87
|
+
const output = encoder.FS.readFile(encoder.outputFilename);
|
|
88
|
+
encoder.delete();
|
|
89
|
+
const encodeTime = Date.now() - encodeStart;
|
|
90
|
+
|
|
91
|
+
const totalTime = Date.now() - start;
|
|
92
|
+
const realTimeRatio = totalTime / (duration * 1000);
|
|
93
|
+
const workerViable = totalTime < 30000;
|
|
94
|
+
|
|
95
|
+
console.log(` Total: ${totalTime}ms (${realTimeRatio.toFixed(2)}x real-time)`);
|
|
96
|
+
console.log(` SVG: ${svgTime}ms, RGBA: ${rgbaTime}ms, H.264: ${encodeTime}ms`);
|
|
97
|
+
console.log(` RGBA/frame: ${(rgbaTime/totalFrames).toFixed(0)}ms`);
|
|
98
|
+
console.log(` Worker viable (<30s): ${workerViable ? 'YES ✓' : 'NO ✗'}`);
|
|
99
|
+
console.log(` Output size: ${(output.length / 1024).toFixed(0)}KB`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
runTests().catch(console.error);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { renderSVG, initWasm, defineTemplate } from './dist/sdk/workers/index.js';
|
|
2
|
+
import { Resvg } from '@resvg/resvg-wasm';
|
|
3
|
+
import HME from 'h264-mp4-encoder';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Typical real-world template
|
|
7
|
+
const template = defineTemplate({
|
|
8
|
+
name: 'worker-test',
|
|
9
|
+
type: 'video',
|
|
10
|
+
size: { width: 1280, height: 720 },
|
|
11
|
+
video: { fps: 30, duration: 10 }, // 10 second clip
|
|
12
|
+
render: ({ tw, progress, frame }) => React.createElement(
|
|
13
|
+
'div',
|
|
14
|
+
{ style: tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800') },
|
|
15
|
+
React.createElement('h1', {
|
|
16
|
+
style: tw('text-7xl font-black text-white ease-out-cubic enter-bounce-in-up/0/800')
|
|
17
|
+
}, 'LOOPWIND'),
|
|
18
|
+
React.createElement('p', {
|
|
19
|
+
style: tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/400/600')
|
|
20
|
+
}, 'Worker Viability Test'),
|
|
21
|
+
React.createElement('div', { style: tw('flex gap-4 mt-8') },
|
|
22
|
+
React.createElement('div', {
|
|
23
|
+
style: tw('px-4 py-2 bg-white/10 rounded-full text-white ease-out enter-fade-in-up/900/400')
|
|
24
|
+
}, 'Feature 1'),
|
|
25
|
+
React.createElement('div', {
|
|
26
|
+
style: tw('px-4 py-2 bg-white/10 rounded-full text-white ease-out enter-fade-in-up/1050/400')
|
|
27
|
+
}, 'Feature 2'),
|
|
28
|
+
React.createElement('div', {
|
|
29
|
+
style: tw('px-4 py-2 bg-white/10 rounded-full text-white ease-out enter-fade-in-up/1200/400')
|
|
30
|
+
}, 'Feature 3')
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function runTests() {
|
|
36
|
+
console.log('=== Worker Viability Tests ===\n');
|
|
37
|
+
console.log('Testing various configurations to find what works in Workers');
|
|
38
|
+
console.log('Cloudflare Workers limits: 30s CPU time (paid), 128MB memory\n');
|
|
39
|
+
|
|
40
|
+
await initWasm();
|
|
41
|
+
|
|
42
|
+
const configs = [
|
|
43
|
+
{ width: 640, height: 360, fps: 24, duration: 5, name: '360p/24fps/5s' },
|
|
44
|
+
{ width: 640, height: 360, fps: 24, duration: 10, name: '360p/24fps/10s' },
|
|
45
|
+
{ width: 854, height: 480, fps: 24, duration: 5, name: '480p/24fps/5s' },
|
|
46
|
+
{ width: 854, height: 480, fps: 24, duration: 10, name: '480p/24fps/10s' },
|
|
47
|
+
{ width: 1280, height: 720, fps: 24, duration: 5, name: '720p/24fps/5s' },
|
|
48
|
+
{ width: 1280, height: 720, fps: 24, duration: 10, name: '720p/24fps/10s' },
|
|
49
|
+
{ width: 1280, height: 720, fps: 30, duration: 5, name: '720p/30fps/5s' },
|
|
50
|
+
{ width: 1280, height: 720, fps: 30, duration: 10, name: '720p/30fps/10s' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const results = [];
|
|
54
|
+
|
|
55
|
+
for (const config of configs) {
|
|
56
|
+
const { width, height, fps, duration, name } = config;
|
|
57
|
+
const totalFrames = fps * duration;
|
|
58
|
+
|
|
59
|
+
console.log(`\nTesting ${name} (${totalFrames} frames)...`);
|
|
60
|
+
const start = Date.now();
|
|
61
|
+
|
|
62
|
+
// SVG generation
|
|
63
|
+
const svgStart = Date.now();
|
|
64
|
+
const svgs = [];
|
|
65
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
66
|
+
const progress = i / (totalFrames - 1);
|
|
67
|
+
const svg = await renderSVG(template, { progress, frame: i });
|
|
68
|
+
svgs.push(svg);
|
|
69
|
+
}
|
|
70
|
+
const svgTime = Date.now() - svgStart;
|
|
71
|
+
|
|
72
|
+
// RGBA conversion
|
|
73
|
+
const rgbaStart = Date.now();
|
|
74
|
+
const rgbaFrames = [];
|
|
75
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
76
|
+
const resvg = new Resvg(svgs[i], { fitTo: { mode: 'width', value: width } });
|
|
77
|
+
const rendered = resvg.render();
|
|
78
|
+
rgbaFrames.push(rendered.pixels);
|
|
79
|
+
}
|
|
80
|
+
const rgbaTime = Date.now() - rgbaStart;
|
|
81
|
+
|
|
82
|
+
// H.264 encoding
|
|
83
|
+
const encodeStart = Date.now();
|
|
84
|
+
const encoder = await HME.createH264MP4Encoder();
|
|
85
|
+
encoder.width = width;
|
|
86
|
+
encoder.height = height;
|
|
87
|
+
encoder.frameRate = fps;
|
|
88
|
+
encoder.quantizationParameter = 23;
|
|
89
|
+
encoder.speed = 10;
|
|
90
|
+
encoder.groupOfPictures = fps;
|
|
91
|
+
encoder.initialize();
|
|
92
|
+
|
|
93
|
+
for (const frame of rgbaFrames) {
|
|
94
|
+
encoder.addFrameRgba(frame);
|
|
95
|
+
}
|
|
96
|
+
encoder.finalize();
|
|
97
|
+
const output = encoder.FS.readFile(encoder.outputFilename);
|
|
98
|
+
encoder.delete();
|
|
99
|
+
const encodeTime = Date.now() - encodeStart;
|
|
100
|
+
|
|
101
|
+
const totalTime = Date.now() - start;
|
|
102
|
+
const realTimeRatio = totalTime / (duration * 1000);
|
|
103
|
+
const workerViable = totalTime < 30000;
|
|
104
|
+
|
|
105
|
+
results.push({
|
|
106
|
+
name,
|
|
107
|
+
totalTime,
|
|
108
|
+
svgTime,
|
|
109
|
+
rgbaTime,
|
|
110
|
+
encodeTime,
|
|
111
|
+
realTimeRatio,
|
|
112
|
+
workerViable,
|
|
113
|
+
outputSize: output.length
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(` Total: ${totalTime}ms (${realTimeRatio.toFixed(2)}x real-time)`);
|
|
117
|
+
console.log(` SVG: ${svgTime}ms, RGBA: ${rgbaTime}ms, H.264: ${encodeTime}ms`);
|
|
118
|
+
console.log(` Worker viable: ${workerViable ? 'YES ✓' : 'NO ✗'}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('\n\n=== SUMMARY ===\n');
|
|
122
|
+
console.log('Config | Total | RGBA/frame | Ratio | Worker OK');
|
|
123
|
+
console.log('--------------------|----------|------------|--------|----------');
|
|
124
|
+
for (const r of results) {
|
|
125
|
+
const frames = parseInt(r.name.split('/')[1]) * parseInt(r.name.split('/')[2].replace('s', ''));
|
|
126
|
+
const rgbaPerFrame = (r.rgbaTime / frames).toFixed(0);
|
|
127
|
+
console.log(
|
|
128
|
+
`${r.name.padEnd(19)} | ${(r.totalTime + 'ms').padEnd(8)} | ${(rgbaPerFrame + 'ms').padEnd(10)} | ${r.realTimeRatio.toFixed(2)}x | ${r.workerViable ? 'YES ✓' : 'NO ✗'}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log('\n=== CONCLUSIONS ===');
|
|
133
|
+
const viableConfigs = results.filter(r => r.workerViable);
|
|
134
|
+
if (viableConfigs.length > 0) {
|
|
135
|
+
console.log('Worker-viable configurations (< 30s):');
|
|
136
|
+
viableConfigs.forEach(r => console.log(` - ${r.name}: ${r.totalTime}ms`));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
runTests().catch(console.error);
|