@babylonjs/lottie-player
v10.0.2
Published
Function-based WebGL2 Lottie player with stencil-then-cover vector rendering, text and image layers, and an off-thread Web Worker delivery path.
Readme
@babylonjs/lottie-player
A worker-first WebGL2 vector Lottie player with full JSON and binary animation delivery.
Both players render through
@babylonjs/lite-gl, a small WebGL2
micro-engine. The full player parses supported Lottie JSON at runtime; the binary player loads a
smaller deterministic binary animation whose paths and draw data were prepared at build time.
Dependency. This package depends on the published @babylonjs/lite-gl
(its WebGL2 backend) as a normal npm dependency.
Version 10 takeover. Version 10 is a breaking replacement for the experimental v9 player under
this canonical npm identity. Existing consumers on @babylonjs/lottie-player@^9 remain on the
frozen v9 line; v10 is versioned independently from Babylon.js. The old LocalPlayer sprite-atlas
API is replaced by the worker-first vector APIs documented below. The package license changes from
MIT in v9 to Apache-2.0 in v10. The canonical source repository is private, while the npm tarball
ships this README, the license, declarations, and compiled runtime modules. JavaScript source maps
are intentionally excluded so the public tarball neither embeds private TypeScript nor points to
source files that are not shipped.
Quick start
All commands run from the repo root:
| Goal | Command | What it does |
| --------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Live viewer | pnpm demo | Builds + serves an interactive viewer at http://localhost:5180. Compare full JSON and binary animation playback in local tooling or a worker. |
| Runtime benchmark | pnpm measure | Measures the current renderer with tracked fixtures in headed Chrome and opens a compact report. |
| Build the library | pnpm build | Compiles per-module ESM/types and self-contained classic workers into dist/. |
| Watch build | pnpm dev | Runs TypeScript in watch mode. |
The two node scripts behind those:
node demo/serve.mjs— the live viewer (esbuild watch + dev server).node measure/bench/run.mjs— the current-player runtime benchmark.
CI and npm releases
azure-pipelines-npm-verify.yml runs for pull requests targeting
main and pushes to main. It checks formatting, linting and types, unit tests, API documentation,
the production build, visual regressions, and the final npm package contents.
azure-pipelines-npm-publish.yml runs the full release validation
and publication path when config/release.json changes on main. Set type
to patch, minor, or major, increment nonce, and merge the reviewed change to publish from
main. Manually queued runs choose the release type in the ADO UI and default to dry-run mode;
manual live publication requires dryRun to be explicitly disabled.
The resolver requires the existing canonical npm package, uses the newer of npm latest and the
highest npm-lottie-player-v* tag, and rejects unchanged releases, version/tag collisions, any
candidate below the source manifest version, and patch/minor releases that contain breaking-change
markers. The first standalone release uses an explicit major bump from npm v9 to v10. Publication
happens before the matching source tag is created. Because the accidental 11.0.0 publication was
withdrawn and cannot be reused on npm, the next major release after v10 skips that exact version and
publishes as 11.0.1.
azure-pipelines-pr-snapshot.yml builds every PR into an
interactive demo snapshot and posts its URL on the PR. It also attaches an installable npm tarball
with a unique 0.0.0-pr.<PR>.<build> version to the ADO build as the npm-package artifact, but
never publishes PR packages to npm. Demo deployment uses the BabylonJS-Deployment and
BabylonJS-CI-Infrastructure variable groups. Deployment supplies DEPLOY_TOKEN and
DEPLOYMENT_SERVER; Infrastructure supplies DEPLOY_ENDPOINT_UPLOAD, SNAPSHOT_CDN_URL,
SNAPSHOTS_STORAGE_ACCOUNT, and GITHUB_SERVICE_CONNECTION. Fork PRs still produce the package
artifact but skip deployment and commenting so deployment secrets remain unavailable. DEPLOY_TOKEN
must remain secret, and the ADO option that makes secrets available to fork builds must remain disabled.
The GitHub service connection named by GITHUB_SERVICE_CONNECTION must be pre-authorized for this
pipeline before snapshot runs can post PR comments. A manually queued snapshot run must provide its
PR number through the prNumber parameter.
Measurement options (env vars)
$env:ANIMS="gradients,path-morph" # tracked fixture IDs or local anim names
$env:PERF_DURATION="5" # seconds per fixture (default 5)
$env:BASELINE_RESULTS="baseline.json" # optional previous results for report deltas
$env:NO_OPEN="1" # don't open the generated report
pnpm measureWhat this is, and why
Lottie is a JSON format for vector animations: shapes, gradients, strokes, masks, morphing paths, text, and image layers. The full player parses that document and renders supported content via stencil-then-cover. The binary compiler moves validation, path flattening, tessellation, and packing offline; the binary player samples transforms and morphs from immutable buffers.
Goals
- WebGL2. The renderer targets the broadly available
@babylonjs/lite-glWebGL2 surface. - Explicit source-format entry points. Full accepts JSON; binary accepts only compiler-produced binary animations and has no JSON parser, text renderer, or image renderer.
- Small bundle, fast time-to-first-frame. Measured continuously (see below).
- Off-thread in production. The shipped path renders on an
OffscreenCanvasinside a Web Worker. Main-thread playback exists in repository tooling for testing and measurement only; it is not a public package mode.
Public API
Every public player runs off-thread on an OffscreenCanvas; the main thread only owns the DOM
canvas, sizing, and control messages. The full worker performs JSON parsing and runtime
tessellation. The binary worker decodes geometry prepared by the offline compiler. Both workers
sample animation and submit GPU work. Local renderer factories remain internal for the workers and
repository tooling; they are not package exports.
The architecture has three independent dimensions:
- Player: full JSON or binary animation.
- Execution: public playback runs in a worker; main-thread playback is internal demo, benchmark, and test tooling.
- Delivery: a consumer bundler discovers and emits the selected worker automatically, or the host serves prebuilt worker assets and uses the standalone ESM or classic-script client.
There are two source-format variants:
- Full JSON renders all runtime-supported layer types, including text and embedded or external images. Use it as the fallback for documents outside the compiler profile.
- Binary renders the compiler's constrained vector profile from deterministic binary animation assets. Compilation rejects unsupported content instead of silently dropping it.
The player and delivery choices combine as follows. Explicit-URL hosts use one shared standalone client; the supplied worker file selects the player:
| Delivery | Full JSON | Binary |
| -------------------------- | ------------------------------------------------------- | -------------------------------------------------------------- |
| Bundler (automatic worker) | @babylonjs/lottie-player → createLottieWorkerPlayer | @babylonjs/lottie-player/binary → createBinaryWorkerPlayer |
| Explicit worker URL (ESM) | /standalone + workers/full.worker.js | /standalone + workers/binary.worker.js |
| Classic script | standalone.min.js + workers/full.worker.js | standalone.min.js + workers/binary.worker.js |
Bundler usage
The root entry creates the full worker automatically:
import { createLottieWorkerPlayer, playWorkerAnimationAsync } from "@babylonjs/lottie-player";
const player = createLottieWorkerPlayer();
await playWorkerAnimationAsync(player, {
container: document.getElementById("animation")!,
animationSource: "./animation.json",
loop: true,
fit: "scale-down",
onError: () => document.getElementById("fallback")?.removeAttribute("hidden"),
});The full JSON and binary players support the same aspect-ratio-preserving fit policies:
| fit | Container larger than the animation | Container smaller than the animation |
| ------------ | -------------------------------------------------------- | ----------------------------------------------- |
| contain | Enlarges until one dimension reaches the container edge. | Shrinks until the entire animation fits. |
| scale-down | Keeps the authored dimensions. | Shrinks until the entire animation fits. |
| none | Keeps the authored dimensions. | Keeps the authored dimensions and may overflow. |
contain is the default. The player observes container resizes for contain and scale-down;
none always uses the Lottie document's top-level w and h, so it does not observe the container.
The container's CSS controls canvas alignment and whether overflow is visible or clipped.
Each player handle is one-shot. The first accepted playWorkerAnimationAsync call resolves true
after setup is queued; another call on the same handle resolves false, including while a URL is
still loading. Use onFirstRender when the first painted frame matters. A worker load, parse, or
render failure invokes onError and disposes the player. The callback deliberately carries no error
string so production delivery does not pay for diagnostic text; applications should show their
normal static fallback and may log the animation URL themselves.
When animationSource is a URL, external image assets (u + p) resolve relative to the final
JSON response URL, including after redirects. A parsed document has no source URL, so its external
image paths must already be absolute; embedded data: images work in either form.
Compile compatible vector content before shipping the binary player:
npx babylon-lottie-compile animation.json animation.binimport { createBinaryWorkerPlayer, playWorkerAnimationAsync } from "@babylonjs/lottie-player/binary";
const player = createBinaryWorkerPlayer();
await playWorkerAnimationAsync(player, {
container: document.getElementById("animation")!,
animationSource: "./animation.bin",
});The compiler also exports compileLottieBinary, compileLottieFile, compileLottieProgram,
encodeLottieProgram, and getCompiledLottieUnsupportedReasons from
@babylonjs/lottie-player/compiler. Use the unsupported-reasons API in asset pipelines that need
to route incompatible documents to the full JSON player.
Explicit worker URL
Use /standalone when the host serves a prebuilt worker itself. Pair each constructor with its
same-origin worker URL:
import { createLottieWorkerPlayer, playWorkerAnimationAsync } from "@babylonjs/lottie-player/standalone";
const player = createLottieWorkerPlayer({ workerUrl: "/static/workers/full.worker.js" });
await playWorkerAnimationAsync(player, {
container: document.getElementById("animation")!,
animationSource: "./animation.json",
});For a binary animation, call createBinaryWorkerPlayer with workers/binary.worker.js and pass its
URL or an ArrayBuffer/Uint8Array source.
Classic-script hosts use the equivalent LiteLottie global, which exposes both constructors. The
global name is intentionally stable for existing classic-script hosts.
<script src="./standalone.min.js"></script>
<script>
const player = LiteLottie.createLottieWorkerPlayer({ workerUrl: "./workers/full.worker.js" });
LiteLottie.playWorkerAnimationAsync(player, {
container: document.getElementById("animation"),
animationSource: "./animation.json",
});
</script>Fixed binary splash
For first-paint-critical, fixed-layout binary splashes, the package also ships a paired classic script and binary worker:
fixed-binary-splash/client.min.jsfixed-binary-splash/worker.min.js
This client auto-starts under a deliberately narrow HTML contract:
<canvas id="l" data-a="animation.bin" style="width: 512px; height: 512px"></canvas>
<script src="./fixed-binary-splash/client.min.js"></script>Copy the binary animation asset beside the two scripts. The client derives their directory from
document.currentScript, loads worker.min.js, and stores the worker on canvas._w for the host's
later dismissal path to terminate.
This path is an auto-executing classic script, not an ESM API. It assumes the host has already gated
on OffscreenCanvas, provides stable nonzero canvas dimensions, keeps an independent fallback, and
owns worker termination. It intentionally omits capability checks, resize observation, callbacks,
inline animation objects, variables, configurable looping, and error UI. Use the regular bundler or
standalone entries when those capabilities are needed.
Architecture
Rendering pipeline
Per frame, the player walks layers back-to-front, resolves each layer's world matrix (parent chain applied), and dispatches to a renderer keyed by layer kind:
- Fill (
rendering/fill-renderer.ts) — the core. Vector fills via stencil-then-cover: a stencil pass marks pixel coverage (nonzero winding emulated with two cull passes — back facesINCR_WRAP, front facesDECR_WRAP, since lite-gl's stencil is single-sided), then a full-screen cover pass paints the covered region. Also handles solids, linear/radial gradients, strokes (tessellated to geometry inrendering/stroke-geometry.ts), add-mode masks, and opaque vector alpha track mattes. Masks and mattes reserve one stencil bit each, so their intersection needs no texture or extra framebuffer. MSAA comes free from the antialiased default framebuffer. - Text (
rendering/text-renderer.ts) — rasterizes each text block once on anOffscreenCanvas(so it works on the main thread and in a worker) and draws it as a premultiplied textured quad. - Image (
rendering/image-renderer.ts) — decodes each asset vialoadTexture2D(fetch+createImageBitmap, worker-safe) and draws a textured quad.
Module map (src/)
| Area | Files |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Public entries | index.ts (.), binary.ts (./binary), standalone.ts (./standalone), compiler/compiler.mjs (./compiler) |
| JSON model | animation/lottie-raw.ts, animation/parse.ts, animation/sample.ts, animation/matrix.ts, animation/geometry.ts |
| JSON player | player/player-core.ts, player/full-player.ts |
| Binary player | binary/format.ts, binary/player.ts, client/binary-client.ts, worker/binary.worker.ts |
| Rendering | rendering/fill-renderer.ts, rendering/stroke-geometry.ts, rendering/text-renderer.ts, rendering/image-renderer.ts, rendering/layer-renderer.ts, rendering/gl-frame.ts |
| Worker delivery | client/runtime.ts, client/blob-worker.ts, client/default-worker.ts, worker/protocol.ts, worker/full.worker.ts |
Worker (src/worker/)
The production off-thread path has one worker per source format:
| File | Role |
| ------------------ | ---------------------------------------------------------------------------------------------------------------- |
| protocol.ts | Shared lifecycle messages plus distinct inline JSON and binary payloads. |
| full.worker.ts | Fetches/parses JSON and drives the full renderer through controller.ts and dispatch.ts. |
| binary.worker.ts | Fetches/decodes binary animations, binds immutable geometry, samples transforms and morphs, and drives playback. |
Worker-safety: fill, stroke, mask, and image renderers are DOM-free. Text was the only worker-unsafe renderer (it used
document.createElement); it now rasterizes on anOffscreenCanvasand uploads viacreateDynamicTexture/updateDynamicTexture, so one code path serves both threads.
Performance benchmark
pnpm measure runs the current renderer core against every tracked visual fixture plus optional
local animations from anims/. Binary fixtures are compiled during staging and benchmark the
resulting binary animation bytes; full fixtures benchmark JSON. ANIMS restricts a run by fixture ID, JSON
filename, or local animation name.
Each fixture runs in a fresh page in headed Chrome. The benchmark deliberately drives the internal
main-thread renderer so RAF CPU time and WebGL draw calls are directly observable. It reports
potential FPS, RAF average and p95, draw calls, initialization time, time to first frame, and JS heap.
Production bundle sizes remain guarded separately by tests/worker-client-size.test.ts.
Results are written to measure/.out/results.json and measure/.out/report.html. To compare a
change, save the first run's JSON outside .out, then set BASELINE_RESULTS to that file for the
second run; the report shows per-metric deltas.
Test fixtures
Local animation JSONs live in the gitignored anims/ directory. Drop any Lottie
*.json files you are licensed to test there and the demo + benchmark pick them up. The demo
attempts to compile each document and enables the binary player only when compilation succeeds. The benchmark
follows each tracked fixture's declared variant.
Visual regression tests
Blocking visual tests use only the hand-authored Apache-2.0 fixtures under
tests/visual/fixtures/. They do not read anims/ and do not depend on the Babylon Assets CDN.
Normal runs build and exercise the published dist standalone API plus its full/binary workers,
then compare their canvases with reviewed goldens:
pnpm build
pnpm test:visualGoldens are generated locally from the installed Babylon.js Lottie player where it supports the
feature, or from the installed lottie-web canvas renderer for masks, mattes, morphs, and images.
They are never generated from the candidate renderer. Only regenerate them for an intentional
fixture/reference change, and inspect every image before keeping it:
pnpm build
pnpm test:visual:updateThe Playwright configuration uses Playwright's pinned full Chromium channel because its ANGLE WebGL2 stencil behavior matches production Chrome; the lightweight headless shell does not render this stencil pipeline reliably. CI installs that pinned Chromium build and uses SwiftShader for a deterministic software-GPU run; local golden review uses the machine's real GPU.
Demo tooling (demo/)
serve.mjs— live viewer (pnpm demo, port 5180). Dropdowns for Animation, Renderer (Full JSON / Binary), and Thread (Main / Worker).build.mjs/_shared.mjs— one-shot PR snapshot build and shared esbuild configuration. They bundle the viewer plus the two worker entries.
For a static hosted build, exclude gitignored local animations so they are not copied into the deployment directory:
$env:DEMO_INCLUDE_LOCAL_ANIMS = "false"
pnpm build:demoStatus
Full JSON: fills, solids, linear/radial gradients with animated colors, butt/round-cap strokes with round joins, independent gradient opacity stops, add-mode masks, opaque vector alpha track mattes, text, images, morphing paths, hidden layers, nested group transforms, MSAA, z-order, comp clipping, and parent transforms — in the internal main-thread harness and public worker delivery.
Binary: shape and null layers, layer transforms and parenting, animated nested group transforms, fixed-topology GPU morphs, solid and static gradient fills, static compound nonzero fills with holes, add masks, opaque alpha mattes, and the compiler's constrained static stroke profile. The compiler reports and rejects unsupported content; it is not a generic replacement for every shape-only Lottie document.
Not yet supported by full JSON: mask subtract/intersect/inverted/feather, inverted/luma/fractional-alpha
track mattes, square stroke caps, non-round stroke joins, even-odd fill rules, time stretch,
precompositions, mergePaths, trimPath, per-glyph text animators, and gradient strokes.
The source manifest remains private to prevent accidental publication from the repository root.
The release pipeline builds and publishes the generated dist/ package, including root,
./binary, ./compiler, ./standalone, the classic standalone client, the fixed binary splash
pair, and both prebuilt workers.
