@px-petals/roku.motion-web 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/audio/audio.cues.js +27 -0
- package/audio/audio.js +259 -0
- package/bands/bands.js +392 -0
- package/bundle/bundle.fonts.js +55 -0
- package/bundle/bundle.js +131 -0
- package/focus/focus.direction.js +46 -0
- package/focus/focus.dom.js +158 -0
- package/focus/focus.js +277 -0
- package/focus/focus.tree.js +146 -0
- package/hit/hit.geometry.js +113 -0
- package/hit/hit.js +129 -0
- package/list/list.flex.js +181 -0
- package/list/list.items.js +80 -0
- package/list/list.js +518 -0
- package/lottie/lottie.js +124 -0
- package/lottie/lottie.layers.js +416 -0
- package/lottie/lottie.paint.js +238 -0
- package/lottie/lottie.paths.js +474 -0
- package/lottie/lottie.props.js +265 -0
- package/lottie/lottie.shapes.js +259 -0
- package/mixer/mixer.js +307 -0
- package/mixer/mixer.mix.js +107 -0
- package/mixer/mixer.stage.js +431 -0
- package/motion/motion.convert.js +404 -0
- package/motion/motion.flow.js +421 -0
- package/motion/motion.keys.js +150 -0
- package/motion/motion.machine.js +382 -0
- package/motion/motion.nested.js +345 -0
- package/motion/motion.values.js +368 -0
- package/motion.js +1155 -0
- package/package.json +47 -4
- package/player/overlay/overlay.js +325 -0
- package/player/overlay/overlay.layout.js +208 -0
- package/player/player.fit.js +101 -0
- package/player/player.js +421 -0
- package/player/player.raster.js +127 -0
- package/pointer/pointer.js +292 -0
- package/readme.md +345 -0
- package/script/luau/lib/base/base.js +202 -0
- package/script/luau/lib/base/base.table.js +189 -0
- package/script/luau/lib/lib.args.js +152 -0
- package/script/luau/lib/lib.js +23 -0
- package/script/luau/lib/math/math.bit32.js +144 -0
- package/script/luau/lib/math/math.js +288 -0
- package/script/luau/lib/math/math.vector.js +125 -0
- package/script/luau/lib/string/string.format.js +143 -0
- package/script/luau/lib/string/string.js +160 -0
- package/script/luau/lib/string/string.pattern.js +505 -0
- package/script/luau/load/load.js +362 -0
- package/script/luau/load/load.ops.js +79 -0
- package/script/luau/luau.convert.js +259 -0
- package/script/luau/luau.js +132 -0
- package/script/luau/luau.md +111 -0
- package/script/luau/vm/vm.js +167 -0
- package/script/luau/vm/vm.meta.js +401 -0
- package/script/luau/vm/vm.num.js +236 -0
- package/script/luau/vm/vm.run.js +842 -0
- package/script/luau/vm/vm.value.js +406 -0
- package/script/play/play.js +199 -0
- package/script/play/play.objects.js +295 -0
- package/script/play/play.scope.js +91 -0
- package/script/rive/api/api.statics.js +31 -0
- package/script/rive/api/api.type.js +188 -0
- package/script/rive/api/artboard/artboard.js +89 -0
- package/script/rive/api/assets/assets.js +72 -0
- package/script/rive/api/data/data.value.js +99 -0
- package/script/rive/api/draw/draw.contours.js +163 -0
- package/script/rive/api/draw/draw.measure.js +196 -0
- package/script/rive/api/draw/draw.paint.js +235 -0
- package/script/rive/api/draw/draw.path.js +153 -0
- package/script/rive/api/draw/draw.renderer.js +117 -0
- package/script/rive/api/math/math.color.js +96 -0
- package/script/rive/api/math/math.mat2d.js +180 -0
- package/script/rive/api/math/math.vector.js +121 -0
- package/script/rive/api/media/media.audio.js +113 -0
- package/script/rive/api/media/media.draw.js +73 -0
- package/script/rive/api/media/media.image.js +61 -0
- package/script/rive/api/media/media.mesh.js +50 -0
- package/script/rive/api/pointer/pointer.event.js +64 -0
- package/script/rive/api/pointer/pointer.route.js +69 -0
- package/script/rive/api/text/text.font.js +129 -0
- package/script/rive/api/text/text.js +278 -0
- package/script/rive/api/text/text.layout.js +259 -0
- package/script/rive/context/context.js +76 -0
- package/script/rive/context/context.memory.js +196 -0
- package/script/rive/context/context.viewmodel.js +302 -0
- package/script/rive/context/lists/lists.js +160 -0
- package/script/rive/context/lists/lists.values.js +281 -0
- package/script/rive/host/host.globals.js +44 -0
- package/script/rive/host/host.js +533 -0
- package/script/rive/host/host.node.js +45 -0
- package/script/rive/rive.js +23 -0
- package/script/rive/rive.md +180 -0
- package/script/script.js +11 -0
- package/skin/skin.deform.js +514 -0
- package/skin/skin.ik.js +260 -0
- package/skin/skin.js +297 -0
- package/README.md +0 -3
package/readme.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# motion web
|
|
2
|
+
|
|
3
|
+
Plays a Motion bundle on a 2D canvas, for browsers that can't run Rive's own runtimes: those need WebAssembly (Chrome 57 and Safari 11 onward, both 2017), and the WebGL2 one also needs WebGL2, which Safari only gained in iOS 15. It's the web counterpart of [runtime/roku](../roku/readme.md): the same bundles, state machine, live data, keys and text.
|
|
4
|
+
|
|
5
|
+
- **No dependencies:** 50 KB minified, 18.7 KB gzipped, bundled for ES2015. The pose mixer added 8.8 KB and 3.1 KB gzipped to the 32 KB and 12.5 KB before it; fit, layout bands and nested machines 1.1 KB gzipped.
|
|
6
|
+
- **Draws vector** from each animation's Lottie with its own canvas renderer, or from the bundle's raster sheets.
|
|
7
|
+
- **Plays Rive's state machines whole:** layers, 1D and direct blend states, cross-fading transitions and exact exit times, by [mixing animations](#layers-blends-and-cross-fades).
|
|
8
|
+
- **Older browsers:** only 2D canvas, Promise and XMLHttpRequest are needed. The source avoids built-ins older WebViews lack (`fetch`, `Map`, `Path2D`, `async`); `.tests/compat.test.js` holds it to that. A host's Babel lowers the syntax.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { createMotion } from '@px-petals/roku.motion-web'
|
|
14
|
+
|
|
15
|
+
const motion = createMotion(canvas)
|
|
16
|
+
motion.on('event', (name) => console.log('fired', name))
|
|
17
|
+
await motion.load('https://<server>/b/<source>/<options>/bundle/motion.json', { inputs: { score: 3 } })
|
|
18
|
+
|
|
19
|
+
motion.setInputs({ score: 4 }) // view-model values; an Event input set to true fires
|
|
20
|
+
motion.setInputs({ 'player.score': 4 }) // a nested view model's property, by dotted path
|
|
21
|
+
motion.setInputs({ player: { score: 4 } }) // the same, written as an object
|
|
22
|
+
motion.setInputs({ mode: 'on', picture: 'stripes' }) // an enum by its value's name, an image by its asset's name
|
|
23
|
+
motion.setProperties([ { key: 'fill', value: '4294901760' } ]) // { key, value } strings, typed by the bundle
|
|
24
|
+
motion.key('enter') // or 'up', 'shift+tab', a Roku name ('OK', 'back'), or a KeyboardEvent; see Focus and keys
|
|
25
|
+
motion.control('pause') // play, loop, pause, stop
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A bundle is what [lottie-roku](../../bin/lib/lottie/readme.md) writes, and what the [server](../../server/readme.md)'s `bundle` output is. Size the canvas with CSS and call `motion.resize()` when its displayed size changes; the animation is fitted into it by its bundle's fit and alignment: `contain`, centred, unless the bundle says otherwise ([below](#fit-alignment-and-layout)).
|
|
29
|
+
|
|
30
|
+
### Options
|
|
31
|
+
|
|
32
|
+
| Option | Default | Meaning |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `renderer` | `auto` | `auto` draws vector and moves an animation whose frames take over 20 ms to its raster sheets; `vector` and `raster` force one |
|
|
35
|
+
| `clicks` | `true` | A tap or click on the canvas clicks the animation, as OK does on Roku: the Click listeners with no target hear a click anywhere on it, and those with a target hear a click on that target. `false` turns both off; the other pointer listeners answer either way |
|
|
36
|
+
| `fit` | the bundle's, else `contain` | How the animation fits the canvas, over the bundle's: `contain`, `cover`, `fill`, `fitWidth`, `fitHeight`, `scaleDown`, `none` or `layout`; `load(url, { fit })` sets it too |
|
|
37
|
+
| `alignment` | the bundle's, else centred | Where it sits in the canvas: a Rive alignment name (`topLeft`, `bottomCenter`, …) or `[x, y]` from -1 (left, top) to 1 (right, bottom) |
|
|
38
|
+
| `keys` | `'auto'` | `auto`: the canvas can be given DOM focus (it gets a `tabIndex`) and, while it has it, its keys go to the bundle's [focus and keyboard listeners](#focus-and-keys). `manual`: it listens to no keys; the host routes them with `motion.key`, `focus`, `blur` and `click` |
|
|
39
|
+
| `muted` | `false` | Play no sound. `motion.setMuted(flag)` changes it, `motion.muted` reads it |
|
|
40
|
+
|
|
41
|
+
### Events
|
|
42
|
+
|
|
43
|
+
| Event | Detail |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `loaded` | The bundle is ready and its first state has started |
|
|
46
|
+
| `state` | The state entered |
|
|
47
|
+
| `playback` | `playing`, `paused`, `stopped`, `finished` |
|
|
48
|
+
| `finished` | The state's animation played to its end |
|
|
49
|
+
| `values` | Every input's value (an enum as its name), after the machine's own actions and any [write-back](#data-binding) from the animation |
|
|
50
|
+
| `event` | A custom event the machine fired, or `openUrl:<url>` |
|
|
51
|
+
| `focus` | Rive's focus moved: the detail is the focused target's `rid`, or null when none is |
|
|
52
|
+
| `audio` | A sound started; the detail is its id in the bundle |
|
|
53
|
+
| `error` | Why a load or a state failed, or why a sound did not play |
|
|
54
|
+
|
|
55
|
+
`motion.state`, `motion.values`, `motion.playback` and `motion.renderer` read the same at any time, and so do `motion.placement` (`{ fit, alignment }`, as they stand) and `motion.band` (the layout band showing, by its width; 0 for none). `motion.artboard` is `{ width, height, u, v }`: the artboard's size where it is the canvas's (0 where the animation's own plays) and the weights toward the neighbouring layout bands.
|
|
56
|
+
|
|
57
|
+
## Fit, alignment and layout
|
|
58
|
+
|
|
59
|
+
[Rive's fits](../../bin/lib/rive/readme.md#fit-alignment-and-responsive-layouts) scale the artboard into the canvas, and the alignment puts what is left over (`player/player.fit.js`; its tests are in `player/.tests`):
|
|
60
|
+
|
|
61
|
+
| Fit | Scale |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `contain` | The tighter side, so it all shows |
|
|
64
|
+
| `cover` | The looser side, so the canvas is full and the canvas cuts the rest |
|
|
65
|
+
| `fill` | Each axis on its own, stretching it |
|
|
66
|
+
| `fitWidth`, `fitHeight` | That side's ratio on both |
|
|
67
|
+
| `none` | 1 artboard unit to a CSS pixel |
|
|
68
|
+
| `scaleDown` | `contain`, never above 1 CSS pixel to a unit |
|
|
69
|
+
| `layout` | The layout band for the canvas (blended with the next where the converter says it can be), then `contain` |
|
|
70
|
+
|
|
71
|
+
- **A responsive artboard** (`fit: "layout"` in its bundle, with `bands`) plays the widest of its layout bands that the canvas's CSS width has room for, or the narrowest when none does, and scales it to the canvas; when the layout depends on the height (`grid`), the band is also the tallest row the canvas's CSS height has room for (`bands/`: the canvas falls in a **cell** of the grid, between four corners). **`motion.resize()` picks again** and swaps in the other band's variant of the animation that is playing, at the same moment, without restarting it or touching the machine. The converter writes the bands and the grid, and says [what is approximate between them](../../bin/lib/rive/readme.md#fit-alignment-and-responsive-layouts).
|
|
72
|
+
- **Between two bands that blend** (`lerp: true`, which the converter sets where nothing reflows between them), the canvas draws the layout of its own width: each band's variant is mixed with the next band's by `u = (w - w0) / (w1 - w0)` (`bands/`), and the artboard is as wide as the canvas, as Rive's `layout` fit makes it, so the picture fills the canvas width the way Rive's does. It is a property by property mix of the two Lotties by their `rid` tags, with one weight shared by every band (`bands/bands.js`), so it costs one extra evaluation per laid-out property and nothing else: a plain bundle draws it as one animation, a bundle with the mixer as the poses' sources, and it is the same on the manual clock (no animation frame is asked for when `manual` is set). A reflow is never blended: the converter puts a band either side of it. Pointer hits follow the blended picture.
|
|
73
|
+
- **Between four corners of a cell** (`cell: true`, on a bundle whose layout depends on the height), the canvas draws the layout of its own width **and height**: the cell's four variants are mixed bilinearly, weighted `(1-u)(1-v)`, `u(1-v)`, `(1-u)v` and `uv`, `v = (h - h0) / (h1 - h0)`, through the same hook: each corner's property gets an evaluator that mixes the neighbours' values by the two live weights (`t` is `u`, and `v`), so it is still one animation, or the mixer's poses. Along a side of a cell that only blends one way (`lerp` or `rise`) it is the two corners of that side; where the converter did not mark the cell, the corner plays, scaled. The artboard is the canvas's size in each direction that blends, and a layout that does not depend on the height (no `grid`, but `heightRange`) is the canvas's height within the range the converter solved, so its content sits at the top as Rive's does rather than centred in the letterbox. A list's container box is the same bilinear mix of its corners' boxes, so the rows follow. **Limits:** text overlays take the lower band's box; the raster renderer plays the lower band's sheets; past the last band or row the edge cell scales (and below the first, the first), as Rive's layout would not be solved there.
|
|
74
|
+
- **With the mixer,** each band has a mixer of its own variants of the animations, all on the one stage, so a band swap mid-fade or mid-blend carries on with the layers' times and weights (`mixed-nested` and `layout-mixed` in the browser tests). Nested machines advance in `onTick` beside the stage, and keep going once it has run out.
|
|
75
|
+
- **Another fit plays the file's own size,** the bands not used, and `layout` on a bundle without bands is `contain`.
|
|
76
|
+
- **Canvas pixels:** `none` and `scaleDown` count CSS pixels, so the animation keeps its size on a dense screen; Rive's own runtimes count the drawing buffer's.
|
|
77
|
+
|
|
78
|
+
## Nested artboards
|
|
79
|
+
|
|
80
|
+
A bundle's `nested` lists the nested artboards that run a state machine of their own (a `NestedStateMachine`, or a playing `NestedSimpleAnimation`, which is a machine of one state). Each runs through the same flow as the bundle's machine (`motion/motion.nested.js`), on the player's clock, and moves its precomp layer's time remap: the layer's `nid` names the slot the renderer reads it from. Its clock is its own, at the instance's `speed`: what the host's animation does (it loops, finishes, mixes with others, cross-fades) does not move it. A host whose Lottie is stretched to hold the nested animation (for a player without machines) loops and ends at the length of its own keys (`own`), which `bundle/` reads as the animation's `frames`.
|
|
81
|
+
|
|
82
|
+
- **Inputs:** a nested machine starts with the values its `NestedBool` and `NestedNumber` set, and takes the inputs of the bundle that share its inputs' names (`setInputs`, `setProperties`).
|
|
83
|
+
- **Bound to the host's view model.** A nested artboard whose placement names a property of the host's view model (`dataBindPathIds`) is driven by that instance: the slots inside it are the host's own (`gauge.level`, filled as any slot is, converters included), one nested artboard placed twice follows two instances, and its machine's view-model conditions, blends and listener actions read and set the host's properties by the same dotted names. What such a machine sets (`shared` in `nested`) is written into the host's values (`values` changes, and the event follows), as a write-back is, and the other nested artboards that share the property take it.
|
|
84
|
+
- **Pointer listeners** inside a nested artboard are hit-tested like the host's: the renderer names the groups a nested layer draws `<nid>/<rid>` while it draws it, the pointer module watches those names, and a hit goes to that machine (`nested.pointer`). They follow the group that moves the artboard, its transform and the fit. Several levels deep, each machine's listeners are its own.
|
|
85
|
+
- **In a list row,** a nested artboard runs as in the host: the row's machines are the row's `nested`, run beside the row's own on the row's clock and written into the row's slots, so a row's spinner keeps turning after the row's own animation ends. Its pointer listeners work too: each row records its own shapes as it draws, a target is named by the row (`<row>|nest.1/2`), a press goes to that row's machine, and rows that come and go keep a press on the others (`pointer.listen` keeps the state of a target it watches again).
|
|
86
|
+
- **They keep running when the outer animation finishes,** so a bundle whose only motion is nested artboards stays alive; pausing or stopping pauses them too.
|
|
87
|
+
- **Several layers** play at once, as the host's do: each nested machine with more than one layer keeps a stage of its own (`mixer/mixer.stage.js`), the time remap follows the first layer, and the poses of all go in the slot `pz:<id>`, which the properties that carry `pz` (the ones a segment keys) mix by weight (`lottie/lottie.props.js`). Cross-fades and blends between a nested machine's states work as the host's.
|
|
88
|
+
- **Text and images** inside a nested artboard: a text layer the converter kept in the asset (the live ones) is drawn at its layer's matrix with the slot's words (`drawDocument` in `player/overlay/overlay.js`), in the bundle's fonts; a bound image picks the asset its layer names, as the host's does.
|
|
89
|
+
- **Layouts:** a nested artboard laid out at the size of its place changes with the canvas as the host's boxes do: its clip (`wh`) and the properties inside it are tagged for the layout bands and the height grid's cells to mix.
|
|
90
|
+
- **Draw order:** a nested artboard draws between its siblings, as the converter cut the host's layers; one that does not clip (`nc`) draws past its own edge.
|
|
91
|
+
- **Not run:** `NestedBool` or `NestedNumber` values the outer timeline animates; a machine's view-model conditions, listeners and nested artboards under a placement that owns its instance (`isStateful`; what it sets is baked in or follows the host property its value names); a pointer listener on the whole nested artboard (Rive offers none).
|
|
92
|
+
|
|
93
|
+
## Lists
|
|
94
|
+
|
|
95
|
+
A bundle's `lists` (from a Rive `ArtboardComponentList`) says how to draw a view-model list property as one instance of a row artboard per item, each bound to its own view-model instance. `list/` does it; the format is in [the bundle's `lists`](../../bin/lib/lottie/readme.md#motionjson).
|
|
96
|
+
|
|
97
|
+
- **Rows:** each item gets its own compiled copy of the row's Lottie, so its slots are its own: the item's instance (or its own values over the row view model's default instance) goes through the row's binds and converters into that copy's slots. A row is cut off at its own edge, as an artboard is, and drawn over the animation, after it.
|
|
98
|
+
- **Layout:** no Yoga in the browser; `list/list.flex.js` places the rows, which are boxes of the row artboard's size, in the container's box (the band's, when the artboard is responsive) by the container's direction (either one reversed), gap, padding, wrap and main and cross alignment, and a container that hugs takes its rows' size. Rows that do not fit overflow, as in Rive when they cannot shrink.
|
|
99
|
+
- **Host API:** `setInputs({ rows: [ ... ] })` or `setProperties([ { key: 'rows', value: '[ ... ]' } ])` (the value is JSON) replaces the items; rows come and go live, and `values` and the `values` event report the list. An item is `{ viewModel?, instance?, values? }`:
|
|
100
|
+
- `{ instance: 'Two' }` is a named instance of the row's view model, `'Two'` the same
|
|
101
|
+
- `{ values: { tint: '#FF66BB6A', size: 200 } }` is a blank instance (the default one's values) with these set; a property's name may stand in the item itself (`{ size: 200 }`)
|
|
102
|
+
- `viewModel` picks the row artboard when a list has several; it defaults to the first
|
|
103
|
+
- **Rows are kept by position:** a row whose item is unchanged stays as it is, state machine included; an item that differs from what was at its place makes a new row, which starts from its first frame.
|
|
104
|
+
- **Machines:** a row with a state machine runs it for each row, with the nested-machine runner (`motion/motion.nested.js`) on the player's clock; its inputs are the item's values by name. Rows keep the clock running, as nested machines do.
|
|
105
|
+
- **Not done:** text inside a row that is not in a nested artboard (text layers are overlays, which only the animation has); the list's own transform and opacity; a list beside other layout children in its container (the rows are laid out as if alone in it; the converter warns); a row that fills its slot instead of having a fixed size; drawing rows under the `raster` renderer or over the raster fallback (`auto` keeps a bundle with lists on vector); changing one row's value by path (`rows.1.size`), which a host does by setting the list.
|
|
106
|
+
|
|
107
|
+
## How it matches Roku
|
|
108
|
+
|
|
109
|
+
| | Roku (`runtime/roku`) | Web |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| Vector | `AnimatedImage` | `lottie/`: this package's canvas renderer |
|
|
112
|
+
| Raster | Posters stepping through sheets | `player/player.raster.js`: `drawImage` per frame |
|
|
113
|
+
| Live colour on sheets | Poster `blendColor` on white masks | `source-in` fills over the same masks |
|
|
114
|
+
| Live data in vector | Motion.Task patches the Lottie's slots and reloads it | The renderer reads slot values every frame; nothing reloads |
|
|
115
|
+
| Text | Labels moved by Animations | `player/overlay/`: canvas text along the same tracks. Runs flow together, wrapping at the box width with `measureText` (words, then characters for a word wider than the box), aligned across the box and down its height. Wrap points follow the browser's metrics, not Rive's shaping. Letter spacing draws character by character. A live value replaces the first run |
|
|
116
|
+
| State machine | `motion/state`, `motion.flow.bs` | `motion/motion.machine.js`, `motion.flow.js`, ported line for line, and extended for layers and timed transitions |
|
|
117
|
+
| Layers, blends, cross-fades | The first layer, the animation nearest a 1D blend's value (a direct blend's first), instant switches ([the limit](../roku/readme.md#what-rive-does-that-roku-does-not)) | `mixer/`: the pose mixer |
|
|
118
|
+
|
|
119
|
+
## Data binding
|
|
120
|
+
|
|
121
|
+
Values are the bundle's view-model properties by name, each one's [converters](../../bin/lib/lottie/readme.md#data-converters) applied on the way to its slot. `setInputs`, `setProperties`, `start.inputs` and the `values` event all use the same names.
|
|
122
|
+
|
|
123
|
+
- **Nested view models** are dotted paths: `player.score`. `setInputs` also takes the nested object (`{ player: { score: 4 } }`), which it flattens; `setProperties` takes the dotted key, typed by the property it names. A nested view model's default values come from its default instance.
|
|
124
|
+
- **Converters** apply when a bound value changes: the range, round, operation, formula, string, enum and list converters of the manifest's descriptors (`motion/motion.convert.js`), and a descriptor type this player does not know passes the value through with one `error` event saying so. A `script` descriptor runs the script (see [Scripts](#scripts)). A text overlay shows its slot's converted text (a number through a `toString` converter reads `72.6`).
|
|
125
|
+
- **Enums** are held as the index of their value, which the machine's guards compare. `setInputs` takes the name (`'on'`) or the index; `motion.values` and the `values` event give the name. A text slot bound to an enum shows its name.
|
|
126
|
+
- **Images:** a bound image property picks the asset an image layer draws. `setInputs({ picture: 'stripes' })` takes the asset's name (or its Lottie id, `image_1`, or its index); the layer draws that image at its own size, about the same fraction of it as the authored one. Every image of the file is in each animation that has a bound image, loaded up front. Raster sheets show the default image.
|
|
127
|
+
- **Lists** are drawn: see [Lists](#lists). A list property holds its items (`[ { viewModel: 'Row', instance: 'One' }, ... ]`) in `values` and in the `values` event, and `setInputs({ rows: [ ... ] })` sets them.
|
|
128
|
+
- **Write-backs:** where a bind writes a property back into the view model (the toSource direction, or two-way), the player reads that property as drawn at each frame, undoes its converter, and sets the view-model value when it has moved: `values` changes, a `values` event follows, and a guard on it can fire. Only properties on the animation's own layers are read (not those inside a nested artboard's precomp); what a nested machine sets in the host's view model goes straight to the values. A listener or state action that sets a bound property already reflects in `values`.
|
|
129
|
+
- **Skins:** a bundle whose `data.skins` lists skinned paths moved by bound bones has them deformed live (`skin/`). Each bone's binds (`x`, `y`, `rotation` in radians, `scaleX`, `scaleY`, `length`) have a slot's source, so they read the view-model value through the same converters; a bone whose bind has no value keeps its setup pose, and a skin none of whose binds has a value leaves its slot at the setup deform the bundle carries. Whenever any bound value of a skin changes (the default instance at load, a host write, a machine write-back, a nested shared input), the skin is recomputed as Rive does and its path `{ i, o, v, c }` is written into the slot `sid`, so the path draws deformed like any other slot. A skin whose values have not changed keeps its path. The skin code is its own module (`skin/skin.js`), fetched with a dynamic `import()` only for a bundle whose `data.skins` (or a list row's) has skins; the legacy single-script build contains it. A skin makes its rig once (setup values, scratch matrices, the path it writes) and a deform allocates nothing. A node of the skin's `nodes` (bound `x`, `y`, `rotation`, `scaleX`, `scaleY`) is posed with the bones, a root bone with `node` sits under it, and `spaceNode` makes `space` the inverse of its live world; a root bone's own `parentWorld` wins over the skin's. A vertex with `r` has its corner rounded after the deform (two points, whatever the radius). What an animation keys (`keys[animation]`: tracks of bone, node and vertex channels) is played under the bound values: for that animation the slot is a function of the frame (a slot value may be a function the renderer calls with the frame), recomputed only when the frame or a bound value changed. A skin's `ik` lists IK constraints over its bones, `{ tip, count, invert, strength, target, space, binds }`, whose `strength` and target `x`, `y` bind like a bone's: once the bones are posed the chains are solved (`skin/skin.ik.js`, rive-runtime's `IKConstraint`, in preallocated buffers) and the bones below a chain follow, before the deform. A root bone sits at its own `x`, `y` in `parentWorld`; a child bone sits `length` along its parent's x axis. A tendon is the bone's world times its inverse bind; a vertex goes through the `skin` transform, then the weighted sum of its tendons' matrices (a cubic handle, an offset from its vertex, goes through as the point vertex + offset with its own weights, else the vertex's, and is offset again from the deformed vertex), then `space`; a vertex without weights keeps its setup position.
|
|
130
|
+
|
|
131
|
+
## Audio
|
|
132
|
+
|
|
133
|
+
A bundle's [`audio`](../../bin/lib/lottie/readme.md#audio) block plays on `HTMLAudioElement`, present in every browser on the floor (Web Audio is not).
|
|
134
|
+
|
|
135
|
+
- **Preloaded** when the bundle loads: each sound's element starts downloading then. A sound that fails to load is an `error`, not a failed load.
|
|
136
|
+
- **Played** when the machine fires an event the bundle maps to a sound (a state starting, a listener), and when an animation passes a cue's frame, each cue once per pass, again on every loop. A keyed cue's event reaches `event` too, as a fired one does. The `audio` event follows each start.
|
|
137
|
+
- **`control('pause')`** pauses what is sounding and `play` continues it; **`stop`** silences and rewinds it. **`muted`** plays nothing and silences what is sounding.
|
|
138
|
+
- **Overlap:** a sound can sound up to four times at once; a fifth restarts the oldest.
|
|
139
|
+
- **Autoplay:** browsers refuse sound until the page has been touched. The refusal is an `error` event (`sound "x" did not play: the browser plays sound only after the page has been touched`), never an exception; sound after any tap, click or key press plays. A sound due before that is not replayed.
|
|
140
|
+
- **Formats:** whatever the browser decodes; Rive audio is WAV, MP3 or FLAC, and FLAC is not played in browsers before about 2017.
|
|
141
|
+
|
|
142
|
+
## The renderer
|
|
143
|
+
|
|
144
|
+
`lottie/` draws the subset of Lottie that rive2lottie and lottie-roku write: shape layers (groups, paths, rectangles, ellipses, stars and polygons, fills, strokes, linear and radial gradients, dashes, trim paths, merge paths), precomps with time remapping, solids, images, masks, track mattes and blend modes. Text layers are drawn as overlays instead, as on Roku.
|
|
145
|
+
|
|
146
|
+
- **Slots are live:** a property with a slot id reads the slot table first, so `setInputs` costs nothing to draw.
|
|
147
|
+
- **Checked against lottie-web:** `.tests/browser` draws every motion-stress fixture animation at its first, middle and last frames with both renderers and fails above 2% differing pixels; all are under 0.5%.
|
|
148
|
+
|
|
149
|
+
## Pointer events
|
|
150
|
+
|
|
151
|
+
Pointer listeners answer where the pointer is. A listener that watches a shape or group (`target.rid` in the [manifest](../../bin/lib/lottie/readme.md#the-machine)) is hit-tested against that object's geometry in the last frame drawn, so it follows the animation, the mixer's poses, the fit and the canvas's size.
|
|
152
|
+
|
|
153
|
+
| Interaction | Runs when |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `PointerEnter`, `PointerExit` | The pointer moves onto or off the target (or onto or off the canvas, for a listener with no target); also when the target moves under a still pointer, on the next pointer event, and when the pointer leaves the canvas or a touch ends |
|
|
156
|
+
| `PointerMove` | The pointer moves while on the target |
|
|
157
|
+
| `PointerDown`, `PointerUp` | A press or release with the pointer on the target |
|
|
158
|
+
| `Click` | A release on the target that began on it. A click anywhere on the canvas (the DOM `click`) runs the Click listeners that have no target; `motion.click()` and OK run them all |
|
|
159
|
+
|
|
160
|
+
- **Events:** `pointerdown`, `pointermove`, `pointerup`, `pointercancel` and `pointerleave` where the browser has `PointerEvent` (mouse, touch and pen together); otherwise `mousedown`, `mousemove`, `mouseup`, `mouseleave` and `touchstart`, `touchmove`, `touchend`, `touchcancel`, ignoring the mouse events a touch makes up for 800 ms after it. Only the primary button counts. A touch has no hover, so lifting a finger also exits what it was on. Nothing is attached to the canvas unless a bundle has a pointer listener to answer.
|
|
161
|
+
- **Hit test:** while a vector frame draws, each group tagged with a watched `rid` hands the renderer's traced paths (after their trims and merges) and the matrix to canvas pixels to `hit/`. Nothing is flattened until a point is tested, so a watched target costs a few references per frame; a test flattens curves to line segments and applies the nonzero rule in JavaScript. `isPointInPath` would need the paths traced again on the context for every test, and its answer depends on the context's transform; keeping the paths also lets a test run without a canvas.
|
|
162
|
+
- **Pointer to canvas pixels:** the client position is scaled by `canvas.width / its displayed width`, so CSS size, device pixel ratio and `resize()` need no handling.
|
|
163
|
+
- **Limits:** raster frames have no geometry, so an `auto` renderer stays on vector while a bundle has targeted listeners (`raster` hits nothing). A hit tests the shape's geometry, not its fill (a stroke is not widened); a group that draws nothing, because it or a group above it is at opacity 0 (a Solo's hidden child, say), does not hit, and a shape inside a nested artboard is a target of that artboard's own machine only (see [Nested artboards](#nested-artboards)). Rive's click needs a down and up on the same target, as here; its multi-touch is not modelled (the first pointer counts).
|
|
164
|
+
|
|
165
|
+
## Focus and keys
|
|
166
|
+
|
|
167
|
+
Rive's focus model, as [the manifest](../../bin/lib/lottie/readme.md#focus) carries it: focusable targets in Tab order, Tab and the arrows, focus actions, Focus and Blur listeners, and keyboard listeners that hear a key through the focused target and the targets above it. `.tests/parity` measures it against `@rive-app/canvas` (`focus-tab`, `focus-action`, `focus-keys`, `click-focus`, each on its RML and its `.riv`).
|
|
168
|
+
|
|
169
|
+
**Rive's focus is not DOM focus.** Rive's focus says which target of the graphic the keys belong to, and a focus action can move it at any time. DOM focus says whether the page's keys reach the canvas at all.
|
|
170
|
+
|
|
171
|
+
- **Motion never takes DOM focus.** It does not call `focus()` on load, when a Rive focus action runs, or when the host calls `motion.focus`, and it adds no element that could take it. The user or the host gives focus (Tab, a click on the canvas, `canvas.focus()`). Making the canvas focusable with a `tabIndex` is all it does to let that happen, and only when the bundle has a focus tree or keyboard listeners; a click on the canvas gives it focus by the browser's default. Once the canvas has focus, Motion is free to use it, but it has no reason to move it.
|
|
172
|
+
- **Rive's focus routes keys only while the canvas has DOM focus** (`keys: 'auto'`); a `FocusTarget` that runs while it does not still moves Rive's focus (Focus listeners run, the target draws as focused) and the keys wait for DOM focus.
|
|
173
|
+
- **When the canvas loses DOM focus** to another element, Rive's focus is cleared, so Blur listeners run and the target stops drawing as focused, and keys stop being routed; when the window itself loses focus (another tab, another app) Rive's focus is kept for when it returns. This is `@rive-app/canvas`'s own rule.
|
|
174
|
+
|
|
175
|
+
### Automatic (`keys: 'auto'`, the default)
|
|
176
|
+
|
|
177
|
+
The canvas listens to `focus`, `blur`, `keydown` and `keyup` while the bundle has something to answer. Keys are named as Rive names them, from `event.code` (the physical key: `KeyA`, `Digit1`, `Numpad1`, `F1`), then `event.key` (`Enter`, `Esc`, `Up`, the short forms of old browsers), then `event.keyCode`; modifiers are read from the event and must match a listener's exactly.
|
|
178
|
+
|
|
179
|
+
| Event | Does |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| DOM focus arrives by the keyboard | Rive's focus enters the tree: the first stop coming from before the canvas, the last coming from after (Shift+Tab). Where `:focus-visible` is unknown, a press on the canvas within 600 ms marks it as the pointer's |
|
|
182
|
+
| DOM focus arrives by a click or touch | Rive's focus stays where it was; the next Tab enters the tree. A press focuses a target only through a listener that runs `FocusTarget`, as in Rive |
|
|
183
|
+
| `keydown` (`keyup`) | Offered to the focused target's keyboard listeners, then those above it; Tab and Shift+Tab move focus; an arrow no listener took moves it by position |
|
|
184
|
+
| Tab off the last stop | Left to the browser, which moves DOM focus on; keys are ignored until focus is given again |
|
|
185
|
+
| DOM focus leaves | Rive's focus is cleared (Blur listeners run), unless the window itself lost focus |
|
|
186
|
+
|
|
187
|
+
`preventDefault` is called only for a key Rive used: one a listener took, a Tab or arrow that moved focus, or a Tab held by a `stop` edge (and an arrow while anything is focused, so the page does not scroll). A key nothing used (Tab off the end, an unbound letter) is the page's.
|
|
188
|
+
|
|
189
|
+
### Manual (`motion.key` and friends, always available)
|
|
190
|
+
|
|
191
|
+
For hosts that route the remote themselves, such as a TV web app, with `keys: 'manual'` so the canvas listens to nothing. These work with `auto` too.
|
|
192
|
+
|
|
193
|
+
| Call | Does |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `motion.key(key)` | A key press: a Rive key name (`enter`, `up`, `tab`, `a`), a Roku remote name (`OK`, `back`), either with modifiers in front (`shift+tab`, `ctrl+a`), or a `KeyboardEvent`. Returns whether the bundle used it: false for a Tab off the end of the tree or a key nothing heard, so the host can move on. Enter and OK also click: the focused target, or, with no focus tree, every Click listener |
|
|
196
|
+
| `motion.keyUp(key)` | The release, for listeners with the up phase |
|
|
197
|
+
| `motion.focus(target)` | Moves Rive's focus to a target by `rid` or name, or `'next'`, `'previous'`, `'up'`, `'down'`, `'left'`, `'right'`. Returns whether it moved. Does not touch DOM focus |
|
|
198
|
+
| `motion.blur()` | Clears Rive's focus; Blur listeners run |
|
|
199
|
+
| `motion.focused` | The focused target's `rid`, or null |
|
|
200
|
+
| `motion.click(target?)` | Without a target, every Click listener (as OK); with one, a press and release on it |
|
|
201
|
+
|
|
202
|
+
With no host keys at all (`auto`), nothing else is needed: a bundle with no focus tree runs its keyboard listeners for the keys the canvas hears while it has DOM focus, with no focus rules, as before.
|
|
203
|
+
|
|
204
|
+
**Visible focus.** Rive draws focus itself, through the focus listeners the file has (a state, a view-model colour); Motion draws no ring.
|
|
205
|
+
|
|
206
|
+
**Nested artboards and lists.** The tree is one across them, as Rive's is. A nested artboard's targets are in the host's tree named `<sid>/<rid>` (the instance's id, then the object's index in the nested artboard; a nested artboard that owns a `FocusData` is `<sid>/`), where its placement sits in the hierarchy: Tab and the arrows cross into and out of it, and `motion.focus('<sid>/<rid>')` or `motion.focus('Pad/Button')` (the placement's name, then the node's) focuses one. A key goes to the focused node's listeners, then the nested artboard's nodes above it, then the host's nodes above the placement; each listener runs in the machine that owns it, as do the Focus and Blur listeners of every node whose subtree gains or loses focus. A focus action a nested machine runs (`FocusTarget` of a node of its own artboard, `FocusMove`, `FocusClear`) moves the host's one focus, including the one a nested state runs as it starts. A list's rows are in the tree in item order where the list sits, each item's nodes named `row.<id>/<rid>` (and `row.<id>/<sid>/<rid>` for a nested artboard in the row, `<id>` the row's own number, which stays with it while it is kept); the tree is rebuilt as rows come and go, and a row that loses its place while focused leaves focus on a node nothing hears until the next Tab, as `@rive-app/canvas` does. A row's own machine can now set properties of the row's view model (a listener, a state's action), and the row's slots follow.
|
|
207
|
+
|
|
208
|
+
**Limits.** A directional move measures where the vector renderer last drew each target (a group by the centre of what it drew; Rive measures a group at its origin), so the `raster` renderer has nothing to measure and `auto` stays on vector while a bundle has a focus tree. The directional weights are fitted to `@rive-app/canvas`, not documented by Rive. `TransitionFocusCondition` and `SemanticData` are not read.
|
|
209
|
+
|
|
210
|
+
## Layers, Blends and Cross-fades
|
|
211
|
+
|
|
212
|
+
Rive plays several animations of one artboard at once and mixes what they do. A bundle's machine carries that ([the machine](../../bin/lib/lottie/readme.md#the-machine)), and the web player does it through a **pose mixer** (`mixer/`). A bundle that has a second layer, a blend state, a transition with a duration or one with an exact exit time, a state that plays backwards or at another speed, or animations that key different properties (the machine's `holds`) plays through it; any other bundle plays one animation at a time, as before, at the same cost.
|
|
213
|
+
|
|
214
|
+
| Rive | Plays as |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| **Layers** | Each layer is in a state of its own, and its animation plays beside the others'. Layers apply in order, so a later layer overrides the properties it keys |
|
|
217
|
+
| **1D blend** | The two animations either side of the input's value, weighted `1 - t` and `t` for how far `t` along it is; outside the range, the animation at that end alone. The input is a state machine input or a view model property |
|
|
218
|
+
| **Direct blend** | Each animation weighted by its own input or view model property (0-100, clamped) or its fixed mix |
|
|
219
|
+
| **Cross-fade** | A transition with `durationMs` mixes the incoming state in over that time while the outgoing one plays on, along the transition's `ease` curve when it has one. One interrupted by another continues from where it was |
|
|
220
|
+
| **Exact exit time** | A transition with `exitTime` waits for that fraction or ms of its state's timeline (a looping one too), counted from entering the state; its other guards still apply |
|
|
221
|
+
|
|
222
|
+
- **How a pose mixes.** A pose is an animation at a time with a weight. The mixer takes the artboard's longest animation as the picture's structure and gives each property that any animation keys (`ky`) an evaluator that reads it in every pose's animation, by `rid`, at the pose's time. The poses apply in order, each lerping the value so far toward its own by its weight, as Rive applies animations; a pose whose animation does not key the property leaves it alone. A pair of axes (position, scale, size) keyed on one axis only (`kc`) is mixed on that axis only.
|
|
223
|
+
- **What the value so far is.** Rive's artboard keeps what it last applied, and the mixer does the same: each frame starts from what the property held after the previous one, not from its own value. A property no active animation keys stays where it was (`blend-hold`), and weights that do not add up to one settle over a few frames. A 1D blend half way between two animations lands a third of the way from the first, not half way (`blend-1d`; Rive's own result). A stage that has finished applies the poses several times per input change to settle as Rive's loop does, since nothing steps it. Drawing the same frame again (a resize) does not move it on.
|
|
224
|
+
- **A Solo's choice does not mix.** The visibility of each of a Solo's children is a property tagged `<id>.solo` ([the converter](../../bin/lib/lottie/readme.md#solo)); Rive sets the choice from any animation that applies it, however little weight it has, so the mixer applies such a property whole from every pose with weight above 0, the last pose winning. A data-bound Solo is slots filled by the `solo` converter descriptor (`motion.convert.js`).
|
|
225
|
+
- **What mixes.** Numbers and vectors (positions, scales, rotations, opacity, sizes, colours) component by component, and paths vertex by vertex where they have the same vertices. A value that cannot blend takes the heavier of the two.
|
|
226
|
+
- **What it relies on.** rive2lottie writes every animation of an artboard with the same layers and shape items and warns where they differ (`structureWarnings`). On every fixture and file in this repository they match.
|
|
227
|
+
- **Time is the stage's.** `mixer/mixer.stage.js` holds each layer's runs and clocks. `finished` and the OnComplete interactions come from the layer that ran out; the `finished` event and `motion.state` follow the first layer.
|
|
228
|
+
- **Cues and speed.** A mixed bundle plays the audio cues of every animation that has weight, each once per pass and again on every loop, from the stretch of its timeline the step passed (`sweeps` in `mixer/mixer.stage.js`). A state's `speed` scales its clock, and `Reverse` and `ReverseBounce` play its timeline from the end; `Bounce` needs nothing, as a ping-pong timeline is written out there and back.
|
|
229
|
+
- **Text overlays** of several animations at once mix channel by channel as shapes do (`mixOverlays`): a channel that an animation moves (more than one key) lerps toward it by its weight, and one it holds still is left alone. Overlays do not keep the last value across frames.
|
|
230
|
+
- **Limits.**
|
|
231
|
+
- Mixed bundles draw vector only: raster sheets and the adaptive move to them cannot mix, and `renderer: 'raster'` plays the first layer with instant switches.
|
|
232
|
+
- A nested artboard that no machine plays (its precomp time is not tagged) follows the first pose's time, and is not mixed; one a machine plays keeps its own clock.
|
|
233
|
+
- A cross-fade follows a cubic `ease`; an elastic or `hold` transition interpolation is linear here.
|
|
234
|
+
- The back leg of a ping-pong state plays about 3% of pixels off Rive's in the `state-modes` fixture, which therefore checks only the forward leg: the timeline is written out as a time-remapped precomp and Rive's reflection at the turn differs by a couple of frames.
|
|
235
|
+
- A view model property can only drive a blend through the input the converter makes from it (`amount`, `card.mix`): the instance's other bindings and converters on a blend's property are not applied.
|
|
236
|
+
|
|
237
|
+
## Layout
|
|
238
|
+
|
|
239
|
+
| Path | Holds |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| `motion.js` | `createMotion`: wires the parts below |
|
|
242
|
+
| `audio/` | Sound on `HTMLAudioElement`: preloading, voices, cue timing |
|
|
243
|
+
| `bundle/` | Loading motion.json and its files over XMLHttpRequest; registering its fonts |
|
|
244
|
+
| `bands/` | Layout bands that blend: the grid of bands, where the canvas falls in it (the cell, with its corner weights), and the link that mixes a corner with its neighbours |
|
|
245
|
+
| `focus/` | Rive's focus: the tree and traversal, directional moves, key routing, and the canvas's focus and key events |
|
|
246
|
+
| `hit/` | Pointer hit testing: the paths each targeted group drew, and point-in-shape maths |
|
|
247
|
+
| `list/` | Lists: a row artboard drawn for each item of a view-model list, and the small flex that places them |
|
|
248
|
+
| `lottie/` | The canvas renderer: properties, paths, shapes, paint, layers |
|
|
249
|
+
| `mixer/` | The pose mixer: mixing property values, the stage of what each layer plays, the mixed picture |
|
|
250
|
+
| `motion/` | The state machine, the flow, keys, live values, data converters, nested artboards' machines |
|
|
251
|
+
| `player/` | The clock and canvas, fit and alignment, raster sheets, `overlay/` (text overlays: drawing and run layout) |
|
|
252
|
+
| `pointer/` | Mouse, touch and pen events → pointer interactions: hover per target, press, click |
|
|
253
|
+
| `script/luau/` | A Luau bytecode loader and interpreter (bytecode versions 3 to 14) with a standard library subset, for the scripts a Rive file embeds; see [luau.md](script/luau/luau.md) |
|
|
254
|
+
| `script/rive/` | The Rive scripting host on that VM: a script's factory, `init`/`advance`/`update`/`draw`, `context` and the view model through an adapter the player implements, `Path`, `Paint`, `Mat2D`, `Vector` and `Color`, view-model lists and instances, images, meshes, text, nested artboards and audio as recorded drawings, pointer events into scripted nodes, drawings as Lottie vertex arrays, and path effects; see [rive.md](script/rive/rive.md) |
|
|
255
|
+
| `script/play/` | Runs the scripts a bundle ships on that host: converters, conditions and actions when they are used, and scripted nodes, drawables and path effects every frame (`play.objects.js`); `script/script.js` is what a page gives the player, and only a bundle that has scripts loads it; see [Scripts](#scripts) |
|
|
256
|
+
| `script/rive/` | The Rive scripting host on that VM: a script's factory, `init`/`advance`/`update`/`draw`, `context` and the view model through an adapter the player implements, `Path`, `Paint`, `Mat2D`, `Vector` and `Color`, drawings as Lottie vertex arrays, and path effects; see [rive.md](script/rive/rive.md) |
|
|
257
|
+
|
|
258
|
+
## Scripts
|
|
259
|
+
|
|
260
|
+
A bundle that lists `scripts` (`motion.json`, entry to file) has scripted converters (`{ type: 'script' }`
|
|
261
|
+
descriptors), transition conditions (`{ type: 'Script' }` guards) or listener actions
|
|
262
|
+
(`{ type: 'RunScript' }` actions) that depend on live data; the converter already evaluated the rest.
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
const motion = createMotion(canvas, { scripts: () => import('@px-petals/roku.motion-web/script/script.js') })
|
|
266
|
+
await motion.load('https://example.test/bundle/motion.json')
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
- **A script starts once, with its data.** Rive inits a script when its artboard is made, before the view model binds, where a script that needs one gives up, and inits it again once bound; Motion binds the bundle's default instance from the start and inits each script once, which ends where Rive's retry does. A bundle without a view model gives its scripts none: `context:viewModel()` is nil, as in Rive.
|
|
270
|
+
- **Nothing loads for a bundle without scripts.** The main bundle holds only the loader (`loadScripts` in
|
|
271
|
+
`bundle/bundle.js`) and the guard, action and converter cases of the machine and `motion.convert.js`: 1.5 KB
|
|
272
|
+
minified, 0.5 KB gzipped, together, and the hand-off to scripted objects (`startObjects` and the merge of what
|
|
273
|
+
they draw into the slot table in `motion.js`): 1.2 KB minified, 0.4 KB gzipped more. The VM, the Rive host and `script/play/` are `script/script.js`, the `scripts`
|
|
274
|
+
option (the module, or a function returning it or a promise of it), which Motion asks for only after it has
|
|
275
|
+
read a manifest with `scripts`, together with that bundle's bytecode. The legacy build writes
|
|
276
|
+
`script/script.js` as a file of its own, `.artifacts/dist/motion-web.script.legacy.js` (global
|
|
277
|
+
`MotionScripts`), for a page to load for such a bundle and pass as `scripts: MotionScripts`. Without the option, one `error` event says so, a script
|
|
278
|
+
condition does not pass, a script converter passes its value through and a script action does nothing.
|
|
279
|
+
- **The scripts see the live values** as their view model: a property is a value by its dotted path, a write
|
|
280
|
+
changes it (and moves the machine and the slots), and a trigger a script fires is an event the machine
|
|
281
|
+
takes in the same round. A script converter that reads none of it is run once per distinct input; one that
|
|
282
|
+
reads it, and every condition, runs at each use: a condition once per scripted guard checked when the
|
|
283
|
+
machine settles, a converter once per changed source when the values change.
|
|
284
|
+
- **Pointer events** reach a listener action with the type of the interaction (`click`, `down`, `up`, `enter`,
|
|
285
|
+
`exit`, `move`) and the pointer's position at the origin: the pointer reports the interaction and its target,
|
|
286
|
+
not coordinates. `hit()` is accepted and has no effect.
|
|
287
|
+
- **Not run:** the scripts of nested artboards' machines and list rows, a script converter on a blend state's
|
|
288
|
+
input (the mixer passes its value through), and a script's `advance` for these three protocols (nothing
|
|
289
|
+
drives time for them; scripted objects below do).
|
|
290
|
+
|
|
291
|
+
### Scripted objects
|
|
292
|
+
|
|
293
|
+
A script attached to an object of the artboard (a `node`, a `drawable` or a `pathEffect`) that reads live data or
|
|
294
|
+
needs the clock ships with the bundle: `motion.json` lists it in `scriptObjects` (the entries of the converter's
|
|
295
|
+
`scripts.json`, `bin/lib/rive/transformer/dotlottie/scripts/scripts.export.js`) and its bytecode in
|
|
296
|
+
`scripts`. A script whose output the converter baked ships nothing and costs nothing. The scripts option loads
|
|
297
|
+
the engine for such a bundle as for any other; `script/play/play.objects.js` is the part of it that runs them.
|
|
298
|
+
|
|
299
|
+
| Entry field | Used for |
|
|
300
|
+
| --- | --- |
|
|
301
|
+
| `id`, `kind`, `inputs` | the bytecode in `scripts`, `node`, `drawable` or `pathEffect`, and the script's inputs |
|
|
302
|
+
| `order` | the run order across the bundle (the artboard's object order, the host's first); hosts run in it, so a script reads what an earlier one wrote in the same frame, and one run before it reads the value a frame late |
|
|
303
|
+
| `scope` | the view-model path the script's context is bound at (`""`: the player's own values; `gauge`: a nested artboard bound there), under which every path it reads or writes sits |
|
|
304
|
+
| `reads`, `clock` | the paths it reads, under `scope`, and whether time alone changes what it does. A host with no `clock` whose `reads` held the same values as when it last ran is skipped: not called, not even `advance` |
|
|
305
|
+
| `draws` | a drawable's fixed set of `{ sid, fill?, stroke?, width? }` slots: each contour it draws goes into a `sid` in order (an unused one holds an empty path), its paint into `fill` (or `stroke`, with the thickness into `width`) as Lottie's `[r, g, b, a]` with the opacity multiplied in. More contours than slots are dropped, with one `error` event |
|
|
306
|
+
| `effect` | a path effect's `{ sid, source? }`: the result goes into the slot `sid`; `source` is the slot id of the path it is applied to, whose current geometry is its input. Without `source`, the input is the slot's baked value, taken before the first write. A result of several contours keeps the first |
|
|
307
|
+
|
|
308
|
+
- **When they run.** Each frame the player steps (`options.tick`, before the nested machines), so a paused or
|
|
309
|
+
stopped player runs none and a finished one runs them only while a host has a `clock` or a read that changed.
|
|
310
|
+
Rive's first advance after binding runs no script and the next runs `init`, `advance` and `update` together
|
|
311
|
+
([rive.md](script/rive/rive.md#what-rives-web-runtime-does-as-measured)); the first step here is that
|
|
312
|
+
advance. A script is stopped and released with the player (`destroy`, or loading another bundle).
|
|
313
|
+
- **What they write** lands in the flow's values at once, for the next script to read, and reaches the machine,
|
|
314
|
+
the nested artboards, the slots and the `values` event together, once a frame, as a host's `setInputs` does.
|
|
315
|
+
A trigger a script fires is an event the machine takes. The scripts' view model is `createValuesViewModel`
|
|
316
|
+
(`script/rive/context/lists/lists.values.js`) over the values, rooted at the entry's `scope`.
|
|
317
|
+
- **What they draw** is merged into every animation's slot table after the bindings and skins, and stays
|
|
318
|
+
there between frames: nothing is allocated per frame by the player's loop (the draw list, the paint colours and an
|
|
319
|
+
effect's contour list are kept); the host allocates what a script's own calls and drawing records need.
|
|
320
|
+
- **Cost** is measured in [script/rive/rive.md](script/rive/rive.md#live-objects).
|
|
321
|
+
|
|
322
|
+
## Tests
|
|
323
|
+
|
|
324
|
+
```sh
|
|
325
|
+
npm run motion web test # unit tests, the motion-stress scenarios, and the browser tests
|
|
326
|
+
npm run motion web typecheck
|
|
327
|
+
npm run motion web build:legacy # .artifacts/dist/motion-web.legacy(.min).js, ES5 for the browserslist
|
|
328
|
+
npm run motion web legacy test # the legacy build in real old Chrome and Firefox, in Docker
|
|
329
|
+
npm run motion web parity # the parity scorecard against Rive's canvas runtime, to .artifacts/parity/report.md
|
|
330
|
+
npm run motion web luau:fetch # the official Luau toolchain into .artifacts/luau, for the reference tests (they skip without it)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
- **Scenarios:** `motion/.tests/motion.scenarios.test.js` replays the motion-stress scenarios Roku runs on a device, on a simulated clock, against the same expectations; the oracle ones come from the Rive CLI. The fixtures are a snapshot; [.tests/fixtures/readme.md](.tests/fixtures/readme.md) says how to refresh it.
|
|
334
|
+
- **Bands:** `bands/.tests` weigh a canvas between bands and mix a band with the next, and weigh a canvas in a cell of the grid and mix its four corners; the browser tests draw a canvas between two bands, plain and through the mixer, size the artboard to the canvas in both directions, and keep the manual clock free of animation frames; `layout-responsive` (square and not), `layout-wrap` and `layout-height` in the [parity scorecard](.tests/parity/readme.md) check every size against Rive's.
|
|
335
|
+
- **Focus:** `focus/.tests` walk the tree (order, scopes, `closedLoop` and `stop`, what cannot focus), the focus and blur chain, key routing (spent keys, phases, exact modifiers, `any`, Tab and arrows) and directional scoring, and drive a fake canvas through `keys: 'auto'` (keyboard and pointer focus, leaving, the window losing focus, `preventDefault`, and that nothing calls `focus()`); `focus-tab`, `focus-action`, `focus-keys` and `click-focus` in the [parity scorecard](.tests/parity/readme.md#focus-and-keys) check it against Rive's.
|
|
336
|
+
- **Mixer:** `mixer/.tests` mix values, plan the stage (cross-fades, blends, layers, finishing) and draw mixed poses on a recording context; `motion/.tests/motion.layers.test.js` runs the flow over layers, durations and exit times.
|
|
337
|
+
- **Browser:** `.tests/browser/browser.test.js` builds bundles from the committed fixtures with `rive2lottie` and `lottie-roku`, then runs the renderer comparison and a whole Motion in headless Chromium, once on the modules and once on the legacy build. The pose mixer's tests there step a clock by hand to read a cross-fade halfway, a 1D and a direct blend, two layers and an exit time at exact times, on the converter's [mixer fixtures](../../bin/lib/rive/.tests/files/readme.md).
|
|
338
|
+
- **Skins:** `skin/.tests` check the deform against values worked out by hand (a translated root bone, a two-bone chain with rotation, mixed weights, cubic handle weights, the skin and space transforms, a bound value through a converter, IK chains of one to three bones in each direction and at part strength, a target from a bind and its space, the bones below a chain, corner radius, root bones in their own parent worlds, nodes above the bones and the node the group sits in, tracks under bound values, a list row's skins) and draw a skinned path on a recording context.
|
|
339
|
+
- **Lists:** `list/.tests` place rows by a column, a row, wraps, reversals, padding and hugging, and drive a list through items from instances and from values of their own, rows added and removed, a row's machine, and a draw on a recording context; the parity fixtures `list-bind`, `list-wrap` and `list-machine` compare the rows, a host replacing the list, and the rows' machines with Rive's own.
|
|
340
|
+
- **Luau:** `script/luau/.tests` assemble bytecode with a small writer that follows Luau's `BytecodeBuilder` (no compiler runs) and drive the loader and the interpreter through every opcode group, the metamethods, and the library, patterns especially; one test lowers the VM through the legacy build and runs it. `script/luau/.tests/reference` runs Luau programs through the official `luau-compile` and `luau` (a pinned release, see [luau.md](script/luau/luau.md#checked-against-the-official-luau)) and requires the same output; `npm run luau:fetch` downloads them into `.artifacts/luau/`, the tests skip without them, and the committed -O1 fixtures in `script/luau/.tests/fixtures` still run in CI that has not fetched them.
|
|
341
|
+
- **Scripted objects:** `script/play/.tests/objects.test.js` runs nodes, a drawable and a path effect, compiled from `script/play/.tests/sources` with the pinned official compiler (`node script/play/.tests/regen.js` after `npm run luau:fetch`, into committed `.luaubc` fixtures), against a stand-in player: an easing node ordered before a second that reads it (and the other way round), skipped idle hosts, scopes, inputs, draws into slots and beyond them, an effect on a shape, destroying. `objects.browser.test.js` plays the same scripts in a whole Motion in headless Chromium, on the modules and on the legacy build with its script engine file, and also pauses, redraws and destroys.
|
|
342
|
+
- **Rive scripts:** `script/rive/.tests` drive scripts assembled the same way (a factory-returning module, no compiler) through the host protocol (order of `init`, `advance`, `update` and `draw`, inputs, `markNeedsUpdate`, view-model reads, writes and listeners including nested view models, a drawn path, a path effect, errors), call the geometry API directly (vectors, colours, matrices, paths and their measures, paints, the recording renderer), and run the host lowered through the legacy build. Scripts for lists, images, meshes, text, artboards, sounds and the pointer are Luau source in `script/rive/.tests/sources`, compiled with the pinned official compiler (`node script/rive/.tests/regen.js` after `npm run luau:fetch`) into committed `.luaubc` fixtures, so the tests need nothing fetched. The host's view-model output was also checked against `@rive-app/canvas` playing a real script file, see [rive.md](script/rive/rive.md#what-rives-web-runtime-does-as-measured).
|
|
343
|
+
- **Pointer:** `hit/.tests` test the point-in-shape maths and what a drawn frame records; `pointer/.tests` drive a fake canvas through enter, exit, press, click, touch and the mouse events a touch imitates, and the machine's per-target interactions; the browser tests move a real mouse and dispatch touch pointer events over the `pointer` fixture's shape and group.
|
|
344
|
+
- **Parity:** [.tests/parity/readme.md](.tests/parity/readme.md) plays one fixture per Rive feature in Rive's canvas runtime and in this one, and marks each ✅, ⚠️ or ❌. It is slow, so `npm test` leaves it out.
|
|
345
|
+
- **Legacy browsers:** [.tests/legacy/readme.md](.tests/legacy/readme.md) runs the legacy build, and Rive's canvas runtime for comparison, in Chrome and Firefox from our floor up, and records what Rive needs to run there.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base library: `assert`, `error`, `pcall`, `type`, `tostring`,
|
|
3
|
+
* `ipairs`, `pairs`, `select`, `setmetatable`, `print` and the rest of the
|
|
4
|
+
* globals that are not in a table.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { EMPTY, LuaTable, LuaThrow, NativeFunction, Vector, fail, typeName } from '../../vm/vm.value.js'
|
|
8
|
+
import { equals, rawSet, toNumber, tostring } from '../../vm/vm.meta.js'
|
|
9
|
+
import { numberToString, stringToNumberBase } from '../../vm/vm.num.js'
|
|
10
|
+
import { argError, checkAny, checkInt, checkTable, optInt, register } from '../lib.args.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {any} e something caught
|
|
14
|
+
* @returns {{ value: any } | null} the Lua error value, or null when e is not a Lua error
|
|
15
|
+
*/
|
|
16
|
+
function caught(e) {
|
|
17
|
+
if (e instanceof LuaThrow) return { value: e.value }
|
|
18
|
+
if (e instanceof RangeError) return { value: 'stack overflow' }
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('../../vm/vm.js').VM} vm
|
|
24
|
+
* @param {LuaTable} G the globals
|
|
25
|
+
*/
|
|
26
|
+
export function installBase(vm, G) {
|
|
27
|
+
register(G, {
|
|
28
|
+
assert(vm, args) {
|
|
29
|
+
if (args[0] === undefined || args[0] === false) throw fail(args.length > 1 ? args[1] : 'assertion failed!', args.length > 1 && typeof args[1] !== 'string' ? 0 : 1)
|
|
30
|
+
return args
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
error(vm, args) {
|
|
34
|
+
const level = optInt(args, 1, 'error', 1)
|
|
35
|
+
const value = args[0]
|
|
36
|
+
// a number message is made a string when a position may be added, as lua_concat does
|
|
37
|
+
const wants = (typeof value === 'string' || typeof value === 'number') && level > 0
|
|
38
|
+
throw new LuaThrow(wants && typeof value === 'number' ? numberToString(value) : value, wants ? level : 0)
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
pcall(vm, args) {
|
|
42
|
+
checkAny(args, 0)
|
|
43
|
+
try {
|
|
44
|
+
return [ true ].concat(vm.call(args[0], args.slice(1)))
|
|
45
|
+
} catch (e) {
|
|
46
|
+
const c = caught(e)
|
|
47
|
+
if (c === null) throw e
|
|
48
|
+
return [ false, c.value ]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
xpcall(vm, args) {
|
|
53
|
+
checkAny(args, 0)
|
|
54
|
+
try {
|
|
55
|
+
return [ true ].concat(vm.call(args[0], args.slice(2)))
|
|
56
|
+
} catch (e) {
|
|
57
|
+
const c = caught(e)
|
|
58
|
+
if (c === null) throw e
|
|
59
|
+
return [ false ].concat(vm.call(args[1], [ c.value ]))
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
type(vm, args) {
|
|
64
|
+
checkAny(args, 0)
|
|
65
|
+
return typeName(args[0])
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
typeof(vm, args) {
|
|
69
|
+
checkAny(args, 0)
|
|
70
|
+
return typeName(args[0])
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
tostring(vm, args) {
|
|
74
|
+
checkAny(args, 0)
|
|
75
|
+
return tostring(vm, args[0])
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
tonumber(vm, args) {
|
|
79
|
+
const base = args[1]
|
|
80
|
+
if (base === undefined) {
|
|
81
|
+
checkAny(args, 0)
|
|
82
|
+
return toNumber(args[0])
|
|
83
|
+
}
|
|
84
|
+
const b = checkInt(args, 1, 'tonumber')
|
|
85
|
+
if (b < 2 || b > 36) throw argError(2, 'tonumber', 'base out of range')
|
|
86
|
+
if (typeof args[0] !== 'string') throw argError(1, 'tonumber', 'string expected, got ' + typeName(args[0]))
|
|
87
|
+
return stringToNumberBase(args[0], b)
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
ipairs(vm, args) {
|
|
91
|
+
checkTable(args, 0, 'ipairs')
|
|
92
|
+
return [ vm.inext, args[0], 0 ]
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
pairs(vm, args) {
|
|
96
|
+
checkTable(args, 0, 'pairs')
|
|
97
|
+
return [ vm.next, args[0], undefined ]
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
next(vm, args) {
|
|
101
|
+
const t = checkTable(args, 0, 'next')
|
|
102
|
+
const pos = t.after(args[1])
|
|
103
|
+
if (pos === undefined) throw fail("invalid key to 'next'")
|
|
104
|
+
const nextPos = t.advance(pos)
|
|
105
|
+
return nextPos === null ? undefined : [ t.ck, t.cv ]
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
select(vm, args) {
|
|
109
|
+
const n = args[0]
|
|
110
|
+
if (n === '#') return args.length - 1
|
|
111
|
+
let i = checkInt(args, 0, 'select')
|
|
112
|
+
if (i < 0) i = args.length + i
|
|
113
|
+
else if (i > args.length - 1) return EMPTY
|
|
114
|
+
if (i < 1) throw argError(1, 'select', 'index out of range')
|
|
115
|
+
return args.slice(i)
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
rawget(vm, args) {
|
|
119
|
+
return checkTable(args, 0, 'rawget').get(args[1])
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
rawset(vm, args) {
|
|
123
|
+
const t = checkTable(args, 0, 'rawset')
|
|
124
|
+
rawSet(t, args[1], args[2])
|
|
125
|
+
return t
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
rawequal(vm, args) {
|
|
129
|
+
checkAny(args, 1)
|
|
130
|
+
const a = args[0]
|
|
131
|
+
const b = args[1]
|
|
132
|
+
return a === b || (a instanceof Vector && b instanceof Vector && equals(vm, a, b))
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
rawlen(vm, args) {
|
|
136
|
+
const v = args[0]
|
|
137
|
+
if (v instanceof LuaTable) return v.length()
|
|
138
|
+
if (typeof v === 'string') return v.length
|
|
139
|
+
throw argError(1, 'rawlen', 'table or string expected')
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
setmetatable(vm, args) {
|
|
143
|
+
const t = checkTable(args, 0, 'setmetatable')
|
|
144
|
+
const mt = args[1]
|
|
145
|
+
if (mt !== undefined && !(mt instanceof LuaTable)) throw argError(2, 'setmetatable', 'nil or table expected')
|
|
146
|
+
if (t.frozen) throw fail('Attempt to modify a readonly table')
|
|
147
|
+
if (t.meta !== undefined && t.meta.getStr('__metatable') !== undefined) throw fail('cannot change a protected metatable')
|
|
148
|
+
t.meta = mt
|
|
149
|
+
return t
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
getmetatable(vm, args) {
|
|
153
|
+
checkAny(args, 0)
|
|
154
|
+
const v = args[0]
|
|
155
|
+
const mt = v instanceof LuaTable ? v.meta : typeof v === 'string' ? vm.stringMeta : undefined
|
|
156
|
+
if (mt === undefined) return undefined
|
|
157
|
+
const guard = mt.getStr('__metatable')
|
|
158
|
+
return guard !== undefined ? guard : mt
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
unpack(vm, args) {
|
|
162
|
+
return unpack(checkTable(args, 0, 'unpack'), args[1], args[2])
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
print(vm, args) {
|
|
166
|
+
const parts = []
|
|
167
|
+
for (let i = 0; i < args.length; i++) parts.push(tostring(vm, args[i]))
|
|
168
|
+
vm.print(parts.join('\t'))
|
|
169
|
+
return EMPTY
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
collectgarbage(vm, args) {
|
|
173
|
+
return args[0] === 'count' ? 0 : 0
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
G.set('_G', G)
|
|
178
|
+
G.set('_VERSION', 'Luau')
|
|
179
|
+
|
|
180
|
+
vm.next = /** @type {NativeFunction} */ (G.getStr('next'))
|
|
181
|
+
vm.inext = new NativeFunction('inext', function (vm2, args) {
|
|
182
|
+
const t = checkTable(args, 0, 'ipairs')
|
|
183
|
+
const i = checkInt(args, 1, 'ipairs') + 1
|
|
184
|
+
const v = t.get(i)
|
|
185
|
+
return v === undefined ? undefined : [ i, v ]
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {LuaTable} t
|
|
191
|
+
* @param {any} first key to start at, default 1
|
|
192
|
+
* @param {any} last key to end at, default #t
|
|
193
|
+
* @returns {any[]}
|
|
194
|
+
*/
|
|
195
|
+
export function unpack(t, first, last) {
|
|
196
|
+
const i = first === undefined ? 1 : /** @type {number} */ (toNumber(first))
|
|
197
|
+
const j = last === undefined ? t.length() : /** @type {number} */ (toNumber(last))
|
|
198
|
+
if (j - i >= 1e6) throw fail('too many results to unpack')
|
|
199
|
+
const out = []
|
|
200
|
+
for (let n = i; n <= j; n++) out.push(t.get(n))
|
|
201
|
+
return out
|
|
202
|
+
}
|