@px-petals/roku.motion-web 0.0.0-stage → 1.0.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.
@@ -0,0 +1,292 @@
1
+ /**
2
+ * The pointer on the canvas: mouse, touch and pen events become the pointer
3
+ * interactions a bundle's listeners answer (down, up, enter, exit, move,
4
+ * click), each hit-tested against the shape or group the listener targets.
5
+ *
6
+ * - A target is hovered while the pointer is inside its geometry in the last
7
+ * drawn frame (`hit/`), so enter and exit follow a moving shape as well as
8
+ * a moving pointer.
9
+ * - A listener with no target watches the whole canvas: it enters when the
10
+ * pointer comes onto the canvas and exits when it leaves.
11
+ * - A click is a down and an up on the same target. Clicks on the canvas
12
+ * itself (the `click` event) belong to the listeners with no target, which
13
+ * motion.js forwards.
14
+ *
15
+ * Events: `pointer*` where the browser has them (they cover mouse, touch and
16
+ * pen); otherwise `mouse*` and `touch*`, ignoring the mouse events a touch
17
+ * makes up afterwards.
18
+ */
19
+
20
+ /** @import { Hits } from '../hit/hit.js' */
21
+
22
+ /** A mouse event this soon after a touch is the browser's own imitation of it */
23
+ const TOUCH_MOUSE_MS = 800
24
+
25
+ /**
26
+ * @typedef {object} PointerOptions
27
+ * @property {() => Hits | null} hits the geometry the last frame drew; null while it is not vector
28
+ * @property {(type: string, target: string) => void} fire runs a pointer interaction; target "" for the listeners without one
29
+ * @property {boolean} clicks whether a click fires Click interactions
30
+ */
31
+
32
+ /**
33
+ * @param {HTMLCanvasElement} canvas
34
+ * @param {PointerOptions} options
35
+ */
36
+ export function createPointer(canvas, options) {
37
+ /** @type {Record<string, string[]>} target → interaction types, as the machine lists them */
38
+ let targets = {}
39
+ /** @type {Record<string, boolean>} */
40
+ let inside = {}
41
+ /** @type {Record<string, boolean>} */
42
+ let pressed = {}
43
+ let lastTouch = 0
44
+ let isBound = false
45
+ /** @type {{ name: string, listener: (event: any) => void }[]} */
46
+ const bound = []
47
+
48
+ /**
49
+ * @param {string} target
50
+ * @param {string} type
51
+ * @returns {boolean}
52
+ */
53
+ const listensTo = (target, type) => !!targets[target] && targets[target].indexOf(type) >= 0
54
+
55
+ /**
56
+ * @param {string[]} types
57
+ * @returns {boolean} whether any target listens to one of these
58
+ */
59
+ const hasAny = (types) => {
60
+ for (const target in targets) for (let n = 0; n < types.length; n++) if (listensTo(target, types[n])) return true
61
+ return false
62
+ }
63
+
64
+ /**
65
+ * @param {string} target
66
+ * @param {number} x
67
+ * @param {number} y
68
+ * @returns {boolean} whether the pointer is on the target
69
+ */
70
+ function isOn(target, x, y) {
71
+ if (target === '') return true
72
+ const hits = options.hits()
73
+ return !!hits && hits.test(target, x, y)
74
+ }
75
+
76
+ /**
77
+ * Takes the interactions each target listens to and starts watching for the pointer, or stops.
78
+ *
79
+ * @param {Record<string, string[]>} next the machine's `pointerTargets`
80
+ */
81
+ function listen(next) {
82
+ // what is watched again keeps where the pointer is on it: rows that come and go do not end a press on the others
83
+ /** @type {Record<string, boolean>} */
84
+ const keptInside = {}
85
+ /** @type {Record<string, boolean>} */
86
+ const keptPressed = {}
87
+ for (const name in next) {
88
+ if (!targets[name]) continue
89
+ keptInside[name] = inside[name]
90
+ keptPressed[name] = pressed[name]
91
+ }
92
+ targets = next
93
+ inside = keptInside
94
+ pressed = keptPressed
95
+ const wanted = hasAny([ 'PointerDown', 'PointerUp', 'PointerEnter', 'PointerExit', 'PointerMove' ]) || (options.clicks && hasAnyTargeted('Click'))
96
+ if (wanted && !isBound) bind()
97
+ else if (!wanted && isBound) unbind()
98
+ }
99
+
100
+ /**
101
+ * @param {string} type
102
+ * @returns {boolean} whether a listener on a target (not the whole artboard) has it
103
+ */
104
+ function hasAnyTargeted(type) {
105
+ for (const target in targets) if (target !== '' && listensTo(target, type)) return true
106
+ return false
107
+ }
108
+
109
+ /** @returns {string[]} the targets that have a pointer interaction */
110
+ function names() {
111
+ const list = []
112
+ for (const target in targets) list.push(target)
113
+ return list
114
+ }
115
+
116
+ /**
117
+ * The pointer is at a canvas pixel: targets it entered or left fire, and
118
+ * every one it is on fires a move.
119
+ *
120
+ * @param {number} x
121
+ * @param {number} y
122
+ */
123
+ function moved(x, y) {
124
+ const list = names()
125
+ for (let n = 0; n < list.length; n++) {
126
+ const target = list[n]
127
+ const isNow = isOn(target, x, y)
128
+ if (isNow && !inside[target]) {
129
+ inside[target] = true
130
+ if (listensTo(target, 'PointerEnter')) options.fire('PointerEnter', target)
131
+ } else if (!isNow && inside[target]) {
132
+ inside[target] = false
133
+ pressed[target] = false
134
+ if (listensTo(target, 'PointerExit')) options.fire('PointerExit', target)
135
+ }
136
+ if (isNow && listensTo(target, 'PointerMove')) options.fire('PointerMove', target)
137
+ }
138
+ }
139
+
140
+ /**
141
+ * @param {number} x
142
+ * @param {number} y
143
+ */
144
+ function pressedAt(x, y) {
145
+ moved(x, y)
146
+ const list = names()
147
+ for (let n = 0; n < list.length; n++) {
148
+ const target = list[n]
149
+ if (!isOn(target, x, y)) continue
150
+ pressed[target] = true
151
+ if (listensTo(target, 'PointerDown')) options.fire('PointerDown', target)
152
+ }
153
+ }
154
+
155
+ /**
156
+ * @param {number} x
157
+ * @param {number} y
158
+ */
159
+ function releasedAt(x, y) {
160
+ const list = names()
161
+ for (let n = 0; n < list.length; n++) {
162
+ const target = list[n]
163
+ const wasPressed = pressed[target]
164
+ pressed[target] = false
165
+ if (!isOn(target, x, y)) continue
166
+ if (listensTo(target, 'PointerUp')) options.fire('PointerUp', target)
167
+ // a click on the whole canvas is the canvas's own `click` event
168
+ if (wasPressed && target !== '' && options.clicks && listensTo(target, 'Click')) options.fire('Click', target)
169
+ }
170
+ }
171
+
172
+ /** The pointer left the canvas, or its touch ended: every target it was on is exited. */
173
+ function left() {
174
+ const list = names()
175
+ for (let n = 0; n < list.length; n++) {
176
+ const target = list[n]
177
+ pressed[target] = false
178
+ if (!inside[target]) continue
179
+ inside[target] = false
180
+ if (listensTo(target, 'PointerExit')) options.fire('PointerExit', target)
181
+ }
182
+ }
183
+
184
+ /**
185
+ * A client position as canvas pixels, which the hit geometry is in.
186
+ *
187
+ * @param {{ clientX: number, clientY: number }} point
188
+ * @returns {{ x: number, y: number } | null} null while the canvas has no size
189
+ */
190
+ function place(point) {
191
+ const box = canvas.getBoundingClientRect()
192
+ if (!box.width || !box.height) return null
193
+ return { x: ((point.clientX - box.left) * canvas.width) / box.width, y: ((point.clientY - box.top) * canvas.height) / box.height }
194
+ }
195
+
196
+ /**
197
+ * @param {(x: number, y: number) => void} then
198
+ * @param {{ clientX: number, clientY: number } | undefined} point
199
+ */
200
+ function at(then, point) {
201
+ const where = point ? place(point) : null
202
+ if (where) then(where.x, where.y)
203
+ }
204
+
205
+ /** @param {any} event */
206
+ const isMouseAfterTouch = (event) => event.type.indexOf('mouse') === 0 && Date.now() - lastTouch < TOUCH_MOUSE_MS
207
+
208
+ /** @param {any} event */
209
+ const firstTouch = (event) => (event.changedTouches && event.changedTouches[0]) || (event.touches && event.touches[0])
210
+
211
+ /** @type {Record<string, (event: any) => void>} */
212
+ const handlers = {
213
+ // pen, touch and mouse together
214
+ pointerdown: (event) => {
215
+ if (event.button) return
216
+ at(pressedAt, event)
217
+ },
218
+ pointermove: (event) => at(moved, event),
219
+ pointerup: (event) => {
220
+ if (event.button) return
221
+ at(releasedAt, event)
222
+ // a finger has no hover: lifting it leaves
223
+ if (event.pointerType && event.pointerType !== 'mouse') left()
224
+ },
225
+ pointercancel: () => left(),
226
+ pointerleave: () => left(),
227
+
228
+ // the browsers before pointer events
229
+ mousedown: (event) => {
230
+ if (isMouseAfterTouch(event) || event.button) return
231
+ at(pressedAt, event)
232
+ },
233
+ mousemove: (event) => {
234
+ if (!isMouseAfterTouch(event)) at(moved, event)
235
+ },
236
+ mouseup: (event) => {
237
+ if (isMouseAfterTouch(event) || event.button) return
238
+ at(releasedAt, event)
239
+ },
240
+ mouseleave: (event) => {
241
+ if (!isMouseAfterTouch(event)) left()
242
+ },
243
+ touchstart: (event) => {
244
+ lastTouch = Date.now()
245
+ at(pressedAt, firstTouch(event))
246
+ },
247
+ touchmove: (event) => {
248
+ lastTouch = Date.now()
249
+ at(moved, firstTouch(event))
250
+ },
251
+ touchend: (event) => {
252
+ lastTouch = Date.now()
253
+ at(releasedAt, firstTouch(event))
254
+ left()
255
+ },
256
+ touchcancel: () => {
257
+ lastTouch = Date.now()
258
+ left()
259
+ },
260
+ }
261
+
262
+ function bind() {
263
+ const events = window.PointerEvent
264
+ ? [ 'pointerdown', 'pointermove', 'pointerup', 'pointercancel', 'pointerleave' ]
265
+ : [ 'mousedown', 'mousemove', 'mouseup', 'mouseleave', 'touchstart', 'touchmove', 'touchend', 'touchcancel' ]
266
+ for (let n = 0; n < events.length; n++) {
267
+ bound.push({ name: events[n], listener: handlers[events[n]] })
268
+ canvas.addEventListener(events[n], handlers[events[n]], false)
269
+ }
270
+ isBound = true
271
+ }
272
+
273
+ function unbind() {
274
+ for (let n = 0; n < bound.length; n++) canvas.removeEventListener(bound[n].name, bound[n].listener, false)
275
+ bound.length = 0
276
+ isBound = false
277
+ }
278
+
279
+ return {
280
+ listen,
281
+ destroy() {
282
+ unbind()
283
+ targets = {}
284
+ },
285
+ /** The targets the pointer is on now, for tests and hosts */
286
+ get hovering() {
287
+ const list = []
288
+ for (const target in inside) if (inside[target] && target !== '') list.push(target)
289
+ return list
290
+ },
291
+ }
292
+ }
package/readme.md ADDED
@@ -0,0 +1,270 @@
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, a blend's first animation, 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 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
+
130
+ ## Audio
131
+
132
+ 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).
133
+
134
+ - **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.
135
+ - **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.
136
+ - **`control('pause')`** pauses what is sounding and `play` continues it; **`stop`** silences and rewinds it. **`muted`** plays nothing and silences what is sounding.
137
+ - **Overlap:** a sound can sound up to four times at once; a fifth restarts the oldest.
138
+ - **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.
139
+ - **Formats:** whatever the browser decodes; Rive audio is WAV, MP3 or FLAC, and FLAC is not played in browsers before about 2017.
140
+
141
+ ## The renderer
142
+
143
+ `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.
144
+
145
+ - **Slots are live:** a property with a slot id reads the slot table first, so `setInputs` costs nothing to draw.
146
+ - **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%.
147
+
148
+ ## Pointer events
149
+
150
+ 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.
151
+
152
+ | Interaction | Runs when |
153
+ | --- | --- |
154
+ | `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 |
155
+ | `PointerMove` | The pointer moves while on the target |
156
+ | `PointerDown`, `PointerUp` | A press or release with the pointer on the target |
157
+ | `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 |
158
+
159
+ - **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.
160
+ - **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.
161
+ - **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.
162
+ - **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 or its opacity (an invisible shape still hits, and a stroke is not widened), 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).
163
+
164
+ ## Focus and keys
165
+
166
+ 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`).
167
+
168
+ **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.
169
+
170
+ - **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.
171
+ - **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.
172
+ - **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.
173
+
174
+ ### Automatic (`keys: 'auto'`, the default)
175
+
176
+ 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.
177
+
178
+ | Event | Does |
179
+ | --- | --- |
180
+ | 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 |
181
+ | 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 |
182
+ | `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 |
183
+ | Tab off the last stop | Left to the browser, which moves DOM focus on; keys are ignored until focus is given again |
184
+ | DOM focus leaves | Rive's focus is cleared (Blur listeners run), unless the window itself lost focus |
185
+
186
+ `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.
187
+
188
+ ### Manual (`motion.key` and friends, always available)
189
+
190
+ 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.
191
+
192
+ | Call | Does |
193
+ | --- | --- |
194
+ | `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 |
195
+ | `motion.keyUp(key)` | The release, for listeners with the up phase |
196
+ | `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 |
197
+ | `motion.blur()` | Clears Rive's focus; Blur listeners run |
198
+ | `motion.focused` | The focused target's `rid`, or null |
199
+ | `motion.click(target?)` | Without a target, every Click listener (as OK); with one, a press and release on it |
200
+
201
+ 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.
202
+
203
+ **Visible focus.** Rive draws focus itself, through the focus listeners the file has (a state, a view-model colour); Motion draws no ring.
204
+
205
+ **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.
206
+
207
+ **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.
208
+
209
+ ## Layers, Blends and Cross-fades
210
+
211
+ 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.
212
+
213
+ | Rive | Plays as |
214
+ | --- | --- |
215
+ | **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 |
216
+ | **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 |
217
+ | **Direct blend** | Each animation weighted by its own input or view model property (0-100, clamped) or its fixed mix |
218
+ | **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 |
219
+ | **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 |
220
+
221
+ - **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.
222
+ - **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.
223
+ - **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.
224
+ - **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.
225
+ - **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.
226
+ - **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.
227
+ - **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.
228
+ - **Limits.**
229
+ - 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.
230
+ - 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.
231
+ - A cross-fade follows a cubic `ease`; an elastic or `hold` transition interpolation is linear here.
232
+ - 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.
233
+ - 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.
234
+
235
+ ## Layout
236
+
237
+ | Path | Holds |
238
+ | --- | --- |
239
+ | `motion.js` | `createMotion`: wires the parts below |
240
+ | `audio/` | Sound on `HTMLAudioElement`: preloading, voices, cue timing |
241
+ | `bundle/` | Loading motion.json and its files over XMLHttpRequest; registering its fonts |
242
+ | `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 |
243
+ | `focus/` | Rive's focus: the tree and traversal, directional moves, key routing, and the canvas's focus and key events |
244
+ | `hit/` | Pointer hit testing: the paths each targeted group drew, and point-in-shape maths |
245
+ | `list/` | Lists: a row artboard drawn for each item of a view-model list, and the small flex that places them |
246
+ | `lottie/` | The canvas renderer: properties, paths, shapes, paint, layers |
247
+ | `mixer/` | The pose mixer: mixing property values, the stage of what each layer plays, the mixed picture |
248
+ | `motion/` | The state machine, the flow, keys, live values, data converters, nested artboards' machines |
249
+ | `player/` | The clock and canvas, fit and alignment, raster sheets, `overlay/` (text overlays: drawing and run layout) |
250
+ | `pointer/` | Mouse, touch and pen events → pointer interactions: hover per target, press, click |
251
+
252
+ ## Tests
253
+
254
+ ```sh
255
+ npm run motion web test # unit tests, the motion-stress scenarios, and the browser tests
256
+ npm run motion web typecheck
257
+ npm run motion web build:legacy # .artifacts/dist/motion-web.legacy(.min).js, ES5 for the browserslist
258
+ npm run motion web legacy test # the legacy build in real old Chrome and Firefox, in Docker
259
+ npm run motion web parity # the parity scorecard against Rive's canvas runtime, to .artifacts/parity/report.md
260
+ ```
261
+
262
+ - **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.
263
+ - **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.
264
+ - **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.
265
+ - **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.
266
+ - **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).
267
+ - **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.
268
+ - **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.
269
+ - **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.
270
+ - **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.
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.