@px-petals/roku.motion-web 1.0.0 → 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.
Files changed (77) hide show
  1. package/bands/bands.js +3 -1
  2. package/bundle/bundle.js +56 -0
  3. package/list/list.js +12 -1
  4. package/lottie/lottie.layers.js +64 -11
  5. package/lottie/lottie.paint.js +30 -0
  6. package/lottie/lottie.paths.js +19 -4
  7. package/lottie/lottie.props.js +18 -8
  8. package/lottie/lottie.shapes.js +2 -1
  9. package/mixer/mixer.js +31 -8
  10. package/mixer/mixer.mix.js +17 -0
  11. package/motion/motion.convert.js +32 -1
  12. package/motion/motion.flow.js +20 -2
  13. package/motion/motion.machine.js +28 -13
  14. package/motion/motion.values.js +13 -9
  15. package/motion.js +196 -20
  16. package/package.json +10 -16
  17. package/player/player.js +25 -5
  18. package/readme.md +78 -3
  19. package/script/luau/lib/base/base.js +202 -0
  20. package/script/luau/lib/base/base.table.js +189 -0
  21. package/script/luau/lib/lib.args.js +152 -0
  22. package/script/luau/lib/lib.js +23 -0
  23. package/script/luau/lib/math/math.bit32.js +144 -0
  24. package/script/luau/lib/math/math.js +288 -0
  25. package/script/luau/lib/math/math.vector.js +125 -0
  26. package/script/luau/lib/string/string.format.js +143 -0
  27. package/script/luau/lib/string/string.js +160 -0
  28. package/script/luau/lib/string/string.pattern.js +505 -0
  29. package/script/luau/load/load.js +362 -0
  30. package/script/luau/load/load.ops.js +79 -0
  31. package/script/luau/luau.convert.js +259 -0
  32. package/script/luau/luau.js +132 -0
  33. package/script/luau/luau.md +111 -0
  34. package/script/luau/vm/vm.js +167 -0
  35. package/script/luau/vm/vm.meta.js +401 -0
  36. package/script/luau/vm/vm.num.js +236 -0
  37. package/script/luau/vm/vm.run.js +842 -0
  38. package/script/luau/vm/vm.value.js +406 -0
  39. package/script/play/play.js +199 -0
  40. package/script/play/play.objects.js +295 -0
  41. package/script/play/play.scope.js +91 -0
  42. package/script/rive/api/api.statics.js +31 -0
  43. package/script/rive/api/api.type.js +188 -0
  44. package/script/rive/api/artboard/artboard.js +89 -0
  45. package/script/rive/api/assets/assets.js +72 -0
  46. package/script/rive/api/data/data.value.js +99 -0
  47. package/script/rive/api/draw/draw.contours.js +163 -0
  48. package/script/rive/api/draw/draw.measure.js +196 -0
  49. package/script/rive/api/draw/draw.paint.js +235 -0
  50. package/script/rive/api/draw/draw.path.js +153 -0
  51. package/script/rive/api/draw/draw.renderer.js +117 -0
  52. package/script/rive/api/math/math.color.js +96 -0
  53. package/script/rive/api/math/math.mat2d.js +180 -0
  54. package/script/rive/api/math/math.vector.js +121 -0
  55. package/script/rive/api/media/media.audio.js +113 -0
  56. package/script/rive/api/media/media.draw.js +73 -0
  57. package/script/rive/api/media/media.image.js +61 -0
  58. package/script/rive/api/media/media.mesh.js +50 -0
  59. package/script/rive/api/pointer/pointer.event.js +64 -0
  60. package/script/rive/api/pointer/pointer.route.js +69 -0
  61. package/script/rive/api/text/text.font.js +129 -0
  62. package/script/rive/api/text/text.js +278 -0
  63. package/script/rive/api/text/text.layout.js +259 -0
  64. package/script/rive/context/context.js +76 -0
  65. package/script/rive/context/context.memory.js +196 -0
  66. package/script/rive/context/context.viewmodel.js +302 -0
  67. package/script/rive/context/lists/lists.js +160 -0
  68. package/script/rive/context/lists/lists.values.js +281 -0
  69. package/script/rive/host/host.globals.js +44 -0
  70. package/script/rive/host/host.js +533 -0
  71. package/script/rive/host/host.node.js +45 -0
  72. package/script/rive/rive.js +23 -0
  73. package/script/rive/rive.md +180 -0
  74. package/script/script.js +11 -0
  75. package/skin/skin.deform.js +514 -0
  76. package/skin/skin.ik.js +260 -0
  77. package/skin/skin.js +297 -0
package/readme.md CHANGED
@@ -114,18 +114,19 @@ A bundle's `lists` (from a Rive `ArtboardComponentList`) says how to draw a view
114
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
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
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 |
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
118
 
