@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-renderer

npm · Source on GitHub

How 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.ts JSDoc, 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:

  1. find the latest keyframe whose time <= t (call it a)
  2. find the earliest keyframe whose time > t (call it b)
  3. apply a.easing to (t - a.time) / (b.time - a.time) to get u
  4. return lerp(a.value, b.value, u) (numeric) or lerpColor(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 to linear.

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 by time).
  • 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 by renderAnimation.

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 by renderAnimation.

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.