Files
craft-libs/crates/lottie/README.md
Dylan M. Taylor bd79e32630 craft-lottie: the document model, with a bounded reader and a writer (#938)
`craft_lottie::model` is the Lottie document as the file has it: one
struct per object of the lottie-spec 1.0 object model (animation, assets,
layers by type, transform, masks, effects, shape items, text, markers,
fonts, glyphs), one field per key. `Animation::read` reads within
`Limits` (bytes, nesting, layers, assets, shape items); `to_json` writes.

Keys the model does not name, values of another type than expected, and
layers and shape items of an unknown type stay in `extra` and are written
back, so a document survives a read and a write. Animatable properties
stay JSON (`Animated`) and are read with `read_prop`.
2026-11-11 18:47:08 -05:00

102 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# craft-lottie
Lottie (Bodymovin) JSON: the document as the file has it, and the encodings its values use. Bytes
in, a neutral model out, and back. Which layer, shape, effect and text feature becomes what in an
application's document is the application's work.
No file access: callers pass bytes, so the crate builds for wasm32.
## The document model
`Animation::read(bytes)` gives the document; `to_json` and `to_vec` write it. The model is the
lottie-spec 1.0 object model, one struct per object and one field per key:
- `Animation`: version, frame rate, frame range, size, `assets`, `layers`, `markers`, `fonts`,
`chars`, `meta`.
- `Asset`: an image (`dir`, `path`, `embedded`) or a precomposition (`layers`).
- `Layer`: the keys every layer has (name, `index`, `parent`, timing, blend mode, `transform`,
`masks`, `effects`, matte keys) and `content`, by `ty`: `Precomp`, `Solid`, `Image`, `Null`,
`Shape`, `Text`, or `Other` for a type the model has no struct for.
- `Shape`: the keys every shape item has and `kind`, by `ty`: group, rectangle, ellipse, polystar,
path, fill, stroke, gradient fill and stroke, transform, trim, round corners, repeater, merge,
pucker and bloat, twist, zig-zag, offset path, or `Other`.
- `Transform`, `Mask`, `Effect` (an effect and its values), `Marker`, `Fonts`, `Font`, `Char`.
- `TextData`: the documents over time (`TextDocument`), range animators (`TextRange`,
`TextSelector`, `TextStyle`), alignment and path options.
Every field is an `Option`: files leave keys out, and a writer sets what it needs. An animatable
property is an `Animated`, the property's JSON; `Animated::read` gives its value or keyframes (see
below). What a value means depends on the property, so the model does not decode values.
Nothing a file holds is dropped because the model does not know it. A key the struct does not
name, and a key whose value has another type than the struct expects, stay in the struct's `extra`
and are written back. A layer or shape of an unknown `ty` keeps its `ty` and its keys. So a
document read and written back is the same JSON, with three exceptions: an entry of a list that is
not an object where objects are expected is left out, a whole number written `30.0` comes back
`30`, and a flag written `1` or `0` where the schema has a boolean comes back `true` or `false`.
`Limits` bounds what is read: bytes of JSON (256 MiB), nesting (128 levels, which is also where
`serde_json` stops), layers of the document and its precompositions together (100,000), assets
(20,000) and shape items (2,000,000). Passing one is `LottieError::TooLarge`. No reader panics on
any input.
## The encodings
These work on `serde_json` values, for the values the model leaves as JSON.
| Part | Read | Write |
|---|---|---|
| Document JSON and header (`w`, `h`, `fr`, `ip`, `op`, `nm`) | `parse_document`, `Header::read` | the application builds the JSON |
| Animatable property (`{a, k, x}`) and keyframes | `read_prop` | `write_prop`, `write_keys` |
| Ease of a segment (`o`, `i`) and speed / influence | `Tangents::read`, `to_speed` | `Tangents::from_speed` |
| Bezier path (`{c, v, i, o}`) | `Bezier::from_json` | `Bezier::to_json` |
| Gradient stop list (`g.k`) | `Gradient::from_flat` | `Gradient::to_flat` |
| Numbers, colours, `#rrggbb` | `nums`, `color`, `hex_color` | `round6`, `to_hex` |
| Embedded images (`data:` URIs, RFC 4648 base64) | `base64::data_uri` | `base64::to_data_uri`, `image_mime` |
Not here: dotLottie (`.lottie`), which is a ZIP archive around the same JSON, and layer styles
(`sy`), which lottie-spec 1.0 does not define (they stay in a layer's `extra`).
## What the readers accept
Files in the wild come from many Bodymovin versions and other exporters, so the readers are
tolerant where players are:
- a property that is a bare value (`"o": 100`), a property without `a` whose `k` is a key list,
and a key value wrapped once more (`[[x, y]]`);
- keys before 5.0: a segment ends with `e` and the last key has no `s`; the value of a key is
then the `e` of the key before;
- ease tangents whose `x` and `y` are a number or a list, per dimension; a missing `x` is a
third of the way and a missing `y` is linear;
- a path without `c`: Bodymovin 3.x and early 4.x keep the flag outside the path (`cl` on a
mask, `closed` on an `sh` item), so the caller passes it; tangent lists shorter or longer than
the vertex list;
- colours written 0..=255 instead of 0..=1;
- a gradient list without alpha pairs, and a colour count larger than the list.
Nothing is indexed by a number from the file, and no reader panics.
## Ease
A Lottie keyframe holds the start value of a segment and the segment's normalised cubic Bezier
ease: `o` (out of this key) and `i` (into the next), each with `x` (time fraction) and `y` (value
progress fraction), per dimension. After Effects' temporal ease is speed (units per second) and
influence (fraction of the segment). For a segment of duration `d` and value change `Δv`:
```text
o.x = influence_out o.y = speed_out · influence_out · d / Δv
i.x = 1 − influence_in i.y = 1 − speed_in · influence_in · d / Δv
```
`Tangents::from_speed` and `Tangents::to_speed` are this formula and its inverse. A spatial
property eases along its motion path: one dimension, with `Δv` the arc length, which the caller
computes.
## Sources and clean room
Implemented from the public Lottie format description (the lottie-spec 1.0 schema and the
LottieFiles format documentation); no player or exporter source was used. The encodings come from
EffectCraft's `effectcraft-lottie` crate; the document model is written here from the same
description. The four fixtures in `tests/fixtures` are hand-written
JSON (original work), not exported from any product.