@effect-motion/export 0.2.0 → 0.3.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/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 { type Entity, Scene } from "effect-motion";
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` → SVG string sink → resvg → ffmpeg — reading the
12
- * framerate and dimensions from the scene's own frame metadata and feeding
13
- * declared fonts to the rasterizer.
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
- /** How many frames to rasterize concurrently (order preserved). Default 4. */
43
- readonly concurrency?: number;
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, R, Entities extends Entity.AnyEntity>(scene: Scene.Scene<E, R, Entities>, outPath: string, options?: VideoOptions) => Effect.Effect<void, E | Ffmpeg.EncodeError | Resvg.RasterizeError, Exclude<R, Scope.Scope | SceneInternalR> | ChildProcessSpawner.ChildProcessSpawner>;
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 { Layer } from "effect";
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 { Scene, Svg } from "effect-motion";
6
- import * as Ffmpeg from "./Ffmpeg";
7
- import { resvgOptions } from "./Fonts";
8
- import * as Resvg from "./Resvg";
9
- // SVG string sink + built-in shape renderers, provided internally so callers
10
- // don't wire renderers for the export path
11
- const renderLayer = Svg.layer.pipe(Layer.provideMerge(Svg.shapesLayer));
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
- if (meta.width % 2 !== 0 || meta.height % 2 !== 0) {
34
- const odd = meta.width % 2 !== 0
35
- ? `width ${meta.width}`
36
- : `height ${meta.height}`;
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
- const pngStream = Stream.mapEffect(allFrames, (frame) => renderer
46
- .render(frame, {})
47
- .pipe(Effect.flatMap((svg) => Resvg.rasterize(svg, fontOpts))), { concurrency });
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(Effect.provide(renderLayer));
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 * as Fonts from "./Fonts";
4
- export * as Resvg from "./Resvg";
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 Fonts from "./Fonts";
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.2.0",
4
- "description": "Node-only export tools for effect-motion: rasterize SVG frames to PNG via resvg",
3
+ "version": "0.3.1",
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
- "svg",
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.2.0"
37
+ "@effect-motion/thorvg": "^0.1.0",
38
+ "effect-motion": "^0.3.1"
39
39
  },
40
40
  "peerDependencies": {
41
- "effect": ">=4.0.0-beta.94"
41
+ "effect": ">=4.0.0-beta.98"
42
42
  },
43
43
  "devDependencies": {
44
- "@effect/platform-node": "4.0.0-beta.94",
44
+ "@effect/platform-node": "4.0.0-beta.98",
45
45
  "@types/node": "^26.1.1",
46
- "effect": "4.0.0-beta.94",
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
- });