@molecule/api-animation-renderer
Utility · animation-renderer · API (Node) · v1.0.2 · Apache-2.0
Server-side animation rendering — render an animation document to Lottie JSON or MP4/GIF (frame-by-frame via canvas-render + ffmpeg). Queue-driven.
npm install @molecule/api-animation-rendererHow it works
@molecule/api-animation-renderer is a utility package for the API (Node) side (animation-renderer).
// Wire adapters once at startup.
import { configureAnimationRenderer, type FfmpegAdapter } from '@molecule/api-animation-renderer'
import { type CanvasDocument, renderCanvasDocument } from '@molecule/api-canvas-render'
// Your thin ffmpeg wrapper — shell out to ffmpeg, resolve the encoded buffer.
const spawnFfmpeg = async (
format: 'mp4' | 'gif',
frames: Buffer[],
opts: { fps: number; width: number; height: number },
): Promise<Buffer> => {
throw new Error(`wire ffmpeg here: ${format}, ${frames.length} frames @ ${opts.fps}fps`)
}
const ffmpeg: FfmpegAdapter = {
encodeMp4: (frames, opts) => spawnFfmpeg('mp4', frames, opts),
encodeGif: (frames, opts) => spawnFfmpeg('gif', frames, opts),
}
configureAnimationRenderer({
// Each per-frame snapshot is canvas-render-shaped — its layers ARE canvas-render layers.
canvas: { renderFrame: (doc, opts) => renderCanvasDocument(doc as CanvasDocument, opts) },
ffmpeg,
concurrency: 2,
})Reference
Server-side animation rendering for molecule.dev. Consumes an
AnimationDocument (the keyframe representation produced by
@molecule/app-feature-animation-canvas-react) and produces either:
- a Lottie 5.x JSON document (lossless; the canvas at every frame reconstructs in any Lottie player), or
- an MP4 / animated GIF (frame-by-frame raster via an injected canvas-render adapter, then concatenated with an injected ffmpeg adapter).
The package is queue-driven — renderAnimation() enqueues a job and
returns a RenderJob handle whose done promise resolves to a
RenderResult. getRenderStatus polls a job by id;
cancelRender aborts a queued or running job.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
// Wire adapters once at startup.
import { configureAnimationRenderer, type FfmpegAdapter } from '@molecule/api-animation-renderer'
import { type CanvasDocument, renderCanvasDocument } from '@molecule/api-canvas-render'
// Your thin ffmpeg wrapper — shell out to ffmpeg, resolve the encoded buffer.
const spawnFfmpeg = async (
format: 'mp4' | 'gif',
frames: Buffer[],
opts: { fps: number; width: number; height: number },
): Promise<Buffer> => {
throw new Error(`wire ffmpeg here: ${format}, ${frames.length} frames @ ${opts.fps}fps`)
}
const ffmpeg: FfmpegAdapter = {
encodeMp4: (frames, opts) => spawnFfmpeg('mp4', frames, opts),
encodeGif: (frames, opts) => spawnFfmpeg('gif', frames, opts),
}
configureAnimationRenderer({
// Each per-frame snapshot is canvas-render-shaped — its layers ARE canvas-render layers.
canvas: { renderFrame: (doc, opts) => renderCanvasDocument(doc as CanvasDocument, opts) },
ffmpeg,
concurrency: 2,
})
// Submit a render and poll for completion.
import {
type AnimationDocument,
getRenderStatus,
renderAnimation,
} from '@molecule/api-animation-renderer'
const doc: AnimationDocument = {
width: 1080,
height: 1920,
duration: 2,
fps: 30,
layers: [
{
id: 'bg',
kind: 'rect',
shape: { x: 0, y: 0, width: 1080, height: 1920, fill: '#0ea5e9' },
tracks: {
'transform.opacity': [
{ time: 0, value: 0 },
{ time: 1, value: 1 },
],
},
},
],
}
const job = renderAnimation(doc, { format: 'mp4', resolution: { width: 1080, height: 1920 } })
const result = await job.done
console.log(result.contentType, result.frameCount)
// Or, in an HTTP-driven flow:
const snapshot = getRenderStatus(job.id) // → { status: 'rendering', framesRendered: 17, ... }
// Framework-neutral HTTP handlers (Express/Fastify/Koa need only a thin shim).
import {
type AnimationRenderResponse,
createAnimationCancelHandler,
createAnimationRenderHandler,
createAnimationStatusHandler,
} from '@molecule/api-animation-renderer'
const submit = createAnimationRenderHandler()
const status = createAnimationStatusHandler()
const cancel = createAnimationCancelHandler()
// Adapt your framework's response object to the minimal contract:
const shim = (res: {
setHeader: (n: string, v: string) => void
status: (s: number) => unknown
send: (b: unknown) => unknown
json: (b: unknown) => unknown
}): AnimationRenderResponse => ({
setHeader: (n, v) => res.setHeader(n, v),
setStatus: (s) => void res.status(s),
sendBuffer: (b) => void res.send(b),
sendJson: (b) => void res.json(b),
})
// Mount (Express shown as comments):
// router.post('/animation/render', (req, res, next) => submit({ body: req.body }, shim(res)).catch(next))
// router.get('/animation/render/:id', (req, res, next) => status({ params: req.params, query: req.query }, shim(res)).catch(next))
// router.delete('/animation/render/:id', (req, res, next) => cancel({ params: req.params }, shim(res)).catch(next))
console.log(typeof shim, typeof submit, typeof status, typeof cancel)
Type
utility
Installation
npm install @molecule/api-animation-renderer
API
Interfaces
AnimationRenderRequest
Minimal request shape consumed by the handlers.
interface AnimationRenderRequest {
/** Parsed JSON body — `{ doc, options }`. Only used by the submit handler. */
body?: unknown
/** Path/query parameter holding the job id. Used by status / cancel handlers. */
params?: { id?: string }
/** Whether the client requested the result buffer inline (`?download=1`). */
query?: { download?: string }
}
AnimationRenderResponse
Minimal response shape consumed by the handlers.
interface AnimationRenderResponse {
/** Set a single response header. */
setHeader: (name: string, value: string) => void
/** Set the HTTP status code. */
setStatus: (status: number) => void
/** Write a binary buffer body and end the response. */
sendBuffer: (buffer: Buffer) => void
/** Write a JSON body and end the response. */
sendJson: (body: unknown) => void
}
CreateAnimationRenderHandlerOptions
Options for createAnimationRenderHandler.
interface CreateAnimationRenderHandlerOptions {
/**
* Optional pre-flight validator. Throw to reject the request; the
* thrown error's `.message` becomes the JSON `error` field with HTTP 400.
*/
validate?: (doc: AnimationDocument, options: RenderAnimationOptions) => void | Promise<void>
}
LottieDocument
Lottie root document shape (the bits we emit). The full schema accepts many more optional fields; consumers tolerate missing-but-allowed keys.
interface LottieDocument {
/** Lottie schema version. */
v: string
/** Frame rate. */
fr: number
/** In point (frames). Always 0. */
ip: number
/** Out point (frames) — `floor(duration * fps)`. */
op: number
/** Width in pixels. */
w: number
/** Height in pixels. */
h: number
/** Document name. */
nm: string
/** Document direction (1 = LTR; spec default). */
ddd: 0
/** Asset references — empty in our output. */
assets: unknown[]
/** Layer list. */
layers: LottieLayer[]
}
RenderQueueOptions
Configuration for RenderQueue.
interface RenderQueueOptions {
/** Maximum jobs that may run simultaneously. Defaults to `1`. */
concurrency?: number
/**
* Function that performs the actual render — injected so tests can
* replace it with a deterministic stub. Receives the `AnimationDocument`,
* the resolved options, and the per-job cancellation signal.
*/
runner: (
doc: AnimationDocument,
options: ResolvedRenderOptions,
signal: AbortSignal,
onProgress: (frame: number) => void,
) => Promise<RenderResult>
}
ResolvedRenderOptions
Options after defaults have been applied — the shape the queue runner is guaranteed to receive.
interface ResolvedRenderOptions extends RenderAnimationOptions {
format: 'lottie' | 'mp4' | 'gif'
width: number
height: number
fps: number
totalFrames: number
}
RendererConfig
Adapter set passed to configureAnimationRenderer /
createAnimationRenderer.
interface RendererConfig {
/**
* Adapter for rasterising one frame to PNG. In production this typically
* wraps `renderCanvasDocument` from `@molecule/api-canvas-render` (a
* direct-import utility — there is no 'canvas-render' bond category).
* Optional — only the lottie path can complete without it.
*/
canvas?: CanvasRenderAdapter
/**
* Adapter for concatenating per-frame PNGs into MP4 / GIF. In
* production this shells out to ffmpeg. Optional — only the lottie
* path can complete without it.
*/
ffmpeg?: FfmpegAdapter
/**
* Maximum concurrent jobs. Defaults to `1` (sequential), since the
* frame-rasterization path tends to be CPU-bound and the ffmpeg path
* spawns child processes.
*/
concurrency?: number
}
AnimationRenderer
Bound, pre-configured renderer instance — output of
createAnimationRenderer. Identical surface to the singleton
functions exported below.
interface AnimationRenderer {
renderAnimation: (doc: AnimationDocument, options?: RenderAnimationOptions) => RenderJob
getRenderStatus: (jobId: string) => RenderJobSnapshot | undefined
cancelRender: (jobId: string) => boolean
}
SnapshotDocument
The canvas-render document shape we produce.
interface SnapshotDocument {
width: number
height: number
background?: string
layers: SnapshotLayer[]
}
AnimationTransform
2D affine transform applied to an animation layer.
interface AnimationTransform {
x?: number
y?: number
rotation?: number
scaleX?: number
scaleY?: number
opacity?: number
}
Keyframe
Numeric or color value held at a particular point in time.
time is an absolute time in seconds, counted from the start of the
animation (0). The interpolated value at any time t is:
- find the latest keyframe whose
time <= t(call ita) - find the earliest keyframe whose
time > t(call itb) - apply
a.easingto(t - a.time) / (b.time - a.time)to getu - return
lerp(a.value, b.value, u)(numeric) orlerpColor(a.value, b.value, u)(color).
If t < firstKeyframe.time, the first value is held; if
t > lastKeyframe.time, the last value is held.
interface Keyframe<TValue = number> {
/** Absolute time in seconds. Must be `>= 0`. */
time: number
/** Property value at this point in time. */
value: TValue
/** Easing applied between this keyframe and the next. Defaults to `linear`. */
easing?: Easing
}
AnimationLayer
A CanvasDocument-style shape (rectangle, ellipse, line, path,
text, image) whose visual properties may be animated by per-property
keyframe tracks.
The renderer never owns a CanvasDocument — instead, at every
output frame it builds one from the layer's static fields plus the
interpolated keyframe values, and hands that CanvasDocument to
the injected canvas-render adapter. This keeps animation-renderer fully
decoupled from any specific renderer implementation: substitute the
adapter and the same animation document can target a server-side raster,
a PDF, or a test fixture.
interface AnimationLayer {
/**
* Stable layer identifier. Used in Lottie output as the layer's `nm`
* (name) field and as the seed for deterministic z-ordering.
*/
id: string
/** Layer kind — mirrors the `kind` field on canvas-render layers. */
kind: 'rect' | 'ellipse' | 'line' | 'path' | 'text' | 'image' | 'group'
/** Static layer payload. Animated properties live in `tracks`. */
shape: Record<string, unknown>
/**
* Per-property keyframe tracks. The key is the path of the animated
* property in `shape` (top-level keys only — e.g. `"x"`, `"width"`,
* `"fill"`), and the value is a list of keyframes ordered by `time`.
*
* Transform-level animations live in their own dedicated keys
* (`"transform.x"`, `"transform.opacity"`, etc.) which the renderer
* applies to the resulting `CanvasDocument`'s top-level transform
* fields.
*/
tracks?: Record<string, Keyframe<number | string>[]>
/** Optional initial transform — folded into per-frame transform values. */
transform?: AnimationTransform
/** When `kind === 'group'`, the children animate inside this layer's transform. */
children?: AnimationLayer[]
}
AnimationDocument
Top-level animation document consumed by renderAnimation.
interface AnimationDocument {
/** Width in user-space units. */
width: number
/** Height in user-space units. */
height: number
/** Frames per second. Defaults to 30 if omitted at the call site. */
fps?: number
/** Total duration in seconds. */
duration: number
/** Optional document background color (CSS string). */
background?: string
/** Layers in z-order (later = on top). */
layers: AnimationLayer[]
}
RenderAnimationOptions
Options accepted by renderAnimation.
interface RenderAnimationOptions {
/** Output format. Defaults to `"lottie"`. */
format?: AnimationFormat
/**
* Output resolution as `{ width, height }` in pixels. Defaults to the
* document's `width`/`height`.
*/
resolution?: { width: number; height: number }
/** Frames per second. Overrides `doc.fps`. Defaults to `doc.fps ?? 30`. */
fps?: number
/** Optional, stable identifier — useful when callers want job ids derived from app state. */
jobId?: string
}
RenderResult
Result of a successful render.
interface RenderResult {
/** Output buffer. */
buffer: Buffer
/** MIME type — `application/json` for lottie, `video/mp4`, `image/gif`. */
contentType: string
/** Suggested filename extension (no leading dot). */
extension: 'json' | 'mp4' | 'gif'
/** Number of frames the output contains (for lottie this is `floor(duration * fps)`). */
frameCount: number
}
RenderJob
Handle returned by renderAnimation — gives callers a way to
track progress, retrieve the final result, or cancel the job.
interface RenderJob {
/** Stable, unique identifier. */
id: string
/** Current status. */
status: RenderJobStatus
/** Output format. */
format: AnimationFormat
/** Total frames the job will produce. */
totalFrames: number
/** Frames rendered so far. Always `0` for the lottie path. */
framesRendered: number
/** Populated when `status === 'complete'`. */
result?: RenderResult
/** Populated when `status === 'failed'`. */
error?: string
/** Promise that resolves once the job reaches a terminal state. */
done: Promise<RenderResult>
}
RenderJobSnapshot
Snapshot of a job — the shape returned by getRenderStatus.
interface RenderJobSnapshot {
id: string
status: RenderJobStatus
format: AnimationFormat
totalFrames: number
framesRendered: number
contentType?: string
extension?: 'json' | 'mp4' | 'gif'
error?: string
}
FfmpegAdapter
Adapter that concatenates per-frame PNG buffers into an MP4 or GIF.
Implementations typically shell out to ffmpeg (the recommended path —
no Node-side video encoding) but the surface is deliberately abstract
so tests can substitute an in-memory implementation that simply
concatenates the buffers it receives. Keeping this an injectable seam
means the package itself never imports child_process or
fluent-ffmpeg — host applications wire whatever encoder they prefer.
interface FfmpegAdapter {
/**
* Concatenate PNG frame buffers into an MP4.
*
* @param frames - Array of PNG buffers, one per output frame.
* @param options - Frame rate + dimensions.
*/
encodeMp4: (
frames: Buffer[],
options: { fps: number; width: number; height: number },
) => Promise<Buffer>
/**
* Concatenate PNG frame buffers into an animated GIF.
*
* @param frames - Array of PNG buffers, one per output frame.
* @param options - Frame rate + dimensions.
*/
encodeGif: (
frames: Buffer[],
options: { fps: number; width: number; height: number },
) => Promise<Buffer>
}
CanvasRenderAdapter
Adapter that rasterizes a single canvas snapshot to a PNG buffer.
In production this typically wraps renderCanvasDocument from
@molecule/api-canvas-render (a direct-import utility — there is no
'canvas-render' bond category). The package accepts it as an injectable
dependency rather than importing the canvas-render module directly so
the adapter can be swapped at runtime (PNG via @napi-rs/canvas,
alternative WASM raster, headless browser, etc.) without rewriting the
animation pipeline.
interface CanvasRenderAdapter {
/**
* Render one frame.
*
* @param doc - A canvas-render-style document — `{ width, height,
* background?, layers: [...] }` with each layer matching the
* canvas-render `Layer` discriminated union.
* @param options - Output options. The animation renderer always asks
* for PNG; alternative formats are not used for frame composition.
*/
renderFrame: (
doc: { width: number; height: number; background?: string; layers: unknown[] },
options: { format: 'png'; width: number; height: number; dpi?: number },
) => Promise<{ buffer: Buffer }>
}
Types
AnimationFormat
Output formats supported by renderAnimation.
type AnimationFormat = 'lottie' | 'mp4' | 'gif'
RenderJobStatus
Job lifecycle states. The state machine is strictly forward-only:
queued → rendering → complete (or cancelled / failed from any
non-terminal state).
type RenderJobStatus = 'queued' | 'rendering' | 'complete' | 'cancelled' | 'failed'
Easing
Easing function applied between two keyframes. linear is the default
(no easing). step snaps from from to to at the start of the
interval. The cubic bezier curves match the canonical CSS easing names
to keep the animation document portable to web players.
type Easing = 'linear' | 'step' | 'ease-in' | 'ease-out' | 'ease-in-out'
Classes
RenderQueue
Sequential render queue. One queue instance owns its job records — call
RenderQueue.submit to enqueue a job and RenderQueue.cancel
to abort one.
class RenderQueue {
/** All jobs ever enqueued, indexed by id. */
private readonly jobs = new Map<string, JobRecord>()
/** Ids waiting to start. */
private readonly waiting: string[] = []
/** Number of currently running jobs. */
private running = 0
/** Concurrency limit. */
private readonly concurrency: number
/** Render runner. */
private readonly runner: RenderQueueOptions['runner']
/**
* Create a new render queue with the given configuration.
*
* @param options - Queue configuration. The `runner` is required.
*/
constructor(options: RenderQueueOptions) {
this.concurrency = options.concurrency ?? 1
this.runner = options.runner
}
/**
* Enqueue a render job. Returns immediately with a `RenderJob`
* whose `done` promise resolves when the job reaches a terminal state.
*
* @param doc - Animation document.
* @param options - Resolved render options.
* @param id - Stable id (host-supplied or generated).
* @returns The enqueued job handle.
*/
submit(doc: AnimationDocument, options: ResolvedRenderOptions, id: string): RenderJob {
const controller = new AbortController()
let resolve!: (r: RenderResult) => void
let reject!: (e: unknown) => void
const done = new Promise<RenderResult>((res, rej) => {
resolve = res
reject = rej
})
const job: RenderJob = {
id,
status: 'queued',
format: options.format,
totalFrames: options.totalFrames,
framesRendered: 0,
done,
}
const record: JobRecord = { job, doc, options, controller, resolve, reject }
this.jobs.set(id, record)
this.waiting.push(id)
this.dispatch()
return job
}
/**
* Read-only snapshot of a job's state (no Promise / no controllers).
* Suitable for HTTP responses and worker logs.
*
* @param id - Job id.
* @returns The snapshot, or `undefined` if no such job exists.
*/
snapshot(id: string): RenderJobSnapshot | undefined {
const rec = this.jobs.get(id)
if (!rec) return undefined
const { job } = rec
return {
id: job.id,
status: job.status,
format: job.format,
totalFrames: job.totalFrames,
framesRendered: job.framesRendered,
...(job.result
? { contentType: job.result.contentType, extension: job.result.extension }
: {}),
...(job.error ? { error: job.error } : {}),
}
}
/**
* Cancel a queued or running job. Resolves to `true` if the job
* existed and was in a non-terminal state; `false` otherwise.
*
* @param id - Job id.
* @returns `true` if a transition to `cancelled` happened.
*/
cancel(id: string): boolean {
const rec = this.jobs.get(id)
if (!rec) return false
if (isTerminal(rec.job.status)) return false
rec.job.status = 'cancelled'
rec.controller.abort()
rec.reject(new Error('cancelled'))
// Remove from waiting queue if it was still pending — otherwise the
// running job's runner will observe the abort signal.
const idx = this.waiting.indexOf(id)
if (idx !== -1) this.waiting.splice(idx, 1)
return true
}
/**
* Internal: pump pending jobs onto running slots respecting `concurrency`.
* Each running job uses the doc/options captured on its own record at
* submit-time — no parameters needed.
*/
private dispatch(): void {
while (this.running < this.concurrency && this.waiting.length > 0) {
const id = this.waiting.shift()!
const rec = this.jobs.get(id)
if (!rec) continue
if (rec.job.status === 'cancelled') continue
this.running++
rec.job.status = 'rendering'
this.runner(rec.doc, rec.options, rec.controller.signal, (frame) => {
if (rec.job.status === 'rendering') rec.job.framesRendered = frame
})
.then((result) => {
if (rec.job.status === 'cancelled') return
rec.job.status = 'complete'
rec.job.result = result
rec.job.framesRendered = rec.options.totalFrames
rec.resolve(result)
})
.catch((err: unknown) => {
if (rec.job.status === 'cancelled') return
rec.job.status = 'failed'
rec.job.error = err instanceof Error ? err.message : String(err)
rec.reject(err)
})
.finally(() => {
this.running--
if (this.waiting.length > 0) {
this.dispatch()
}
})
}
}
}
Functions
createAnimationRenderHandler(handlerOptions)
Build a handler for POST /animation/render. Validates the body,
enqueues the job, and returns the job id and initial status. The
response is sent immediately — the caller polls
createAnimationStatusHandler for completion.
function createAnimationRenderHandler(
handlerOptions?: CreateAnimationRenderHandlerOptions,
): (req: AnimationRenderRequest, res: AnimationRenderResponse) => Promise<void>
handlerOptions— Optional validator.
Returns: Async handler accepting { body } + a response shim.
createAnimationStatusHandler()
Build a handler for GET /animation/render/:id. Returns either the
current snapshot (for non-terminal states) or the final buffer with
the appropriate content-type when the job has completed and the
client requested it via ?download=1.
function createAnimationStatusHandler(): (
req: AnimationRenderRequest,
res: AnimationRenderResponse,
) => Promise<void>
Returns: Async handler.
createAnimationCancelHandler()
Build a handler for DELETE /animation/render/:id. Cancels a queued
or running job. Returns 404 if no such job exists or 409 if the job
had already reached a terminal state.
function createAnimationCancelHandler(): (
req: AnimationRenderRequest,
res: AnimationRenderResponse,
) => Promise<void>
Returns: Async handler.
applyEasing(u, easing)
Apply an Easing curve to a normalized parameter u ∈ [0, 1].
function applyEasing(u: number, easing: Easing | undefined): number
u— Normalized progress along the keyframe segment.easing— Easing function name. Defaults tolinear.
Returns: Eased progress in [0, 1].
lerp(a, b, u)
Linear interpolate between two numbers.
function lerp(a: number, b: number, u: number): number
a— Start value.b— End value.u— Eased progress in[0, 1].
Returns: Interpolated value.
lerpColor(a, b, u)
Interpolate a CSS-style hex color string between two endpoints. Both
inputs must be #rgb, #rrggbb, or #rrggbbaa. Non-hex strings (CSS
names, rgb(), etc.) snap on segment boundaries — the renderer is
expected to feed the keyframe machinery normalized hex values.
function lerpColor(a: string, b: string, u: number): string
a— Start color (hex string).b— End color (hex string).u— Eased progress in[0, 1].
Returns: Interpolated #rrggbb (or #rrggbbaa if either input had alpha).
valueAtTime(track, t)
Compute the value of a keyframe track at time t (seconds).
Implements the rules described in Keyframe: hold-before-first,
hold-after-last, segment-wise easing + linear interpolation.
function valueAtTime(track: Keyframe<T>[], t: number): T | undefined
track— Ordered keyframes (must be sorted ascending bytime).t— Time in seconds.
Returns: Interpolated value or undefined if the track is empty.
toLottie(doc, options)
Convert an AnimationDocument to a Lottie 5.x JSON document.
function toLottie(
doc: AnimationDocument,
options: { fps: number; width: number; height: number },
): LottieDocument
doc— Source animation document.options— FPS + dimensions overrides.
Returns: A Lottie 5.x-shaped object that can be JSON.stringify-ed directly into a .json payload.
makeStandardRunner(adapters)
Build the standard runner that the public API uses — splits on output format and dispatches to the lottie / mp4 / gif paths.
Exported so callers building a custom queue (e.g. Redis-backed) can compose the same render pipeline with their own scheduling layer.
function makeStandardRunner(adapters: {
canvas?: CanvasRenderAdapter
ffmpeg?: FfmpegAdapter
toLottie: (
doc: AnimationDocument,
opts: { fps: number; width: number; height: number },
) => unknown
snapshotAtTime: (doc: AnimationDocument, t: number) => unknown
}): (
doc: AnimationDocument,
options: ResolvedRenderOptions,
signal: AbortSignal,
onProgress: (frame: number) => void,
) => Promise<RenderResult>
adapters— Canvas + ffmpeg adapters.
Returns: A function suitable as RenderQueueOptions.runner.
createAnimationRenderer(config)
Build a fresh renderer with its own queue. Use this when an app needs to keep multiple animation pipelines isolated from each other (e.g. preview vs export queues).
function createAnimationRenderer(config?: RendererConfig): AnimationRenderer
config— Adapter set + queue configuration.
Returns: A bound renderer instance.
configureAnimationRenderer(config)
Configure the singleton renderer. Call once at process startup with
the host's adapters wired (typically a wrapper around
renderCanvasDocument from @molecule/api-canvas-render plus a thin
ffmpeg wrapper). Subsequent calls replace the singleton and
detach the previous queue (in-flight jobs continue to completion but
status lookups against the old queue must use the returned instance).
function configureAnimationRenderer(config?: RendererConfig): AnimationRenderer
config— Adapter set + queue configuration.
Returns: The newly created singleton.
renderAnimation(doc, options)
Submit an animation document to the singleton renderer. Returns
synchronously with a RenderJob handle whose done promise
resolves to the rendered buffer.
For MP4 / GIF output the singleton must have been configured with a
canvas adapter and an ffmpeg adapter via
configureAnimationRenderer; the lottie path needs neither.
function renderAnimation(doc: AnimationDocument, options?: RenderAnimationOptions): RenderJob
doc— Animation document.options— Output format, resolution, fps, optional jobId.
Returns: The enqueued render job.
getRenderStatus(jobId)
Look up the current state of a previously submitted job.
function getRenderStatus(jobId: string): RenderJobSnapshot | undefined
jobId— The id returned byrenderAnimation.
Returns: Job snapshot, or undefined if no such job exists.
cancelRender(jobId)
Cancel a queued or running job. Resolves to true if the job was
found and transitioned to cancelled; false otherwise.
function cancelRender(jobId: string): boolean
jobId— The id returned byrenderAnimation.
Returns: Whether cancellation took effect.
snapshotAtTime(doc, t)
Materialize the document at time t (seconds).
Each layer's shape is shallow-copied; for every key that appears in
layer.tracks, the keyframed value at time t overrides the static
value. Transform tracks (keys starting with "transform.") write to
the resulting layer's top-level transform fields (x, y,
rotation, etc.) which the canvas-render adapter consumes directly.
function snapshotAtTime(doc: AnimationDocument, t: number): SnapshotDocument
doc— The animation document.t— Time in seconds.
Returns: A canvas-render-shaped document containing the frame snapshot.
Injection Notes
The renderer is decoupled from any concrete canvas / ffmpeg
implementation — adapters are passed via
configureAnimationRenderer. There is NO 'canvas-render' bond
category: @molecule/api-canvas-render is a direct-import utility, so
wrap its renderCanvasDocument as the canvas adapter exactly as shown.
The same pipeline can target @napi-rs/canvas, a WASM raster, or a
headless browser by substituting the adapter. Lottie output requires no
adapters at all — MP4/GIF require BOTH (jobs fail with a clear error
otherwise).
Job state lives in process memory only — no database table, no
environment variables. Jobs do not survive restarts, and in
multi-process deployments getRenderStatus must run in the process that
accepted the submit (or supply your own jobId and persist results
yourself). SERVER-ONLY: a browser-guard throws if bundled into client
code.
Resource intensity: a frame-rasterised export of a long, complex animation will produce many megabytes of intermediate PNG buffers. Run the queue on a dedicated worker (cron / job runner / sidecar container) rather than inline on the request thread, and gate the submit handler behind a tier check or rate limiter.