119
119
  ## Data binding
120
120
 
121
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
122
 
123
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`).
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
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
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
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
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.
129
130
 
130
131
  ## Audio
131
132
 
@@ -159,7 +160,7 @@ Pointer listeners answer where the pointer is. A listener that watches a shape o
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.
160
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.
161
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.
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
+ - **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).
163
164
 
164
165
  ## Focus and keys
165
166
 
@@ -220,6 +221,7 @@ Rive plays several animations of one artboard at once and mixes what they do. A
220
221
 
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.
222
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`).
223
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.
224
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.
225
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.
@@ -248,6 +250,74 @@ Rive plays several animations of one artboard at once and mixes what they do. A
248
250
  | `motion/` | The state machine, the flow, keys, live values, data converters, nested artboards' machines |
249
251
  | `player/` | The clock and canvas, fit and alignment, raster sheets, `overlay/` (text overlays: drawing and run layout) |
250
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).
251
321
 
252
322
  ## Tests
253
323
 
@@ -257,6 +327,7 @@ npm run motion web typecheck
257
327
  npm run motion web build:legacy # .artifacts/dist/motion-web.legacy(.min).js, ES5 for the browserslist
258
328
  npm run motion web legacy test # the legacy build in real old Chrome and Firefox, in Docker
259
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)
260
331
  ```
261
332
 
262
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.
@@ -264,7 +335,11 @@ npm run motion web parity # the parity scorecard against Rive's canvas r
264
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.
265
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.
266
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.
267
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).
268
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.
269
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.
270
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
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The `table` library.
3
+ */
4
+
5
+ import { LuaTable, fail, typeName } from '../../vm/vm.value.js'
6
+ import { lessThan, truthy } from '../../vm/vm.meta.js'
7
+ import { argError, checkFunction, checkInt, checkTable, optInt, register } from '../lib.args.js'
8
+ import { unpack } from './base.js'
9
+
10
+ /** @param {LuaTable} t */
11
+ function writable(t) {
12
+ if (t.frozen) throw fail('attempt to modify a readonly table')
13
+ }
14
+
15
+ /**
16
+ * Sorts `list` in place with `less`, stably (merge sort).
17
+ *
18
+ * @param {any[]} list
19
+ * @param {(a: any, b: any) => boolean} less
20
+ */
21
+ function sort(list, less) {
22
+ const n = list.length
23
+ if (n < 2) return
24
+ let src = list
25
+ let dst = new Array(n)
26
+ for (let width = 1; width < n; width *= 2) {
27
+ for (let lo = 0; lo < n; lo += 2 * width) {
28
+ const mid = Math.min(lo + width, n)
29
+ const hi = Math.min(lo + 2 * width, n)
30
+ let i = lo
31
+ let j = mid
32
+ let o = lo
33
+ while (i < mid && j < hi) dst[o++] = less(src[j], src[i]) ? src[j++] : src[i++]
34
+ while (i < mid) dst[o++] = src[i++]
35
+ while (j < hi) dst[o++] = src[j++]
36
+ }
37
+ const swap = src
38
+ src = dst
39
+ dst = swap
40
+ }
41
+ if (src !== list) for (let i = 0; i < n; i++) list[i] = src[i]
42
+ }
43
+
44
+ /**
45
+ * @param {import('../../vm/vm.js').VM} vm
46
+ * @param {LuaTable} G
47
+ */
48
+ export function installTable(vm, G) {
49
+ const table = new LuaTable()
50
+ G.set('table', table)
51
+
52
+ register(table, {
53
+ insert(vm, args) {
54
+ const t = checkTable(args, 0, 'insert')
55
+ writable(t)
56
+ const n = t.length()
57
+ if (args.length === 2) {
58
+ t.set(n + 1, args[1])
59
+ return []
60
+ }
61
+ if (args.length !== 3) throw fail("wrong number of arguments to 'insert'")
62
+ const pos = checkInt(args, 1, 'insert')
63
+ if (pos < 1 || pos > n + 1) throw argError(2, 'insert', 'position out of bounds')
64
+ if (args[2] === undefined) return []
65
+ t.arr.splice(pos - 1, 0, args[2])
66
+ if (t.hn !== null) t.migrate()
67
+ return []
68
+ },
69
+
70
+ remove(vm, args) {
71
+ const t = checkTable(args, 0, 'remove')
72
+ writable(t)
73
+ const n = t.length()
74
+ const pos = optInt(args, 1, 'remove', n)
75
+ if (n === 0 && args[1] === undefined) return undefined
76
+ if (pos < 1 || pos > n) {
77
+ if (args[1] !== undefined && pos !== n + 1) throw argError(2, 'remove', 'position out of bounds')
78
+ return undefined
79
+ }
80
+ const v = t.arr[pos - 1]
81
+ t.arr.splice(pos - 1, 1)
82
+ let m = t.arr.length
83
+ while (m > 0 && t.arr[m - 1] === undefined) m--
84
+ t.arr.length = m
85
+ return v
86
+ },
87
+
88
+ concat(vm, args) {
89
+ const t = checkTable(args, 0, 'concat')
90
+ const sep = args[1] === undefined ? '' : String(args[1])
91
+ const i = optInt(args, 2, 'concat', 1)
92
+ const j = optInt(args, 3, 'concat', t.length())
93
+ const parts = []
94
+ for (let n = i; n <= j; n++) {
95
+ const v = t.get(n)
96
+ if (typeof v === 'string') parts.push(v)
97
+ else if (typeof v === 'number') parts.push(vm.tostring(v))
98
+ else throw fail('invalid value (' + typeName(v) + ') at index ' + n + " in table for 'concat'")
99
+ }
100
+ return parts.join(sep)
101
+ },
102
+
103
+ unpack(vm, args) {
104
+ return unpack(checkTable(args, 0, 'unpack'), args[1], args[2])
105
+ },
106
+
107
+ pack(vm, args) {
108
+ const t = new LuaTable()
109
+ t.setRange(1, args, 0, args.length)
110
+ t.set('n', args.length)
111
+ return t
112
+ },
113
+
114
+ sort(vm, args) {
115
+ const t = checkTable(args, 0, 'sort')
116
+ writable(t)
117
+ const cmp = args[1]
118
+ if (cmp !== undefined) checkFunction(args, 1, 'sort')
119
+ const n = t.length()
120
+ const list = t.arr.slice(0, n)
121
+ /** @type {(a: any, b: any) => boolean} */
122
+ const less = cmp === undefined
123
+ ? function (a, b) { return lessThan(vm, a, b) }
124
+ : function (a, b) { return truthy(vm.call(cmp, [ a, b ])[0]) }
125
+ sort(list, less)
126
+ for (let i = 0; i < n; i++) t.arr[i] = list[i]
127
+ return []
128
+ },
129
+
130
+ find(vm, args) {
131
+ const t = checkTable(args, 0, 'find')
132
+ const init = optInt(args, 2, 'find', 1)
133
+ const n = t.length()
134
+ for (let i = init; i <= n; i++) if (t.get(i) === args[1]) return i
135
+ return undefined
136
+ },
137
+
138
+ clear(vm, args) {
139
+ const t = checkTable(args, 0, 'clear')
140
+ writable(t)
141
+ t.arr.length = 0
142
+ for (let s = 0; s < t.hv.length; s++) t.hv[s] = undefined
143
+ t.dead = t.hv.length
144
+ return []
145
+ },
146
+
147
+ create(vm, args) {
148
+ const n = checkInt(args, 0, 'create')
149
+ if (n < 0) throw argError(1, 'create', 'size out of range')
150
+ const t = new LuaTable()
151
+ if (args[1] !== undefined) for (let i = 1; i <= n; i++) t.set(i, args[1])
152
+ return t
153
+ },
154
+
155
+ clone(vm, args) {
156
+ return checkTable(args, 0, 'clone').clone()
157
+ },
158
+
159
+ freeze(vm, args) {
160
+ const t = checkTable(args, 0, 'freeze')
161
+ if (t.meta !== undefined && t.meta.getStr('__metatable') !== undefined) throw argError(1, 'freeze', 'table has a protected metatable')
162
+ t.frozen = true
163
+ return t
164
+ },
165
+
166
+ isfrozen(vm, args) {
167
+ return checkTable(args, 0, 'isfrozen').frozen
168
+ },
169
+
170
+ getn(vm, args) {
171
+ return checkTable(args, 0, 'getn').length()
172
+ },
173
+
174
+ move(vm, args) {
175
+ const a1 = checkTable(args, 0, 'move')
176
+ const f = checkInt(args, 1, 'move')
177
+ const e = checkInt(args, 2, 'move')
178
+ const t = checkInt(args, 3, 'move')
179
+ const a2 = args[4] === undefined ? a1 : checkTable(args, 4, 'move')
180
+ writable(a2)
181
+ if (e >= f) {
182
+ if (!(f > 0 || e < Number.MAX_VALUE + f)) throw argError(3, 'move', 'too many elements to move')
183
+ if (t > e || t <= f || a1 !== a2) for (let i = 0; i <= e - f; i++) a2.set(t + i, a1.get(f + i))
184
+ else for (let i = e - f; i >= 0; i--) a2.set(t + i, a1.get(f + i))
185
+ }
186
+ return a2
187
+ },
188
+ }, 'table.')
189
+ }