@effect-motion/export 0.2.0 → 0.3.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/Video.d.ts +15 -10
- package/dist/Video.js +41 -21
- package/dist/index.d.ts +4 -7
- package/dist/index.js +3 -6
- package/package.json +8 -8
- package/dist/Fonts.d.ts +0 -12
- package/dist/Fonts.js +0 -12
- package/dist/Resvg.d.ts +0 -27
- package/dist/Resvg.js +0 -22
package/dist/Video.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
+
import { type ThorvgException } from "@effect-motion/thorvg";
|
|
1
2
|
import * as Effect from "effect/Effect";
|
|
2
3
|
import type * as Scope from "effect/Scope";
|
|
3
4
|
import type { ChildProcessSpawner } from "effect/unstable/process";
|
|
4
|
-
import {
|
|
5
|
-
import * as Ffmpeg from "./Ffmpeg";
|
|
6
|
-
import * as Resvg from "./Resvg";
|
|
5
|
+
import { Scene } from "effect-motion";
|
|
6
|
+
import * as Ffmpeg from "./Ffmpeg.js";
|
|
7
7
|
type SceneRunner = typeof Scene.tick extends Effect.Effect<unknown, unknown, infer RR> ? RR : never;
|
|
8
8
|
type SceneInternalR = SceneRunner | Scope.Scope;
|
|
9
9
|
/**
|
|
10
10
|
* The one-call export path: a scene becomes a video file. Composes the whole
|
|
11
|
-
* pipeline — `Scene.stream` →
|
|
12
|
-
* framerate and dimensions from the scene's own frame metadata
|
|
13
|
-
*
|
|
11
|
+
* pipeline — `Scene.stream` → ThorVG PNG renderer → ffmpeg — reading the
|
|
12
|
+
* framerate and dimensions from the scene's own frame metadata. The ThorVG
|
|
13
|
+
* engine is acquired internally (Node SW layer), so callers wire nothing.
|
|
14
14
|
*/
|
|
15
15
|
/**
|
|
16
16
|
* Scene settings honored by the export path — a subset of the runner's
|
|
@@ -39,8 +39,12 @@ export interface VideoOptions {
|
|
|
39
39
|
readonly binary?: string;
|
|
40
40
|
/** Extra ffmpeg arguments, appended before the output path. */
|
|
41
41
|
readonly extraArgs?: ReadonlyArray<string>;
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Supersampling factor: frames are rasterized at scene dimensions × dpr
|
|
44
|
+
* (the video's pixel size) while the scene keeps its authored logical
|
|
45
|
+
* coordinates and framing. Defaults to 1.
|
|
46
|
+
*/
|
|
47
|
+
readonly dpr?: number;
|
|
44
48
|
/** Scene framerate/dimensions/maxFrames for this export. */
|
|
45
49
|
readonly settings?: VideoSceneSettings;
|
|
46
50
|
}
|
|
@@ -48,7 +52,8 @@ export interface VideoOptions {
|
|
|
48
52
|
* Render a scene to a video file at `outPath`.
|
|
49
53
|
*
|
|
50
54
|
* Fails with {@link Ffmpeg.EncodeError} on odd output dimensions (invalid for
|
|
51
|
-
* `yuv420p`) — before any ffmpeg process is spawned — or on an ffmpeg failure
|
|
55
|
+
* `yuv420p`) — before any ffmpeg process is spawned — or on an ffmpeg failure;
|
|
56
|
+
* a ThorVG render failure surfaces as `ThorvgException`.
|
|
52
57
|
*/
|
|
53
|
-
export declare const render: <E
|
|
58
|
+
export declare const render: <E = never>(scene: Scene.Scene<E, SceneInternalR>, outPath: string, options?: VideoOptions) => Effect.Effect<void, E | Ffmpeg.EncodeError | ThorvgException, ChildProcessSpawner.ChildProcessSpawner>;
|
|
54
59
|
export {};
|
package/dist/Video.js
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Font, Session } from "@effect-motion/thorvg";
|
|
2
|
+
import { EngineNode } from "@effect-motion/thorvg/node";
|
|
2
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
3
5
|
import * as Sink from "effect/Sink";
|
|
4
6
|
import * as Stream from "effect/Stream";
|
|
5
|
-
import {
|
|
6
|
-
import * as
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
7
|
+
import { Fonts, Renderer, Scene } from "effect-motion";
|
|
8
|
+
import * as PngExporter from "effect-motion/PngExporter";
|
|
9
|
+
import * as Ffmpeg from "./Ffmpeg.js";
|
|
10
|
+
// The ThorVG engine (global font registry) plus a render session (the canvas
|
|
11
|
+
// Renderer.render draws each frame onto). The session canvas is resized per
|
|
12
|
+
// frame, so the seed size only has to be valid — the settings dimensions when
|
|
13
|
+
// given, else a 1×1 placeholder the first frame's resize corrects.
|
|
14
|
+
const fontedLayer = (scene, settings) => {
|
|
15
|
+
const fonts = {
|
|
16
|
+
"sans-serif": Font.DEFAULT_FONT_URL,
|
|
17
|
+
...Fonts.urlMap(scene),
|
|
18
|
+
};
|
|
19
|
+
return Layer.provideMerge(Session.layer({
|
|
20
|
+
width: settings?.width ?? 1,
|
|
21
|
+
height: settings?.height ?? 1,
|
|
22
|
+
fonts,
|
|
23
|
+
}), EngineNode.layer("sw", fonts));
|
|
24
|
+
};
|
|
12
25
|
/**
|
|
13
26
|
* Render a scene to a video file at `outPath`.
|
|
14
27
|
*
|
|
15
28
|
* Fails with {@link Ffmpeg.EncodeError} on odd output dimensions (invalid for
|
|
16
|
-
* `yuv420p`) — before any ffmpeg process is spawned — or on an ffmpeg failure
|
|
29
|
+
* `yuv420p`) — before any ffmpeg process is spawned — or on an ffmpeg failure;
|
|
30
|
+
* a ThorVG render failure surfaces as `ThorvgException`.
|
|
17
31
|
*/
|
|
18
32
|
export const render = (scene, outPath, options = {}) => Effect.scoped(Effect.gen(function* () {
|
|
19
|
-
const renderer = yield* Svg.SvgRenderer.Context;
|
|
20
|
-
const fontOpts = resvgOptions(scene);
|
|
21
|
-
const concurrency = options.concurrency ?? 4;
|
|
22
33
|
let frames = Scene.stream(scene, options.settings ?? {});
|
|
23
34
|
if (options.frames !== undefined) {
|
|
24
35
|
frames = Stream.take(frames, options.frames);
|
|
@@ -30,24 +41,33 @@ export const render = (scene, outPath, options = {}) => Effect.scoped(Effect.gen
|
|
|
30
41
|
return; // empty scene: nothing to encode
|
|
31
42
|
}
|
|
32
43
|
const meta = first.value;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
// the encoded pixel size is the logical scene size × dpr; that is
|
|
45
|
+
// what yuv420p needs even, not the logical size
|
|
46
|
+
const dpr = options.dpr ?? 1;
|
|
47
|
+
const outWidth = Math.round(meta.width * dpr);
|
|
48
|
+
const outHeight = Math.round(meta.height * dpr);
|
|
49
|
+
if (outWidth % 2 !== 0 || outHeight % 2 !== 0) {
|
|
50
|
+
const odd = outWidth % 2 !== 0 ? `width ${outWidth}` : `height ${outHeight}`;
|
|
37
51
|
return yield* new Ffmpeg.EncodeError({
|
|
38
52
|
message: `Video dimensions must be even for yuv420p, got ${odd}. ` +
|
|
39
|
-
`Use even scene dimensions, or pass extraArgs with a scale filter.`,
|
|
53
|
+
`Use even scene dimensions (× dpr), or pass extraArgs with a scale filter.`,
|
|
40
54
|
stderr: "",
|
|
41
55
|
cause: meta,
|
|
42
56
|
});
|
|
43
57
|
}
|
|
44
58
|
const allFrames = Stream.concat(Stream.make(meta), rest);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
// each frame is rasterized to a framebuffer by the ThorVG renderer
|
|
60
|
+
// then PNG-encoded. Rasterization is serial: Renderer.render mutates
|
|
61
|
+
// one shared RenderSession canvas per frame (resize/clear/draw/read),
|
|
62
|
+
// so concurrent renders would clobber each other's pixels.
|
|
63
|
+
const pngStream = Stream.mapEffect(allFrames, (frame) => Renderer.render(frame, { dpr }).pipe(Effect.flatMap(PngExporter.toBuffer)));
|
|
48
64
|
yield* Ffmpeg.encode(pngStream, outPath, {
|
|
49
65
|
frameRate: meta.frameRate,
|
|
50
66
|
binary: options.binary,
|
|
51
67
|
extraArgs: options.extraArgs,
|
|
52
68
|
});
|
|
53
|
-
})).pipe(
|
|
69
|
+
})).pipe(
|
|
70
|
+
// the scene's declared url fonts, merged over the default sans, so text
|
|
71
|
+
// in a declared family renders (design D3); path-only entries skipped.
|
|
72
|
+
// Fonts go to both the engine (global registry) and the render session.
|
|
73
|
+
Effect.provide(fontedLayer(scene, options.settings)));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
export * as Ffmpeg from "./Ffmpeg";
|
|
2
|
-
export { EncodeError, type EncodeOptions } from "./Ffmpeg";
|
|
3
|
-
export
|
|
4
|
-
export * as
|
|
5
|
-
export { RasterizeError, type ResvgRenderOptions } from "./Resvg";
|
|
6
|
-
export type { VideoOptions } from "./Video";
|
|
7
|
-
export * as Video from "./Video";
|
|
1
|
+
export * as Ffmpeg from "./Ffmpeg.js";
|
|
2
|
+
export { EncodeError, type EncodeOptions } from "./Ffmpeg.js";
|
|
3
|
+
export type { VideoOptions } from "./Video.js";
|
|
4
|
+
export * as Video from "./Video.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
export * as Ffmpeg from "./Ffmpeg";
|
|
2
|
-
export { EncodeError } from "./Ffmpeg";
|
|
3
|
-
export * as
|
|
4
|
-
export * as Resvg from "./Resvg";
|
|
5
|
-
export { RasterizeError } from "./Resvg";
|
|
6
|
-
export * as Video from "./Video";
|
|
1
|
+
export * as Ffmpeg from "./Ffmpeg.js";
|
|
2
|
+
export { EncodeError } from "./Ffmpeg.js";
|
|
3
|
+
export * as Video from "./Video.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-motion/export",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Node-only export tools for effect-motion:
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Node-only export tools for effect-motion: render scenes to PNG frames (ThorVG) and encode to video (ffmpeg)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"effect",
|
|
16
16
|
"motion",
|
|
17
17
|
"export",
|
|
18
|
-
"
|
|
18
|
+
"thorvg",
|
|
19
19
|
"png",
|
|
20
20
|
"ffmpeg",
|
|
21
21
|
"video"
|
|
@@ -33,17 +33,17 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@resvg/resvg-js": "^2.6.2",
|
|
37
36
|
"ffmpeg-static": "^5.3.0",
|
|
38
|
-
"effect-motion": "^0.
|
|
37
|
+
"@effect-motion/thorvg": "^0.1.0",
|
|
38
|
+
"effect-motion": "^0.3.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"effect": ">=4.0.0-beta.
|
|
41
|
+
"effect": ">=4.0.0-beta.98"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@effect/platform-node": "4.0.0-beta.
|
|
44
|
+
"@effect/platform-node": "4.0.0-beta.98",
|
|
45
45
|
"@types/node": "^26.1.1",
|
|
46
|
-
"effect": "4.0.0-beta.
|
|
46
|
+
"effect": "4.0.0-beta.98",
|
|
47
47
|
"typescript": "^7.0.2",
|
|
48
48
|
"vitest": "^4.1.10"
|
|
49
49
|
},
|
package/dist/Fonts.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { ResvgRenderOptions } from "@resvg/resvg-js";
|
|
2
|
-
import type * as Context from "effect/Context";
|
|
3
|
-
/**
|
|
4
|
-
* Map a scene's declared fonts (the `Fonts` annotation) to resvg font
|
|
5
|
-
* options: every entry with a `src.path` becomes a `fontFiles` entry.
|
|
6
|
-
* Url-only entries are a browser concern and are skipped. System fonts
|
|
7
|
-
* stay loaded (resvg's default) — declared fonts ADD faces; pass
|
|
8
|
-
* `loadSystemFonts: false` yourself for fully self-contained output.
|
|
9
|
-
*/
|
|
10
|
-
export declare const resvgOptions: (scene: {
|
|
11
|
-
readonly annotations: Context.Context<never>;
|
|
12
|
-
}) => ResvgRenderOptions;
|
package/dist/Fonts.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { Fonts } from "effect-motion";
|
|
2
|
-
/**
|
|
3
|
-
* Map a scene's declared fonts (the `Fonts` annotation) to resvg font
|
|
4
|
-
* options: every entry with a `src.path` becomes a `fontFiles` entry.
|
|
5
|
-
* Url-only entries are a browser concern and are skipped. System fonts
|
|
6
|
-
* stay loaded (resvg's default) — declared fonts ADD faces; pass
|
|
7
|
-
* `loadSystemFonts: false` yourself for fully self-contained output.
|
|
8
|
-
*/
|
|
9
|
-
export const resvgOptions = (scene) => {
|
|
10
|
-
const fontFiles = Fonts.get(scene).flatMap((font) => font.src.path === undefined ? [] : [font.src.path]);
|
|
11
|
-
return fontFiles.length === 0 ? {} : { font: { fontFiles } };
|
|
12
|
-
};
|
package/dist/Resvg.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { type ResvgRenderOptions } from "@resvg/resvg-js";
|
|
2
|
-
import * as Effect from "effect/Effect";
|
|
3
|
-
import * as FileSystem from "effect/FileSystem";
|
|
4
|
-
import type * as PlatformError from "effect/PlatformError";
|
|
5
|
-
/**
|
|
6
|
-
* Thin Effect wrappers over resvg. Rasterization is an export tool, not a
|
|
7
|
-
* renderer: it consumes the SVG document strings the string sink produces
|
|
8
|
-
* (or any other SVG) and yields PNG bytes.
|
|
9
|
-
*/
|
|
10
|
-
/** resvg's own options, untranslated — font config and fitTo live here */
|
|
11
|
-
export type { ResvgRenderOptions } from "@resvg/resvg-js";
|
|
12
|
-
declare const RasterizeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
13
|
-
readonly _tag: "RasterizeError";
|
|
14
|
-
} & Readonly<A>;
|
|
15
|
-
/** A failure raised by resvg while parsing or rendering an SVG document. */
|
|
16
|
-
export declare class RasterizeError extends RasterizeError_base<{
|
|
17
|
-
readonly cause: unknown;
|
|
18
|
-
}> {
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Rasterize an SVG document string to PNG bytes. Output dimensions come
|
|
22
|
-
* from the document itself; the string sink stamps the frame's
|
|
23
|
-
* width/height on the root.
|
|
24
|
-
*/
|
|
25
|
-
export declare const rasterize: (svg: string, options?: ResvgRenderOptions) => Effect.Effect<Uint8Array, RasterizeError>;
|
|
26
|
-
/** `rasterize`, then persist through the FileSystem service. */
|
|
27
|
-
export declare const rasterizeToFile: (svg: string, path: string, options?: ResvgRenderOptions) => Effect.Effect<void, RasterizeError | PlatformError.PlatformError, FileSystem.FileSystem>;
|
package/dist/Resvg.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { Resvg as ResvgJs } from "@resvg/resvg-js";
|
|
2
|
-
import * as Data from "effect/Data";
|
|
3
|
-
import * as Effect from "effect/Effect";
|
|
4
|
-
import * as FileSystem from "effect/FileSystem";
|
|
5
|
-
/** A failure raised by resvg while parsing or rendering an SVG document. */
|
|
6
|
-
export class RasterizeError extends Data.TaggedError("RasterizeError") {
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Rasterize an SVG document string to PNG bytes. Output dimensions come
|
|
10
|
-
* from the document itself; the string sink stamps the frame's
|
|
11
|
-
* width/height on the root.
|
|
12
|
-
*/
|
|
13
|
-
export const rasterize = (svg, options) => Effect.try({
|
|
14
|
-
try: () => new Uint8Array(new ResvgJs(svg, options ?? null).render().asPng()),
|
|
15
|
-
catch: (cause) => new RasterizeError({ cause }),
|
|
16
|
-
});
|
|
17
|
-
/** `rasterize`, then persist through the FileSystem service. */
|
|
18
|
-
export const rasterizeToFile = (svg, path, options) => Effect.gen(function* () {
|
|
19
|
-
const bytes = yield* rasterize(svg, options);
|
|
20
|
-
const fs = yield* FileSystem.FileSystem;
|
|
21
|
-
yield* fs.writeFile(path, bytes);
|
|
22
|
-
});
|