@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
@@ -0,0 +1,533 @@
1
+ /**
2
+ * The Rive scripting host: runs one compiled script on the Luau VM the way
3
+ * Rive's runtime does, and gives the player what the script does (view-model
4
+ * writes through the adapter, drawings, a path effect's output).
5
+ *
6
+ * A compiled script's main chunk returns a factory function. The host calls
7
+ * it to make the script's table (`self`), sets the script's inputs on it, then
8
+ * drives it with `self` as the first argument:
9
+ *
10
+ * - `init(self, context)` once, returning false to refuse;
11
+ * - `advance(self, seconds)` each frame while it keeps returning true (a false
12
+ * stops them until a view-model change or an input wakes it);
13
+ * - `update(self)` when an input changed or `context:markNeedsUpdate()` asked
14
+ * (a request made from inside `update` is ignored, as in Rive);
15
+ * - `draw(self, renderer)` each frame, which the host records;
16
+ * - for a path effect, `update(self, path, node)` returns the new path;
17
+ * - for a converter, `convert(self, value)` and `reverseConvert(self, value)`;
18
+ * - for a listener action, `perform(self, pointerEvent)`;
19
+ * - for a transition condition, `evaluate(self)`;
20
+ * - for a layout, `measure(self)` and `resize(self, size)`.
21
+ */
22
+
23
+ import { createLuau } from '../../luau/luau.js'
24
+ import { LuaClosure, LuaTable, NativeFunction, Vector } from '../../luau/vm/vm.value.js'
25
+ import { LuauError, encodeUtf8 } from '../../luau/luau.convert.js'
26
+ import { Context } from '../context/context.js'
27
+ import { viewModelAt } from '../context/context.viewmodel.js'
28
+ import { commandsToContours } from '../api/draw/draw.contours.js'
29
+ import { Path, pathFromContours } from '../api/draw/draw.path.js'
30
+ import { Recorder, Renderer } from '../api/draw/draw.renderer.js'
31
+ import { dataValue, readDataValue } from '../api/data/data.value.js'
32
+ import { installGlobals } from './host.globals.js'
33
+ import { NodeReadData } from './host.node.js'
34
+ import { createAssets } from '../api/assets/assets.js'
35
+ import { PointerEvent, pointerData } from '../api/pointer/pointer.event.js'
36
+
37
+ /** @import { Contour } from '../api/draw/draw.contours.js' */
38
+ /** @import { Drawing } from '../api/draw/draw.renderer.js' */
39
+ /** @import { Scope, ViewModelAdapter } from '../context/context.viewmodel.js' */
40
+ /** @import { NodeInfo } from './host.node.js' */
41
+
42
+ /** a pointer event at the listener's origin, shared since a Vector is never mutated */
43
+ const ORIGIN = new Vector(0, 0, 0)
44
+
45
+ /**
46
+ * @typedef {object} HostOptions
47
+ * @property {import('../../luau/luau.js').Luau} [luau] a VM to run in; default a new one
48
+ * @property {Uint8Array} [bytes] the script's bytecode (see `load`)
49
+ * @property {ViewModelAdapter | null} [viewModel] the view model bound to the script's node, `context:viewModel()`; null for none
50
+ * @property {ViewModelAdapter | null} [rootViewModel] `context:rootViewModel()`; default the same as `viewModel`
51
+ * @property {ViewModelAdapter | null} [parentViewModel] the enclosing data context's, `dataContext():parent():viewModel()`
52
+ * @property {Record<string, ViewModelAdapter>} [globalViewModels] `context:globalViewModel(name)`
53
+ * @property {Record<string, any>} [inputs] the script's inputs by name: numbers, booleans, strings, ARGB colour numbers, or a view-model adapter for a view-model input
54
+ * @property {(message: string) => void} [log] where `print` and `context.log` write
55
+ * @property {(error: Error) => void} [onError] where an error a script raises goes; default logs it
56
+ * @property {boolean} [strict] rethrow a script's errors instead of reporting them
57
+ * @property {(drawings: Drawing[]) => void} [onDraw] called with each frame's drawings
58
+ * @property {import('../api/assets/assets.js').AssetSource} [assets] the images, fonts, audio and artboards the player has, by name; without it every name is accepted (an image of unknown size, the fallback font)
59
+ */
60
+
61
+ /**
62
+ * A script's bytecode without Rive's wrapper: a script asset stores one byte
63
+ * (0) before the Luau version byte, and valid bytecode never starts with 0.
64
+ *
65
+ * @param {Uint8Array} bytes
66
+ * @returns {Uint8Array}
67
+ */
68
+ export function stripWrapper(bytes) {
69
+ return bytes.length > 1 && bytes[0] === 0 ? bytes.subarray(1) : bytes
70
+ }
71
+
72
+ /** One script, instantiated */
73
+ export class ScriptHost {
74
+ /** @param {HostOptions} [options] */
75
+ constructor(options) {
76
+ const o = options || {}
77
+ this.options = o
78
+ /** @type {string[]} */
79
+ this.logged = []
80
+ /** @type {Error[]} errors the script raised, in order */
81
+ this.errors = []
82
+ this.luau = o.luau || createLuau({ print: (text) => this.log(text) })
83
+ /** @type {import('../api/media/media.audio.js').AudioEvent[]} */
84
+ this.audioEvents = []
85
+ this.assets = createAssets(o.assets)
86
+ installGlobals(this.luau, { audio: this.audioEvents, fallback: this.assets.fallback })
87
+ this.recorder = new Recorder()
88
+ /** @type {Record<number, Vector>} where each pointer was last */
89
+ this.pointers = {}
90
+ /** @type {LuaTable | null} the script's own table */
91
+ this.self = null
92
+ /** @type {{ adapter: ViewModelAdapter, scope: Scope }[]} */
93
+ this.scopes = []
94
+ /** `update` is owed */
95
+ this.needsUpdate = false
96
+ /** `advance` is still wanted */
97
+ this.advancing = true
98
+ /** `update` is running, when `markNeedsUpdate` does nothing */
99
+ this.updating = false
100
+ this.initialized = false
101
+ /** false once `init` returned false or raised */
102
+ this.running = true
103
+ this.context = Context.wrap(this.contextEnv())
104
+ /** the DataValue and the PointerEvent reused for each protocol call, so a call allocates none of its own @type {{ in: any, event: any, eventData: any }} */
105
+ this.scratch = { in: null, event: null, eventData: null }
106
+ if (o.bytes) this.load(o.bytes)
107
+ }
108
+
109
+ /**
110
+ * @param {string} message
111
+ */
112
+ log(message) {
113
+ this.logged.push(message)
114
+ if (this.options.log) this.options.log(message)
115
+ }
116
+
117
+ /**
118
+ * What the Context stands on.
119
+ *
120
+ * @returns {import('../context/context.js').ContextEnv}
121
+ */
122
+ contextEnv() {
123
+ const o = this.options
124
+ const viewModel = o.viewModel || null
125
+ return {
126
+ scopeFor: (adapter) => this.scopeFor(adapter),
127
+ immediate: viewModel,
128
+ root: o.rootViewModel === undefined ? viewModel : o.rootViewModel,
129
+ parent: o.parentViewModel || null,
130
+ globals: o.globalViewModels || {},
131
+ assets: this.assets,
132
+ markNeedsUpdate: () => {
133
+ if (!this.updating) this.needsUpdate = true
134
+ },
135
+ log: (message) => this.log(message),
136
+ }
137
+ }
138
+
139
+ /**
140
+ * @param {ViewModelAdapter} adapter
141
+ * @returns {Scope}
142
+ */
143
+ scopeFor(adapter) {
144
+ for (let n = 0; n < this.scopes.length; n++) if (this.scopes[n].adapter === adapter) return this.scopes[n].scope
145
+ /** @type {Scope} */
146
+ const scope = {
147
+ adapter,
148
+ props: {},
149
+ scopeFor: (a) => this.scopeFor(a),
150
+ asset: (kind, name) => (kind === 'image' ? this.assets.image(name) : kind === 'font' ? this.assets.font(name) : undefined),
151
+ call: (fn, args) => {
152
+ this.guard(() => this.luau.callRaw(fn, args))
153
+ },
154
+ wake: () => {
155
+ this.advancing = true
156
+ },
157
+ }
158
+ this.scopes.push({ adapter, scope })
159
+ return scope
160
+ }
161
+
162
+ /**
163
+ * Runs host work that calls into Lua, keeping a script's error out of the player.
164
+ *
165
+ * @template T
166
+ * @param {() => T} work
167
+ * @returns {T | undefined}
168
+ */
169
+ guard(work) {
170
+ try {
171
+ return work()
172
+ } catch (e) {
173
+ if (!(e instanceof LuauError) || this.options.strict) throw e
174
+ this.errors.push(e)
175
+ if (this.options.onError) this.options.onError(e)
176
+ else this.log('error: ' + e.message)
177
+ return undefined
178
+ }
179
+ }
180
+
181
+ // Loading
182
+ // ■■■■■■■
183
+
184
+ /**
185
+ * Loads the script and makes its table. The bytes may still carry Rive's
186
+ * wrapper byte (see `stripWrapper`).
187
+ *
188
+ * @param {Uint8Array} bytes
189
+ * @returns {this}
190
+ */
191
+ load(bytes) {
192
+ const module = this.luau.load(stripWrapper(bytes))
193
+ const returned = this.luau.callRaw(this.luau.vm.instantiate(module), [])
194
+ const factory = returned[0]
195
+ if (!(factory instanceof NativeFunction) && !(factory instanceof LuaClosure)) throw new Error("a script's main chunk must return a factory function")
196
+ const made = this.luau.callRaw(factory, [])[0]
197
+ if (!(made instanceof LuaTable)) throw new Error("a script's factory must return a table")
198
+ this.self = made
199
+ const inputs = this.options.inputs
200
+ if (inputs) for (const name in inputs) this.setValue(name, inputs[name])
201
+ return this
202
+ }
203
+
204
+ /**
205
+ * @param {string} name
206
+ * @param {any} value
207
+ */
208
+ setValue(name, value) {
209
+ if (!this.self) throw new Error('no script is loaded')
210
+ this.self.set(encodeUtf8(name), this.inputValue(value))
211
+ }
212
+
213
+ /**
214
+ * @param {any} value
215
+ * @returns {any}
216
+ */
217
+ inputValue(value) {
218
+ if (typeof value === 'string') return encodeUtf8(value)
219
+ if (value && typeof value === 'object' && typeof value.kind === 'string' && typeof value.name === 'string') return this.assets.input(value)
220
+ if (value && typeof value === 'object' && typeof value.subscribe === 'function') return viewModelAt(this.scopeFor(value), '')
221
+ return value === null ? undefined : value
222
+ }
223
+
224
+ /**
225
+ * Sets a script input, as the player does when it changes (an animated or
226
+ * data-bound input); `update` runs next.
227
+ *
228
+ * @param {string} name
229
+ * @param {any} value
230
+ */
231
+ setInput(name, value) {
232
+ this.setValue(name, value)
233
+ this.needsUpdate = true
234
+ this.advancing = true
235
+ }
236
+
237
+ /**
238
+ * @param {string} name
239
+ * @returns {any} the method, or undefined when the script has none
240
+ */
241
+ method(name) {
242
+ const fn = this.self ? this.self.getStr(name) : undefined
243
+ return fn === undefined ? undefined : fn
244
+ }
245
+
246
+ /**
247
+ * @param {string} name
248
+ * @returns {boolean}
249
+ */
250
+ has(name) {
251
+ return this.method(name) !== undefined
252
+ }
253
+
254
+ /**
255
+ * Calls a method with `self` first.
256
+ *
257
+ * @param {string} name
258
+ * @param {any[]} args
259
+ * @returns {{ ok: boolean, result: any }}
260
+ */
261
+ invoke(name, args) {
262
+ const fn = this.method(name)
263
+ if (fn === undefined) return { ok: true, result: undefined }
264
+ let ok = false
265
+ /** @type {any} */
266
+ let result
267
+ this.guard(() => {
268
+ result = this.luau.callRaw(fn, [ this.self ].concat(args))[0]
269
+ ok = true
270
+ })
271
+ return { ok, result }
272
+ }
273
+
274
+ // Protocols
275
+ // ■■■■■■■■■
276
+
277
+ /**
278
+ * Converter scripts: `convert(self, input)` turns a view-model value into
279
+ * the target's. A number, string or boolean goes in by its type; a colour
280
+ * (an ARGB number) by `kind`.
281
+ *
282
+ * @param {any} value
283
+ * @param {string} [kind] `color` for a colour
284
+ * @returns {{ kind: string, value: any } | undefined} what the script returned (a colour is its ARGB number); undefined when the script has no such method, failed, or returned something but a DataValue
285
+ */
286
+ convert(value, kind) {
287
+ return this.dataCall('convert', value, kind)
288
+ }
289
+
290
+ /**
291
+ * The same for `reverseConvert(self, input)`, which a two-way bind uses to
292
+ * turn the target's value back into the source's.
293
+ *
294
+ * @param {any} value
295
+ * @param {string} [kind]
296
+ * @returns {{ kind: string, value: any } | undefined}
297
+ */
298
+ reverseConvert(value, kind) {
299
+ return this.dataCall('reverseConvert', value, kind)
300
+ }
301
+
302
+ /**
303
+ * @param {string} name
304
+ * @param {any} value
305
+ * @param {string} [kind]
306
+ * @returns {{ kind: string, value: any } | undefined}
307
+ */
308
+ dataCall(name, value, kind) {
309
+ if (!this.initialized) this.init()
310
+ if (!this.running || !this.has(name)) return undefined
311
+ this.scratch.in = dataValue(this.scratch.in, value, kind)
312
+ const r = this.invoke(name, [ this.scratch.in ])
313
+ const out = r.ok ? readDataValue(r.result) : undefined
314
+ return out ? { kind: out.kind, value: out.value } : undefined
315
+ }
316
+
317
+ /**
318
+ * Listener action scripts: `perform(self, pointerEvent)`.
319
+ *
320
+ * @param {{ id?: number, x?: number, y?: number, previousX?: number, previousY?: number, type?: string, timeStamp?: number }} [event] the pointer event, in the listener's local coordinates; none is a pointer at the origin
321
+ * @returns {{ ok: boolean, hit: string }} `hit` is what the script did with the event: "" nothing, "opaque" or "translucent"
322
+ */
323
+ perform(event) {
324
+ if (!this.initialized) this.init()
325
+ if (!this.running || !this.has('perform')) return { ok: false, hit: '' }
326
+ const e = event || {}
327
+ const s = this.scratch
328
+ if (!s.event) {
329
+ s.eventData = pointerData(0, ORIGIN, ORIGIN, 'move', 0)
330
+ s.event = PointerEvent.wrap(s.eventData)
331
+ }
332
+ const d = s.eventData
333
+ d.id = e.id || 0
334
+ d.position = e.x || e.y ? new Vector(e.x || 0, e.y || 0, 0) : ORIGIN
335
+ d.previous = e.previousX === undefined ? d.position : new Vector(e.previousX, e.previousY || 0, 0)
336
+ d.type = e.type || 'move'
337
+ d.time = e.timeStamp || 0
338
+ d.hit = false
339
+ d.translucent = false
340
+ const r = this.invoke('perform', [ s.event ])
341
+ return { ok: r.ok, hit: d.hit ? (d.translucent ? 'translucent' : 'opaque') : '' }
342
+ }
343
+
344
+ /**
345
+ * Transition condition scripts: `evaluate(self)`; true lets the transition pass.
346
+ *
347
+ * @returns {boolean} false when the script has no `evaluate` or fails
348
+ */
349
+ evaluate() {
350
+ if (!this.initialized) this.init()
351
+ if (!this.running || !this.has('evaluate')) return false
352
+ const r = this.invoke('evaluate', [])
353
+ return r.ok && r.result === true
354
+ }
355
+
356
+ /**
357
+ * Layout scripts: `measure(self)` proposes an ideal size.
358
+ *
359
+ * @returns {{ width: number, height: number } | null} null when the script has no `measure` or returns no vector
360
+ */
361
+ measure() {
362
+ if (!this.initialized) this.init()
363
+ if (!this.running || !this.has('measure')) return null
364
+ const r = this.invoke('measure', [])
365
+ const v = r.result
366
+ return r.ok && v instanceof Vector ? { width: v.x, height: v.y } : null
367
+ }
368
+
369
+ /**
370
+ * Layout scripts: `resize(self, size)`, called when the layout gets its size.
371
+ *
372
+ * @param {number} width
373
+ * @param {number} height
374
+ * @returns {boolean} whether the script ran
375
+ */
376
+ resize(width, height) {
377
+ if (!this.initialized) this.init()
378
+ if (!this.running || !this.has('resize')) return false
379
+ return this.invoke('resize', [ new Vector(width, height, 0) ]).ok
380
+ }
381
+
382
+ // Lifecycle
383
+ // ■■■■■■■■■
384
+
385
+ /**
386
+ * Calls `init`. Whatever it returns but false counts as success; an error
387
+ * or false stops the script.
388
+ *
389
+ * @returns {boolean}
390
+ */
391
+ init() {
392
+ if (this.initialized) return this.running
393
+ this.initialized = true
394
+ const r = this.invoke('init', [ this.context ])
395
+ this.running = r.ok && r.result !== false
396
+ this.needsUpdate = true
397
+ return this.running
398
+ }
399
+
400
+ /**
401
+ * Calls `advance` while the script wants it.
402
+ *
403
+ * @param {number} seconds
404
+ * @returns {boolean} whether the script wants more advances
405
+ */
406
+ advance(seconds) {
407
+ if (!this.running || !this.advancing || !this.has('advance')) return false
408
+ const r = this.invoke('advance', [ seconds ])
409
+ if (!r.ok) {
410
+ this.running = false
411
+ return false
412
+ }
413
+ this.advancing = r.result !== false
414
+ return this.advancing
415
+ }
416
+
417
+ /**
418
+ * Calls `update` if an input changed or an update was asked for.
419
+ *
420
+ * @returns {boolean} whether it ran
421
+ */
422
+ update() {
423
+ if (!this.running || !this.needsUpdate) return false
424
+ this.needsUpdate = false
425
+ if (!this.has('update')) return false
426
+ this.updating = true
427
+ try {
428
+ this.invoke('update', [])
429
+ } finally {
430
+ this.updating = false
431
+ }
432
+ return true
433
+ }
434
+
435
+ /**
436
+ * One frame: `advance`, then `update` when owed.
437
+ *
438
+ * @param {number} seconds
439
+ * @returns {{ advanced: boolean, updated: boolean }}
440
+ */
441
+ frame(seconds) {
442
+ const advanced = this.advance(seconds)
443
+ return { advanced, updated: this.update() }
444
+ }
445
+
446
+ /**
447
+ * Calls `draw` and returns what it drew.
448
+ *
449
+ * @param {Drawing[]} [into] a list to fill (emptied first) instead of a new one, so a player that draws every frame allocates no list
450
+ * @returns {Drawing[]}
451
+ */
452
+ draw(into) {
453
+ const rec = this.recorder
454
+ rec.reset(into)
455
+ if (this.running && this.has('draw')) this.invoke('draw', [ Renderer.wrap(rec) ])
456
+ if (this.options.onDraw) this.options.onDraw(rec.drawings)
457
+ return rec.drawings
458
+ }
459
+
460
+ /**
461
+ * The audio the script started, stopped or seeked since the last call.
462
+ *
463
+ * @returns {import('../api/media/media.audio.js').AudioEvent[]}
464
+ */
465
+ audio() {
466
+ return this.audioEvents.splice(0, this.audioEvents.length)
467
+ }
468
+
469
+ /**
470
+ * A pointer event for the scripted node, at a point in the node's own space:
471
+ * calls `pointerDown`, `pointerMove`, `pointerUp` or `pointerExit` if the
472
+ * script has it.
473
+ *
474
+ * @param {'down' | 'move' | 'up' | 'exit'} type
475
+ * @param {number} x
476
+ * @param {number} y
477
+ * @param {number} [id]
478
+ * @param {number} [seconds] the event's time stamp
479
+ * @returns {{ hit: boolean, translucent: boolean }} whether the script claimed it (`event:hit()`), and let it through
480
+ */
481
+ pointer(type, x, y, id, seconds) {
482
+ const key = id || 0
483
+ const at = new Vector(x, y, 0)
484
+ const name = 'pointer' + type.charAt(0).toUpperCase() + type.slice(1)
485
+ const data = pointerData(key, at, this.pointers[key] || at, name, seconds || 0)
486
+ if (type === 'exit') delete this.pointers[key]
487
+ else this.pointers[key] = at
488
+ if (this.running && this.has(name)) this.invoke(name, [ PointerEvent.wrap(data) ])
489
+ return { hit: data.hit, translucent: data.translucent }
490
+ }
491
+
492
+ /**
493
+ * Runs a path effect: `update(self, path, node)` on a path given as Lottie
494
+ * vertex arrays. A script without `update`, or one that fails, leaves the
495
+ * path as it was.
496
+ *
497
+ * @param {Contour[]} contours
498
+ * @param {NodeInfo} [node]
499
+ * @returns {Contour[]}
500
+ */
501
+ effect(contours, node) {
502
+ if (!this.running || !this.has('update')) return contours
503
+ const path = pathFromContours(contours)
504
+ const data = NodeReadData.wrap({ info: node || {}, path })
505
+ this.updating = true
506
+ this.needsUpdate = false
507
+ let out
508
+ try {
509
+ out = this.invoke('update', [ path, data ])
510
+ } finally {
511
+ this.updating = false
512
+ }
513
+ const made = out.ok ? Path.unwrap(out.result) : undefined
514
+ return made ? commandsToContours(made.cmds) : contours
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Makes a host for a script.
520
+ *
521
+ * ```js
522
+ * const host = createScriptHost({ bytes, viewModel: createMemoryViewModel({ score: 0 }), inputs: { speed: 2 } })
523
+ * host.init()
524
+ * host.frame(1 / 60)
525
+ * const drawings = host.draw()
526
+ * ```
527
+ *
528
+ * @param {HostOptions} [options]
529
+ * @returns {ScriptHost}
530
+ */
531
+ export function createScriptHost(options) {
532
+ return new ScriptHost(options)
533
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `NodeReadData`: the node a path effect belongs to, as the effect's `update`
3
+ * sees it (its transform, and the paint of the stroke or fill the effect is
4
+ * on). The player describes the node in plain numbers.
5
+ */
6
+
7
+ import { Vector } from '../../luau/vm/vm.value.js'
8
+ import { defineType } from '../api/api.type.js'
9
+ import { Mat2D } from '../api/math/math.mat2d.js'
10
+ import { Paint, defaultPaint } from '../api/draw/draw.paint.js'
11
+
12
+ /**
13
+ * @typedef {object} NodeInfo
14
+ * @property {number} [x]
15
+ * @property {number} [y]
16
+ * @property {number} [rotation] radians
17
+ * @property {number} [scaleX]
18
+ * @property {number} [scaleY]
19
+ * @property {number[]} [worldTransform] [xx, xy, yx, yy, tx, ty]
20
+ * @property {import('../api/draw/draw.paint.js').PaintData} [paint] the stroke or fill the effect is on
21
+ */
22
+
23
+ /**
24
+ * @typedef {object} NodeData
25
+ * @property {NodeInfo} info
26
+ * @property {any} path the PathData wrapper the effect is given
27
+ */
28
+
29
+ export const NodeReadData = defineType('NodeReadData', {
30
+ fields: {
31
+ x: { get: (n) => n.info.x || 0 },
32
+ y: { get: (n) => n.info.y || 0 },
33
+ rotation: { get: (n) => n.info.rotation || 0 },
34
+ scaleX: { get: (n) => (n.info.scaleX === undefined ? 1 : n.info.scaleX) },
35
+ scaleY: { get: (n) => (n.info.scaleY === undefined ? 1 : n.info.scaleY) },
36
+ position: { get: (n) => new Vector(n.info.x || 0, n.info.y || 0, 0) },
37
+ scale: { get: (n) => new Vector(n.info.scaleX === undefined ? 1 : n.info.scaleX, n.info.scaleY === undefined ? 1 : n.info.scaleY, 0) },
38
+ worldTransform: { get: (n) => Mat2D.wrap(n.info.worldTransform ? n.info.worldTransform.slice() : [ 1, 0, 0, 1, 0, 0 ]) },
39
+ paint: { get: (n) => Paint.wrap(n.info.paint || defaultPaint()) },
40
+ },
41
+ methods: {
42
+ asPath: (n) => n.path,
43
+ asPaint: (n) => Paint.wrap(n.info.paint || defaultPaint()),
44
+ },
45
+ })
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The Rive scripting host: runs the Luau scripts a Rive file carries (node,
3
+ * drawable and path-effect scripts) on the Luau VM beside it, behind an API
4
+ * a player implements for its view model. See `rive.md`.
5
+ *
6
+ * ```js
7
+ * import { createScriptHost, createMemoryViewModel } from '@px-petals/roku.motion-web/script/rive/rive.js'
8
+ *
9
+ * const host = createScriptHost({ bytes, viewModel: createMemoryViewModel({ score: 0 }), inputs: { speed: 2 } })
10
+ * host.init()
11
+ * host.frame(1 / 60)
12
+ * const drawings = host.draw()
13
+ * ```
14
+ */
15
+
16
+ export { ScriptHost, createScriptHost, stripWrapper } from './host/host.js'
17
+ export { createMemoryViewModel } from './context/context.memory.js'
18
+ export { commandsToContours, contoursToCommands, transformContours } from './api/draw/draw.contours.js'
19
+
20
+ /** @typedef {import('./host/host.js').HostOptions} HostOptions */
21
+ /** @typedef {import('./context/context.viewmodel.js').ViewModelAdapter} ViewModelAdapter */
22
+ /** @typedef {import('./api/draw/draw.renderer.js').Drawing} Drawing */
23
+ /** @typedef {import('./api/draw/draw.contours.js').Contour} Contour */