mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
`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`.
102 lines
5.7 KiB
Markdown
102 lines
5.7 KiB
Markdown
# 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.
|