@effect-motion/renderer 0.5.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/dist/Builtins.d.ts +34 -0
- package/dist/Builtins.js +584 -0
- package/dist/EntityRenderer.d.ts +188 -0
- package/dist/EntityRenderer.js +1 -0
- package/dist/Images.d.ts +68 -0
- package/dist/Images.js +116 -0
- package/dist/RenderException.d.ts +29 -0
- package/dist/RenderException.js +25 -0
- package/dist/Renderer.d.ts +174 -0
- package/dist/Renderer.js +176 -0
- package/dist/Sync.d.ts +181 -0
- package/dist/Sync.js +480 -0
- package/dist/Text.d.ts +132 -0
- package/dist/Text.js +318 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +58 -0
- package/dist/node.d.ts +158 -0
- package/dist/node.js +206 -0
- package/package.json +65 -0
package/dist/node.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { deflateSync } from "node:zlib";
|
|
2
|
+
import { Renderer as Gpu, PostProcessing, RenderTarget, Scene as ThreeScene, } from "@effect-motion/three";
|
|
3
|
+
// side effects first-class: installs navigator.gpu (Dawn), WebGPU globals,
|
|
4
|
+
// and the rAF/`self` shims three needs — before any renderer is created
|
|
5
|
+
import * as NodeGpu from "@effect-motion/three/node";
|
|
6
|
+
import { Effect, Scope } from "effect";
|
|
7
|
+
import { builtinRegistry } from "./Builtins.js";
|
|
8
|
+
import { renderCompTargets } from "./Renderer.js";
|
|
9
|
+
import * as Sync from "./Sync.js";
|
|
10
|
+
// ── minimal RGBA → PNG encoder (filter 0 + zlib), lifted from the ThorVG
|
|
11
|
+
// package's node PNG path; node:zlib does the compression ────────────────
|
|
12
|
+
const SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
13
|
+
const crcTable = (() => {
|
|
14
|
+
const t = new Uint32Array(256);
|
|
15
|
+
for (let n = 0; n < 256; n++) {
|
|
16
|
+
let c = n;
|
|
17
|
+
for (let k = 0; k < 8; k++) {
|
|
18
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
19
|
+
}
|
|
20
|
+
t[n] = c >>> 0;
|
|
21
|
+
}
|
|
22
|
+
return t;
|
|
23
|
+
})();
|
|
24
|
+
const crc32 = (bytes) => {
|
|
25
|
+
let c = 0xffffffff;
|
|
26
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
27
|
+
c = (crcTable[(c ^ (bytes[i] ?? 0)) & 0xff] ?? 0) ^ (c >>> 8);
|
|
28
|
+
}
|
|
29
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
30
|
+
};
|
|
31
|
+
const chunk = (type, data) => {
|
|
32
|
+
const typeBytes = Uint8Array.from(type, (ch) => ch.charCodeAt(0));
|
|
33
|
+
const body = new Uint8Array(typeBytes.length + data.length);
|
|
34
|
+
body.set(typeBytes, 0);
|
|
35
|
+
body.set(data, typeBytes.length);
|
|
36
|
+
const out = new Uint8Array(4 + body.length + 4);
|
|
37
|
+
const view = new DataView(out.buffer);
|
|
38
|
+
view.setUint32(0, data.length, false);
|
|
39
|
+
out.set(body, 4);
|
|
40
|
+
view.setUint32(4 + body.length, crc32(body), false);
|
|
41
|
+
return out;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Encode a raw RGBA buffer as a PNG.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* {@link renderToPng} already does this, so reach for it directly only when
|
|
48
|
+
* you have pixels from somewhere else. The encoder is deliberately minimal —
|
|
49
|
+
* no filtering, just zlib — which keeps it fast at the cost of somewhat
|
|
50
|
+
* larger files than an optimizing encoder would produce.
|
|
51
|
+
*
|
|
52
|
+
* @param rgba - Exactly `width * height * 4` bytes, 8 bits per channel.
|
|
53
|
+
* @param width - Image width in pixels.
|
|
54
|
+
* @param height - Image height in pixels.
|
|
55
|
+
* @returns The PNG file bytes.
|
|
56
|
+
* @throws If `rgba` is not exactly `width * height * 4` bytes.
|
|
57
|
+
*/
|
|
58
|
+
export const encodePng = (rgba, width, height) => {
|
|
59
|
+
if (rgba.length !== width * height * 4) {
|
|
60
|
+
throw new Error(`encodePng: buffer is ${rgba.length} bytes, expected ${width * height * 4} (${width}x${height} RGBA)`);
|
|
61
|
+
}
|
|
62
|
+
const stride = width * 4;
|
|
63
|
+
const raw = new Uint8Array((stride + 1) * height);
|
|
64
|
+
for (let y = 0; y < height; y++) {
|
|
65
|
+
raw[y * (stride + 1)] = 0;
|
|
66
|
+
raw.set(rgba.subarray(y * stride, y * stride + stride), y * (stride + 1) + 1);
|
|
67
|
+
}
|
|
68
|
+
const ihdr = new Uint8Array(13);
|
|
69
|
+
const ihdrView = new DataView(ihdr.buffer);
|
|
70
|
+
ihdrView.setUint32(0, width, false);
|
|
71
|
+
ihdrView.setUint32(4, height, false);
|
|
72
|
+
ihdr[8] = 8; // bit depth
|
|
73
|
+
ihdr[9] = 6; // color type: RGBA
|
|
74
|
+
const idat = deflateSync(raw);
|
|
75
|
+
const out = new Uint8Array(SIGNATURE.length + (12 + ihdr.length) + (12 + idat.length) + 12);
|
|
76
|
+
let offset = 0;
|
|
77
|
+
for (const part of [
|
|
78
|
+
SIGNATURE,
|
|
79
|
+
chunk("IHDR", ihdr),
|
|
80
|
+
chunk("IDAT", new Uint8Array(idat.buffer, idat.byteOffset, idat.length)),
|
|
81
|
+
chunk("IEND", new Uint8Array(0)),
|
|
82
|
+
]) {
|
|
83
|
+
out.set(part, offset);
|
|
84
|
+
offset += part.length;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Render one frame and return it as PNG bytes.
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* The whole export path in one call: resolve the frame's fonts and images,
|
|
93
|
+
* sync the retained scene, wait for glyph layouts and decodes, render, read
|
|
94
|
+
* the pixels back off the GPU, and encode.
|
|
95
|
+
*
|
|
96
|
+
* Call it once per frame on the SAME renderer; state is retained between
|
|
97
|
+
* calls, so consecutive frames only pay for what changed.
|
|
98
|
+
*
|
|
99
|
+
* The frame's dimensions must match the renderer's. A mismatch is a defect
|
|
100
|
+
* naming both sizes, not a silent rescale.
|
|
101
|
+
*
|
|
102
|
+
* Output is `pixelWidth × pixelHeight` — larger than the logical size when
|
|
103
|
+
* a `pixelRatio` was given.
|
|
104
|
+
*
|
|
105
|
+
* @param renderer - A renderer from {@link make}.
|
|
106
|
+
* @param frame - The frame to draw.
|
|
107
|
+
* @returns PNG file bytes.
|
|
108
|
+
*/
|
|
109
|
+
export const renderToPng = Effect.fnUntraced(function* (renderer, frame) {
|
|
110
|
+
if (frame.width !== renderer.width || frame.height !== renderer.height) {
|
|
111
|
+
// deliberate defect: a mis-sized frame is a caller bug, not a
|
|
112
|
+
// recoverable condition
|
|
113
|
+
return yield* Effect.die(new Error(`NodeRenderer: frame is ${frame.width}x${frame.height}, renderer was made for ${renderer.width}x${renderer.height}`));
|
|
114
|
+
}
|
|
115
|
+
yield* Sync.resolveResources(renderer.sync, frame).pipe(Effect.provideService(Scope.Scope, renderer.scope));
|
|
116
|
+
yield* Sync.syncFrame(renderer.sync, frame);
|
|
117
|
+
// glyph layouts registered during sync must land before readback —
|
|
118
|
+
// an export frame never ships half-built text
|
|
119
|
+
yield* Sync.whenReady(renderer.sync);
|
|
120
|
+
// three advances nodeFrame.frameId only inside its rAF-driven
|
|
121
|
+
// animation loop (a 16ms setTimeout shim headless). Back-to-back
|
|
122
|
+
// exports outrun it, so FRAME-deduped nodes — the scene PassNode
|
|
123
|
+
// above all — skip their per-frame work and consecutive frames
|
|
124
|
+
// read a stale pass texture (pairwise-duplicated video frames).
|
|
125
|
+
// Drive it explicitly: one exported frame IS one three frame.
|
|
126
|
+
// sync call in a generator: a plain statement, no Effect.sync ceremony
|
|
127
|
+
// (that only buys an allocation and a fiber step for an infallible
|
|
128
|
+
// field write). Effect.sync is for the combinators that take one —
|
|
129
|
+
// ensuring, addFinalizer.
|
|
130
|
+
Gpu.advanceFrame(renderer.gpu);
|
|
131
|
+
yield* renderCompTargets(renderer.gpu, renderer.sync, renderer.pixelRatio);
|
|
132
|
+
const pipeline = ThreeScene.isEmpty(renderer.sync.hudScene)
|
|
133
|
+
? renderer.post
|
|
134
|
+
: renderer.postWithHud;
|
|
135
|
+
yield* PostProcessing.render(pipeline);
|
|
136
|
+
const rgba = yield* Gpu.readRenderTarget(renderer.gpu, renderer.target, renderer.pixelWidth, renderer.pixelHeight);
|
|
137
|
+
return encodePng(rgba, renderer.pixelWidth, renderer.pixelHeight);
|
|
138
|
+
});
|
|
139
|
+
/**
|
|
140
|
+
* Acquire a headless renderer.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* Scoped: the GPU device, render targets, and every retained object are
|
|
144
|
+
* released when the scope closes. Acquire one per export and reuse it for
|
|
145
|
+
* every frame — startup involves creating a GPU device, so per-frame
|
|
146
|
+
* acquisition is dramatically slower.
|
|
147
|
+
*
|
|
148
|
+
* `width` and `height` fix the renderer's size for its lifetime and must
|
|
149
|
+
* match the frames you render.
|
|
150
|
+
*
|
|
151
|
+
* Use `pixelRatio` to supersample: a ratio of 2 renders at twice the linear
|
|
152
|
+
* resolution (four times the pixels), which is the usual way to get cleaner
|
|
153
|
+
* edges in an export.
|
|
154
|
+
*
|
|
155
|
+
* Depth of field is not applied — every frame renders sharp.
|
|
156
|
+
*
|
|
157
|
+
* @param options - Dimensions, supersampling, and any custom entity
|
|
158
|
+
* renderers.
|
|
159
|
+
* @returns A renderer, valid for the current scope.
|
|
160
|
+
*/
|
|
161
|
+
export const make = Effect.fn("NodeRenderer.make")(function* (options) {
|
|
162
|
+
const dpr = options.pixelRatio ?? 1;
|
|
163
|
+
const pixelWidth = Math.round(options.width * dpr);
|
|
164
|
+
const pixelHeight = Math.round(options.height * dpr);
|
|
165
|
+
const registry = {
|
|
166
|
+
...builtinRegistry,
|
|
167
|
+
...options.renderers,
|
|
168
|
+
};
|
|
169
|
+
const sync = Sync.make(registry);
|
|
170
|
+
const device = yield* NodeGpu.makeDevice();
|
|
171
|
+
const { canvas, context } = NodeGpu.stubCanvas(pixelWidth, pixelHeight);
|
|
172
|
+
const gpu = yield* Gpu.make({
|
|
173
|
+
canvas: canvas,
|
|
174
|
+
context: context,
|
|
175
|
+
antialias: true,
|
|
176
|
+
device,
|
|
177
|
+
width: options.width,
|
|
178
|
+
height: options.height,
|
|
179
|
+
pixelRatio: dpr,
|
|
180
|
+
});
|
|
181
|
+
yield* Effect.addFinalizer(() => Sync.dispose(sync));
|
|
182
|
+
const scenePass = PostProcessing.pass(sync.scene, sync.camera);
|
|
183
|
+
// ponytail: no depth of field — the pipeline draws the scene pass
|
|
184
|
+
// straight through.
|
|
185
|
+
const sceneColor = scenePass.getTextureNode();
|
|
186
|
+
const post = PostProcessing.makePipeline(gpu, sceneColor);
|
|
187
|
+
const hudScenePass = PostProcessing.pass(sync.hudScene, sync.hudCamera);
|
|
188
|
+
const hudTex = hudScenePass.getTextureNode();
|
|
189
|
+
const postWithHud = PostProcessing.makePipeline(gpu, sceneColor.mul(hudTex.a.oneMinus()).add(hudTex.rgb.mul(hudTex.a)));
|
|
190
|
+
const target = yield* RenderTarget.make(pixelWidth, pixelHeight);
|
|
191
|
+
Gpu.setRenderTarget(gpu, target);
|
|
192
|
+
const scope = yield* Effect.scope;
|
|
193
|
+
return {
|
|
194
|
+
sync,
|
|
195
|
+
gpu,
|
|
196
|
+
scope,
|
|
197
|
+
post,
|
|
198
|
+
postWithHud,
|
|
199
|
+
target,
|
|
200
|
+
width: options.width,
|
|
201
|
+
height: options.height,
|
|
202
|
+
pixelWidth,
|
|
203
|
+
pixelHeight,
|
|
204
|
+
pixelRatio: dpr,
|
|
205
|
+
};
|
|
206
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effect-motion/renderer",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Retained three.js frame renderer for effect-motion",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/julia-script/effect-motion.git",
|
|
10
|
+
"directory": "packages/renderer"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/julia-script/effect-motion#readme",
|
|
13
|
+
"bugs": "https://github.com/julia-script/effect-motion/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"effect",
|
|
16
|
+
"motion",
|
|
17
|
+
"renderer",
|
|
18
|
+
"three",
|
|
19
|
+
"webgpu",
|
|
20
|
+
"motion-graphics"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./*": {
|
|
28
|
+
"types": "./dist/*.d.ts",
|
|
29
|
+
"default": "./dist/*.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@effect/platform-node": "4.0.0-beta.98",
|
|
40
|
+
"@types/three": "^0.185.1",
|
|
41
|
+
"jpeg-js": "^0.4.4",
|
|
42
|
+
"pngjs": "^7.0.0",
|
|
43
|
+
"three": "^0.185.1",
|
|
44
|
+
"troika-three-text": "^0.52.4",
|
|
45
|
+
"webgl-sdf-generator": "^1.1.1",
|
|
46
|
+
"@effect-motion/three": "^0.5.0",
|
|
47
|
+
"effect-motion": "^0.5.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"effect": ">=4.0.0-beta.98"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^26.1.1",
|
|
54
|
+
"@types/pngjs": "^6.0.5",
|
|
55
|
+
"effect": "4.0.0-beta.98",
|
|
56
|
+
"typescript": "^7.0.2",
|
|
57
|
+
"vitest": "^4.1.10"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsc -p tsconfig.build.json",
|
|
61
|
+
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
62
|
+
"test": "vitest run --passWithNoTests",
|
|
63
|
+
"check": "tsc --noEmit"
|
|
64
|
+
}
|
|
65
|
+
}
|