@px-petals/roku.motion-web 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +201 -0
  2. package/audio/audio.cues.js +27 -0
  3. package/audio/audio.js +259 -0
  4. package/bands/bands.js +392 -0
  5. package/bundle/bundle.fonts.js +55 -0
  6. package/bundle/bundle.js +131 -0
  7. package/focus/focus.direction.js +46 -0
  8. package/focus/focus.dom.js +158 -0
  9. package/focus/focus.js +277 -0
  10. package/focus/focus.tree.js +146 -0
  11. package/hit/hit.geometry.js +113 -0
  12. package/hit/hit.js +129 -0
  13. package/list/list.flex.js +181 -0
  14. package/list/list.items.js +80 -0
  15. package/list/list.js +518 -0
  16. package/lottie/lottie.js +124 -0
  17. package/lottie/lottie.layers.js +416 -0
  18. package/lottie/lottie.paint.js +238 -0
  19. package/lottie/lottie.paths.js +474 -0
  20. package/lottie/lottie.props.js +265 -0
  21. package/lottie/lottie.shapes.js +259 -0
  22. package/mixer/mixer.js +307 -0
  23. package/mixer/mixer.mix.js +107 -0
  24. package/mixer/mixer.stage.js +431 -0
  25. package/motion/motion.convert.js +404 -0
  26. package/motion/motion.flow.js +421 -0
  27. package/motion/motion.keys.js +150 -0
  28. package/motion/motion.machine.js +382 -0
  29. package/motion/motion.nested.js +345 -0
  30. package/motion/motion.values.js +368 -0
  31. package/motion.js +1155 -0
  32. package/package.json +47 -4
  33. package/player/overlay/overlay.js +325 -0
  34. package/player/overlay/overlay.layout.js +208 -0
  35. package/player/player.fit.js +101 -0
  36. package/player/player.js +421 -0
  37. package/player/player.raster.js +127 -0
  38. package/pointer/pointer.js +292 -0
  39. package/readme.md +345 -0
  40. package/script/luau/lib/base/base.js +202 -0
  41. package/script/luau/lib/base/base.table.js +189 -0
  42. package/script/luau/lib/lib.args.js +152 -0
  43. package/script/luau/lib/lib.js +23 -0
  44. package/script/luau/lib/math/math.bit32.js +144 -0
  45. package/script/luau/lib/math/math.js +288 -0
  46. package/script/luau/lib/math/math.vector.js +125 -0
  47. package/script/luau/lib/string/string.format.js +143 -0
  48. package/script/luau/lib/string/string.js +160 -0
  49. package/script/luau/lib/string/string.pattern.js +505 -0
  50. package/script/luau/load/load.js +362 -0
  51. package/script/luau/load/load.ops.js +79 -0
  52. package/script/luau/luau.convert.js +259 -0
  53. package/script/luau/luau.js +132 -0
  54. package/script/luau/luau.md +111 -0
  55. package/script/luau/vm/vm.js +167 -0
  56. package/script/luau/vm/vm.meta.js +401 -0
  57. package/script/luau/vm/vm.num.js +236 -0
  58. package/script/luau/vm/vm.run.js +842 -0
  59. package/script/luau/vm/vm.value.js +406 -0
  60. package/script/play/play.js +199 -0
  61. package/script/play/play.objects.js +295 -0
  62. package/script/play/play.scope.js +91 -0
  63. package/script/rive/api/api.statics.js +31 -0
  64. package/script/rive/api/api.type.js +188 -0
  65. package/script/rive/api/artboard/artboard.js +89 -0
  66. package/script/rive/api/assets/assets.js +72 -0
  67. package/script/rive/api/data/data.value.js +99 -0
  68. package/script/rive/api/draw/draw.contours.js +163 -0
  69. package/script/rive/api/draw/draw.measure.js +196 -0
  70. package/script/rive/api/draw/draw.paint.js +235 -0
  71. package/script/rive/api/draw/draw.path.js +153 -0
  72. package/script/rive/api/draw/draw.renderer.js +117 -0
  73. package/script/rive/api/math/math.color.js +96 -0
  74. package/script/rive/api/math/math.mat2d.js +180 -0
  75. package/script/rive/api/math/math.vector.js +121 -0
  76. package/script/rive/api/media/media.audio.js +113 -0
  77. package/script/rive/api/media/media.draw.js +73 -0
  78. package/script/rive/api/media/media.image.js +61 -0
  79. package/script/rive/api/media/media.mesh.js +50 -0
  80. package/script/rive/api/pointer/pointer.event.js +64 -0
  81. package/script/rive/api/pointer/pointer.route.js +69 -0
  82. package/script/rive/api/text/text.font.js +129 -0
  83. package/script/rive/api/text/text.js +278 -0
  84. package/script/rive/api/text/text.layout.js +259 -0
  85. package/script/rive/context/context.js +76 -0
  86. package/script/rive/context/context.memory.js +196 -0
  87. package/script/rive/context/context.viewmodel.js +302 -0
  88. package/script/rive/context/lists/lists.js +160 -0
  89. package/script/rive/context/lists/lists.values.js +281 -0
  90. package/script/rive/host/host.globals.js +44 -0
  91. package/script/rive/host/host.js +533 -0
  92. package/script/rive/host/host.node.js +45 -0
  93. package/script/rive/rive.js +23 -0
  94. package/script/rive/rive.md +180 -0
  95. package/script/script.js +11 -0
  96. package/skin/skin.deform.js +514 -0
  97. package/skin/skin.ik.js +260 -0
  98. package/skin/skin.js +297 -0
  99. package/README.md +0 -3
package/motion.js ADDED
@@ -0,0 +1,1155 @@
1
+ /**
2
+ * Motion for the web: plays a lottie-roku bundle on a 2D canvas, for
3
+ * browsers that can't run Rive's own runtime (no WebGL2 or WebAssembly).
4
+ * Each animation draws as vector from its Lottie, or from its raster sheets
5
+ * where vector is too slow or asked against; a bundle's state machine,
6
+ * live data, keys and text work as on Roku (runtime/roku).
7
+ *
8
+ * const motion = createMotion(canvas)
9
+ * motion.on('event', (name) => …)
10
+ * await motion.load('https://…/bundle/motion.json', { inputs: { score: 3 } })
11
+ * motion.setInputs({ score: 4 })
12
+ * motion.key('enter')
13
+ *
14
+ * A bundle whose machine has layers, blend states, cross-fades or exit times
15
+ * plays through the pose mixer (mixer/) instead of one animation at a time.
16
+ */
17
+
18
+ import { createAudio } from './audio/audio.js'
19
+ import { loadBundle, loadBytes, loadJson, loadScripts, fileUrl } from './bundle/bundle.js'
20
+ import { loadFonts } from './bundle/bundle.fonts.js'
21
+ import { createAnimation } from './lottie/lottie.js'
22
+ import { createLists } from './list/list.js'
23
+ import { createMixer } from './mixer/mixer.js'
24
+ import { createStage } from './mixer/mixer.stage.js'
25
+ import { createFocus, PHASES } from './focus/focus.js'
26
+ import { createDom } from './focus/focus.dom.js'
27
+ import { createFlow } from './motion/motion.flow.js'
28
+ import { exists, layersOf } from './motion/motion.machine.js'
29
+ import { isClick, keyFromEvent, keyNames, modifiersFromEvent, parseKey } from './motion/motion.keys.js'
30
+ import { createNested } from './motion/motion.nested.js'
31
+ import { flattenInputs, fromPairs, publicValues, slotValues, sourceValue, tintColors, writtenValue } from './motion/motion.values.js'
32
+ import { cellOf, gridOf, hasBlend, linkBands, variantIds } from './bands/bands.js'
33
+ import { alignmentOf, fitName } from './player/player.fit.js'
34
+ import { createPlayer } from './player/player.js'
35
+ import { createPointer } from './pointer/pointer.js'
36
+ import { createRaster } from './player/player.raster.js'
37
+
38
+ /**
39
+ * What a bundle without skins holds: nothing, and no skin code loaded.
40
+ * @type {import('./skin/skin.js').Skins}
41
+ */
42
+ const noSkins = { count: 0, update() {}, table: () => ({}), keyedBy: () => undefined, evaluate: () => ({}) }
43
+
44
+ /** @import { Bundle } from './bundle/bundle.js' */
45
+ /** @import { Media } from './player/player.js' */
46
+
47
+ /**
48
+ * @typedef {object} MotionOptions
49
+ * @property {'auto' | 'vector' | 'raster'} [renderer] `auto` (default) draws vector, moving a slow animation to its sheets
50
+ * @property {boolean} [clicks] a tap or click on the canvas clicks the animation, and on a listener's target clicks it; default true. Other pointer listeners always answer
51
+ * @property {boolean} [muted] play no sound; `setMuted` changes it later. Default false
52
+ * @property {string} [fit] how the animation fits the canvas, over the bundle's: contain, cover, fill, fitWidth, fitHeight, scaleDown, none or layout
53
+ * @property {string | number[]} [alignment] where it sits in the canvas, over the bundle's: a Rive alignment name such as `topLeft`, or `[x, y]` from -1 to 1
54
+ * @property {'auto' | 'manual'} [keys] `auto` (default): the canvas can be given DOM focus (Tab, a click, `canvas.focus()`) and, while it has it, its keys go to the bundle's focus and keyboard listeners. `manual`: the canvas listens to no keys and the host routes them with `key`, `keyUp`, `focus`, `blur` and `click`
55
+ * @property {boolean} [manual] a test hook: no clock of its own, time moves only by `advance`. Default false
56
+ * @property {import('./bundle/bundle.js').ScriptProvider} [scripts] `script/script.js` (the Luau VM and Rive host), or a function that returns it or a promise of it, for a bundle that has scripts to run: `() => import('.../script/script.js')`. A bundle without scripts never asks for it; without it, a bundle's script conditions do not pass and its script converters pass values through
57
+ */
58
+
59
+ /** The values of a Motion with no bundle playing */
60
+ const NO_VALUES = {}
61
+
62
+ /**
63
+ * @typedef {'loaded' | 'state' | 'playback' | 'finished' | 'values' | 'event' | 'audio' | 'focus' | 'error'} MotionEvent
64
+ */
65
+
66
+ /**
67
+ * @param {HTMLCanvasElement} canvas
68
+ * @param {MotionOptions} [options]
69
+ */
70
+ export function createMotion(canvas, options) {
71
+ const settings = { renderer: (options && options.renderer) || 'auto', clicks: !options || options.clicks !== false, muted: !!(options && options.muted), keys: options && options.keys === 'manual' ? 'manual' : 'auto', manual: !!(options && options.manual) }
72
+ /** @type {Record<string, Function[]>} */
73
+ const listeners = {}
74
+
75
+ /** @type {Bundle | null} */
76
+ let bundle = null
77
+ /** @type {ReturnType<typeof createFlow> | null} */
78
+ let flow = null
79
+ /** @type {Record<string, Media>} */
80
+ let media = {}
81
+ /** @type {Record<string, string>} */
82
+ let families = {}
83
+ /** @type {Record<string, string>} */
84
+ let inputOf = {}
85
+ /** Slot id → value for the current values, which the text overlays read */
86
+ let slotTable = {}
87
+ /** The bundle's bound skins, deformed into their slots with the rest; the skin code is loaded only for a bundle that has some */
88
+ let skins = noSkins
89
+ /** @type {typeof import('./skin/skin.js').createSkins | null} */
90
+ let makeSkins = null
91
+ /** Whether a write-back is in progress, whose own redraw must not write again */
92
+ let writing = false
93
+ let loadCount = 0
94
+ /** @type {{ fit?: string, alignment?: any }} what the caller asked for, over the bundle's */
95
+ let asked = { fit: options && options.fit, alignment: options && options.alignment }
96
+ /** @type {any} the layout band playing */
97
+ let band = null
98
+ /** The weights toward the next, wider layout band (`t`) and the next, taller one (`v`), which every band that blends into them draws in; 0 outside a blending segment */
99
+ const live = { t: 0, v: 0 }
100
+ /** The artboard width a blending segment draws at, which is the canvas's; 0 when the band plays at its own */
101
+ let blended = 0
102
+ /** The artboard height likewise: the canvas's where the layout is exact for it, else 0 */
103
+ let blendedHeight = 0
104
+ /** @type {import('./bands/bands.js').Grid | null} the layout bands as a grid, when the artboard is responsive */
105
+ let grid = null
106
+ /** @type {import('./bands/bands.js').Cell | null} where the canvas falls in the grid, while the fit is layout */
107
+ let cell = null
108
+ let nested = createNested([])
109
+ /** The lists of the bundle: a row drawn for each item of a view-model list */
110
+ let lists = createLists([], {}, { base: '', band: bandWidth, error: (message) => emit('error', message), focus: focusAction })
111
+ /** The `version` of the lists' rows the focus tree was built for */
112
+ let listVersion = 0
113
+ /** @type {Media | null} the mixed media of a bundle that plays through the mixer */
114
+ let mixedMedia = null
115
+ let isMixedShown = false
116
+ /** @type {import('./script/play/play.js').Scripts | null} the bundle's scripts, once loaded */
117
+ let scripts = null
118
+ /** @type {import('./script/play/play.objects.js').Objects | null} the bundle's scripted nodes, drawables and path effects, which run every frame */
119
+ let objects = null
120
+ /** Whether the scripted objects are running a frame, whose own writes redraw once, with the frame */
121
+ let isStepping = false
122
+ /** What the objects wrote since they last handed it over, and the triggers they fired */
123
+ const wrote = /** @type {Record<string, any>} */ ({})
124
+ const fired = /** @type {Record<string, boolean>} */ ({})
125
+ let hasWrites = false
126
+ let hasFires = false
127
+ /** @type {any[] | null} every vector animation drawing, once the bundle is loaded */
128
+ let vectorList = null
129
+ let isPointerWatched = false
130
+ let hasFocusNodes = false
131
+ /** @type {Record<string, Media>} a layout band's mixed media, by the band's width and height (`320x180`); they share the stage with `mixedMedia` */
132
+ let mixedBands = {}
133
+
134
+ const player = createPlayer(canvas, {
135
+ adaptive: settings.renderer === 'auto',
136
+ manual: settings.manual,
137
+ onPlayback: (playback) => emit('playback', playback),
138
+ onFinished: (layer) => {
139
+ if (layer === 0) emit('finished')
140
+ if (flow) flow.finished(layer)
141
+ },
142
+ onTick: () => {
143
+ if (flow) flow.tick()
144
+ },
145
+ isPinned: () => isPointerWatched || hasFocusNodes || lists.count > 0,
146
+ overlay: () => ({ values: slotTable, families, inputOf }),
147
+ onFrame: (item, frame) => writeBack(item, frame),
148
+ placement,
149
+ // an animation that holds its own artboard size (a state keying it) is not sized to the canvas, as in Rive
150
+ artboardWidth: (shown) => (holdsSize(shown) ? 0 : blended),
151
+ artboardHeight: (shown) => (holdsSize(shown) ? 0 : blendedHeight),
152
+ tick: (seconds) => {
153
+ if (objects) objects.step(seconds)
154
+ nested.tick(seconds)
155
+ lists.tick(seconds)
156
+ },
157
+ prepare: (shown) => {
158
+ if (nested.count && shown.vector) nested.apply(shown.vector.slots, shown.fps)
159
+ },
160
+ keepAlive: () => nested.count > 0 || lists.isAnimated || (!!objects && (objects.clocked || objects.live)),
161
+ onCue: (cue) => {
162
+ audio.play(cue.sound)
163
+ // a keyed event reaches the host as a fired one does
164
+ emit('event', cue.event)
165
+ },
166
+ })
167
+ const audio = createAudio({
168
+ muted: settings.muted,
169
+ onError: (message) => emit('error', message),
170
+ onPlay: (sound) => emit('audio', sound),
171
+ })
172
+
173
+ // Rive's focus: which target of the graphic the keys belong to. It is not DOM focus, which Motion never takes
174
+ // A node of a nested artboard is `<sid>/<rid>`, a list row's `row.<id>/<rid>`: the machine that owns it runs its listeners and actions
175
+ const focusing = createFocus({
176
+ fire: (type, rid) => {
177
+ const at = rid.lastIndexOf('/')
178
+ if (at < 0) {
179
+ if (flow) flow.focusEvent(type, rid)
180
+ } else if (rid.indexOf('row.') === 0) {
181
+ lists.focusEvent(type, rid.slice(0, at), rid.slice(at + 1))
182
+ } else {
183
+ nested.focusEvent(type, rid.slice(0, at), rid.slice(at + 1))
184
+ }
185
+ },
186
+ run: (actions, scope) => {
187
+ if (!scope) {
188
+ if (flow) flow.act(actions)
189
+ } else if (scope.indexOf('row.') === 0) {
190
+ lists.act(scope, actions)
191
+ } else {
192
+ nested.act(scope, actions)
193
+ }
194
+ },
195
+ centerOf: (rid) => {
196
+ if (rid.indexOf('row.') === 0) return lists.center(rid)
197
+ const hits = player.hits
198
+ return hits ? hits.center(rid) : null
199
+ },
200
+ changed: (rid) => emit('focus', rid),
201
+ })
202
+ const dom = createDom(canvas, { focus: focusing })
203
+
204
+ /**
205
+ * @param {MotionEvent} name
206
+ * @param {any} [detail]
207
+ */
208
+ function emit(name, detail) {
209
+ const list = (listeners[name] || []).slice()
210
+ for (let n = 0; n < list.length; n++) list[n](detail)
211
+ }
212
+
213
+ // a click on the canvas is the listeners with no target of their own; the ones with a target hear the pointer (pointer/)
214
+ const onClick = () => {
215
+ if (flow) flow.click(true)
216
+ }
217
+ if (settings.clicks) canvas.addEventListener('click', onClick)
218
+ // the shapes the host's listeners and those of its list rows watch: a row's are named `<row>|<target>`
219
+ const hitsOf = {
220
+ test: (/** @type {string} */ name, /** @type {number} */ x, /** @type {number} */ y) => {
221
+ if (name.indexOf('|') > 0) return lists.hit(name, x, y)
222
+ const hits = player.hits
223
+ return !!hits && hits.test(name, x, y)
224
+ },
225
+ }
226
+ const pointer = createPointer(canvas, {
227
+ hits: () => /** @type {any} */ (hitsOf),
228
+ fire: (type, target) => {
229
+ if (type !== 'Click' || settings.clicks) {
230
+ if (!nested.pointer(type, target) && !lists.pointer(type, target) && flow) flow.pointer(type, target)
231
+ }
232
+ },
233
+ clicks: settings.clicks,
234
+ })
235
+
236
+ /**
237
+ * Loads a bundle and starts it.
238
+ *
239
+ * @param {string} url motion.json
240
+ * @param {object} [start]
241
+ * @param {Record<string, any>} [start.inputs] values to start with, over the view model's defaults
242
+ * @param {string} [start.state] a state to start in instead of the machine's own
243
+ * @param {Record<string, string>} [start.transitions] without a machine, the animation each one's end plays next
244
+ * @param {string} [start.fit] how the animation fits the canvas, over the bundle's
245
+ * @param {string | number[]} [start.alignment] where it sits in the canvas, over the bundle's
246
+ * @returns {Promise<void>}
247
+ */
248
+ function load(url, start) {
249
+ const attempt = ++loadCount
250
+ if (start && (start.fit !== undefined || start.alignment !== undefined)) asked = { fit: start.fit !== undefined ? start.fit : asked.fit, alignment: start.alignment !== undefined ? start.alignment : asked.alignment }
251
+ return loadBundle(url)
252
+ .then((loaded) => Promise.all([ loaded, loadFonts(loaded), prepareAll(loaded), loadRows(loaded), loadScripts(loaded.manifest, options && options.scripts, (file) => loadBytes(fileUrl(loaded, file)), (message) => emit('error', message)), loadSkins(loaded) ]))
253
+ .then(([ loaded, loadedFamilies, prepared, rows, loadedScripts, skinModule ]) => {
254
+ // a later load won
255
+ if (attempt !== loadCount) return
256
+ bundle = loaded
257
+ scripts = loadedScripts
258
+ if (objects) objects.destroy()
259
+ makeSkins = skinModule ? skinModule.createSkins : null
260
+ skins = makeSkins && loaded.manifest.data && loaded.manifest.data.skins ? makeSkins(loaded.manifest.data.skins) : noSkins
261
+ families = loadedFamilies
262
+ media = prepared.media
263
+ mixedMedia = prepared.mixed
264
+ mixedBands = prepared.bands
265
+ vectorList = null
266
+ isMixedShown = false
267
+ inputOf = inputsOfSlots(loaded.manifest)
268
+ nested = createNested(loaded.manifest.nested, { write: nestedWrote, focus: focusAction })
269
+ lists = createLists(loaded.manifest.lists, rows, { skins: makeSkins, base: loaded.base, families: () => families, band: bandWidth, blended: () => blended, cell: () => cell, error: (message) => emit('error', message), focus: focusAction })
270
+ listVersion = lists.version
271
+ grid = gridOf(loaded.manifest)
272
+ band = chooseBand()
273
+ audio.load(loaded.manifest.audio, (uri) => fileUrl(loaded, uri))
274
+ objects = startObjects(loaded)
275
+ flow = createFlow(loaded.manifest, hooks())
276
+ const tree = focusTree()
277
+ focusing.setup(tree.nodes, tree.keys)
278
+ hasFocusNodes = focusing.hasNodes
279
+ dom.listen(settings.keys === 'auto' && (focusing.hasNodes || tree.keys.length > 0))
280
+ watchPointer(flow.pointerTargets())
281
+ flow.start(flattenInputs((start && start.inputs) || {}), start && start.state, start && start.transitions)
282
+ // what the nested machines focused as they started, now there is a tree
283
+ nested.flushFocus()
284
+ if (start && start.inputs) nested.takeInputs(start.inputs)
285
+ emit('loaded')
286
+ })
287
+ .catch((error) => {
288
+ emit('error', error.message)
289
+ throw error
290
+ })
291
+ }
292
+
293
+ /**
294
+ * Every animation, ready to draw: its Lottie for vector, its sheets for
295
+ * raster, as the renderer setting needs them.
296
+ *
297
+ * @param {Bundle} loaded
298
+ * @returns {Promise<{ media: Record<string, Media>, mixed: Media | null, bands: Record<string, Media> }>}
299
+ */
300
+ function prepareAll(loaded) {
301
+ const manifest = loaded.manifest
302
+ const isMixed = settings.renderer !== 'raster' && usesMixer(manifest)
303
+ // bands that blend are linked before anything draws, so their Lotties are read first and compiled after
304
+ const isBlended = settings.renderer !== 'raster' && hasBlend(manifest)
305
+ /** @type {Record<string, Media>} */
306
+ const prepared = {}
307
+ /** @type {Record<string, any>} */
308
+ const lotties = {}
309
+ const pending = []
310
+ for (const id in manifest.animations) {
311
+ const entry = manifest.animations[id]
312
+ const wantsVector = settings.renderer !== 'raster'
313
+ const wantsRaster = settings.renderer !== 'vector' && !!entry.raster
314
+ /** @type {Media} */
315
+ const item = {
316
+ vector: null,
317
+ raster: wantsRaster ? createRaster(entry, loaded) : null,
318
+ width: entry.width,
319
+ height: entry.height,
320
+ fps: entry.fps,
321
+ frames: entry.frames,
322
+ texts: entry.texts || [],
323
+ cues: (manifest.audio && manifest.audio.cues && manifest.audio.cues[id]) || [],
324
+ tints: () => (entry.raster && flow ? tintColors(entry.raster.tints, manifest.data, flow.values) : []),
325
+ }
326
+ prepared[id] = item
327
+ if (item.raster) pending.push(item.raster.ready)
328
+ if (isMixed || isBlended) {
329
+ // the mixer draws every animation from one of their structures; each is read, not compiled
330
+ pending.push(loadJson(fileUrl(loaded, entry.lottie)).then((lottie) => {
331
+ lotties[id] = lottie
332
+ }))
333
+ } else if (wantsVector) {
334
+ pending.push(loadJson(fileUrl(loaded, entry.lottie)).then((lottie) => {
335
+ item.vector = drawsRows(createAnimation(lottie, { base: loaded.base, families: () => families }))
336
+ return item.vector.ready
337
+ }))
338
+ }
339
+ }
340
+ return Promise.all(pending).then(() => {
341
+ if (isBlended) linkBands(manifest, lotties, live)
342
+ if (!isMixed && isBlended) {
343
+ const ready = []
344
+ for (const id in lotties) {
345
+ prepared[id].vector = drawsRows(createAnimation(lotties[id], { base: loaded.base, families: () => families }))
346
+ ready.push(/** @type {Promise<any>} */ (prepared[id].vector.ready))
347
+ }
348
+ return Promise.all(ready).then(() => ({ media: prepared, mixed: null, bands: {} }))
349
+ }
350
+ if (!isMixed) return { media: prepared, mixed: null, bands: {} }
351
+
352
+ // every layout band mixes its own variants of the animations, on the one stage, so a band moves in step with the layers
353
+ const stage = createStage(manifest.animations)
354
+ /** @type {Record<string, Media>} */
355
+ const bands = {}
356
+ const listed = gridOf(manifest)
357
+ const rows = listed ? listed.rows : []
358
+ for (let j = 0; j < rows.length; j++) {
359
+ for (let i = 0; i < rows[j].length; i++) {
360
+ const corner = rows[j][i]
361
+ if (corner.animations) bands[bandKey(corner)] = mixedOf(manifest, prepared, lotties, loaded.base, stage, corner.animations)
362
+ }
363
+ }
364
+ return { media: prepared, mixed: mixedOf(manifest, prepared, lotties, loaded.base, stage, null), bands }
365
+ })
366
+ }
367
+
368
+ /**
369
+ * The media a mixed bundle shows: one picture of the carrier's size, drawn by the mixer from what the stage says is playing.
370
+ *
371
+ * @param {any} manifest
372
+ * @param {Record<string, Media>} items
373
+ * @param {Record<string, any>} lotties
374
+ * @param {string} base
375
+ * @param {ReturnType<typeof createStage>} stage
376
+ * @param {Record<string, string> | null} variants a layout band's variant of each animation; null for the file's own size
377
+ * @returns {Media}
378
+ */
379
+ function mixedOf(manifest, items, lotties, base, stage, variants) {
380
+ /** @param {string} id the animation a layer names, or its variant in the band */
381
+ const picked = (id) => (variants && variants[id] ? variants[id] : id)
382
+ /** @type {Record<string, any>} */
383
+ const own = {}
384
+ const isVariant = variantIds(manifest)
385
+ for (const id in lotties) if (!isVariant[id]) own[id] = lotties[picked(id)]
386
+ const mixer = createMixer(own, { base, families: () => families })
387
+ const carrier = items[picked(mixer.carrier)]
388
+ return {
389
+ vector: drawsRows(mixer.animation),
390
+ raster: null,
391
+ width: carrier.width,
392
+ height: carrier.height,
393
+ fps: carrier.fps,
394
+ frames: Infinity,
395
+ cues: [],
396
+ texts: [],
397
+ tints: () => [],
398
+ mixed: {
399
+ mixer,
400
+ stage,
401
+ values: () => (flow ? flow.values : {}),
402
+ texts: (id) => (items[picked(id)] ? items[picked(id)].texts : []),
403
+ fps: (id) => (items[picked(id)] ? items[picked(id)].fps : 30),
404
+ cues: (id) => (items[picked(id)] ? items[picked(id)].cues : []),
405
+ item: (id) => items[picked(id)] || null,
406
+ },
407
+ }
408
+ }
409
+
410
+ /**
411
+ * The skin code, when the bundle has skins to deform, its lists' rows' too: a module of its own, fetched only then.
412
+ *
413
+ * @param {Bundle} loaded
414
+ * @returns {Promise<typeof import('./skin/skin.js') | null>}
415
+ */
416
+ function loadSkins(loaded) {
417
+ const data = loaded.manifest.data
418
+ let has = !!(data && data.skins && data.skins.length)
419
+ const listed = loaded.manifest.lists || []
420
+ for (let n = 0; n < listed.length && !has; n++) {
421
+ for (const name in listed[n].rows) {
422
+ const rowData = listed[n].rows[name].data
423
+ if (rowData && rowData.skins && rowData.skins.length) has = true
424
+ }
425
+ }
426
+ return has ? import('./skin/skin.js') : Promise.resolve(null)
427
+ }
428
+
429
+ /**
430
+ * The Lottie of every row artboard the bundle's lists draw.
431
+ *
432
+ * @param {Bundle} loaded
433
+ * @returns {Promise<Record<string, any>>} by file name
434
+ */
435
+ function loadRows(loaded) {
436
+ /** @type {Record<string, any>} */
437
+ const found = {}
438
+ const pending = []
439
+ const listed = loaded.manifest.lists || []
440
+ for (let n = 0; n < listed.length; n++) {
441
+ for (const name in listed[n].rows) {
442
+ const file = listed[n].rows[name].lottie
443
+ pending.push(loadJson(fileUrl(loaded, file)).then((lottie) => {
444
+ found[file] = lottie
445
+ }))
446
+ }
447
+ }
448
+ return Promise.all(pending).then(() => found)
449
+ }
450
+
451
+ /**
452
+ * Has an animation draw the lists' rows after its own layers.
453
+ *
454
+ * @param {import('./lottie/lottie.js').Animation} animation
455
+ * @returns {import('./lottie/lottie.js').Animation}
456
+ */
457
+ function drawsRows(animation) {
458
+ const draw = animation.draw
459
+ animation.draw = (ctx, frame, matrix) => {
460
+ draw(ctx, frame, matrix)
461
+ if (lists.count) lists.draw(ctx, matrix)
462
+ }
463
+ return animation
464
+ }
465
+
466
+ /** The width of the layout band showing, 0 for the file's own size. */
467
+ function bandWidth() {
468
+ return band ? band.width : 0
469
+ }
470
+
471
+ /** What the flow calls back with. */
472
+ function hooks() {
473
+ /** @type {import('./motion/motion.flow.js').Hooks} */
474
+ const result = {
475
+ play(id, loops) {
476
+ player.show(media[variantOf(id)], loops, settings.renderer === 'raster')
477
+ },
478
+ get scripts() {
479
+ return scripts
480
+ },
481
+ valuesChanged(values) {
482
+ if (scripts) scripts.changed(values)
483
+ applySlots(values)
484
+ lists.update(values)
485
+ if (lists.version !== listVersion) refocus()
486
+ if (lists.isPointerStale && flow) watchPointer(flow.pointerTargets())
487
+ if (!isStepping) {
488
+ player.redraw()
489
+ if (objects && objects.live) player.nudge()
490
+ }
491
+ emit('values', publicValues(bundle ? bundle.manifest.data : null, values))
492
+ },
493
+ stateChanged: (state) => emit('state', state),
494
+ event: (name) => {
495
+ audio.fire(name)
496
+ emit('event', name)
497
+ },
498
+ error: (message) => emit('error', message),
499
+ focus: (action) => focusAction(action, ''),
500
+ }
501
+
502
+ const stage = mixedMedia && mixedMedia.mixed ? mixedMedia.mixed.stage : null
503
+ if (!stage || !mixedMedia) return result
504
+
505
+ // a bundle that plays through the mixer: the flow tells the stage what every layer enters
506
+ result.layer = (layer, state, fadeMs, ease) => {
507
+ const missing = unknownAnimation(state, media)
508
+ if (missing) {
509
+ emit('error', "unknown animation '" + missing + "'")
510
+ return
511
+ }
512
+ stage.enter(layer, state, fadeMs, ease)
513
+ if (isMixedShown) {
514
+ player.wake()
515
+ return
516
+ }
517
+ isMixedShown = true
518
+ player.show(mixedFor(), false, false)
519
+ }
520
+ result.elapsed = (layer) => stage.elapsedMs(layer)
521
+ return result
522
+ }
523
+
524
+ /**
525
+ * Binds that write an animated property back into the view model: the
526
+ * property as drawn at this frame, in the view model's units, becomes its
527
+ * value when it has moved.
528
+ *
529
+ * @param {Media} item
530
+ * @param {number} frame
531
+ */
532
+ function writeBack(item, frame) {
533
+ const writes = bundle && bundle.manifest.data && bundle.manifest.data.writes
534
+ if (!writes || !writes.length || !flow || writing || !item.vector) return
535
+
536
+ const current = flow.values
537
+ /** @type {Record<string, any>} */
538
+ const changed = {}
539
+ let any = false
540
+ for (let n = 0; n < writes.length; n++) {
541
+ const write = writes[n]
542
+ const drawn = item.vector.probe(write.sid, frame)
543
+ if (drawn === undefined) continue
544
+ const next = writtenValue(write, drawn, current[write.path], scripts)
545
+ if (next === undefined) continue
546
+ const previous = current[write.path]
547
+ const moved = typeof next === 'number' && typeof previous === 'number' ? Math.abs(next - previous) > 0.0001 * Math.max(1, Math.abs(previous)) : next !== previous
548
+ if (moved) {
549
+ changed[write.path] = next
550
+ any = true
551
+ }
552
+ }
553
+ if (!any) return
554
+
555
+ writing = true
556
+ try {
557
+ flow.takeInputs(changed)
558
+ } finally {
559
+ writing = false
560
+ }
561
+
562
+ }
563
+
564
+ /**
565
+ * A nested artboard's machine set properties of the host's view model: the
566
+ * host takes them as it takes a bind's write-back, and the other nested
567
+ * artboards bound to them follow.
568
+ *
569
+ * @param {Record<string, any>} changes
570
+ */
571
+ function nestedWrote(changes) {
572
+ if (!flow || writing) return
573
+ writing = true
574
+ try {
575
+ flow.takeInputs(changes)
576
+ nested.takeInputs(changes)
577
+ } finally {
578
+ writing = false
579
+ }
580
+ }
581
+
582
+ /**
583
+ * A focus action a machine ran. Its FocusTarget names a node of the
584
+ * machine's own artboard, which `scope` (the machine's id, "" for the
585
+ * bundle's own) puts in the host's tree; a move or a clear is the tree's.
586
+ *
587
+ * @param {any} action
588
+ * @param {string} scope
589
+ */
590
+ function focusAction(action, scope) {
591
+ if (action.type === 'FocusTarget') focusing.focus(scope ? scope + '/' + action.rid : action.rid)
592
+ else if (action.type === 'FocusMove') focusing.move(action.direction)
593
+ else focusing.clear()
594
+ }
595
+
596
+ /**
597
+ * The focus tree of the bundle as it is now: the host's nodes, a nested
598
+ * artboard's among them, and the rows' of each list where its place is,
599
+ * with every key binding that names one.
600
+ *
601
+ * @returns {{ nodes: any[], keys: any[] }}
602
+ */
603
+ function focusTree() {
604
+ const manifest = bundle ? bundle.manifest : {}
605
+ const host = manifest.machine && manifest.machine.focus ? manifest.machine.focus.nodes : []
606
+ /** @type {any[]} */
607
+ const nodes = []
608
+ for (let n = 0; n < host.length; n++) {
609
+ nodes.push(host[n])
610
+ if (host[n].list) {
611
+ const rows = lists.nodes(host[n].list, host[n].rid)
612
+ for (let k = 0; k < rows.length; k++) nodes.push(rows[k])
613
+ }
614
+ }
615
+ return { nodes, keys: (manifest.remoteKeys || []).concat(nested.bindings(), lists.bindings()) }
616
+ }
617
+
618
+ /** Rows came or went: the tree takes their nodes, and what their machines focused as they started. */
619
+ function refocus() {
620
+ listVersion = lists.version
621
+ const tree = focusTree()
622
+ focusing.retree(tree.nodes, tree.keys)
623
+ hasFocusNodes = focusing.hasNodes
624
+ lists.flushFocus()
625
+ }
626
+
627
+ /**
628
+ * A press and release on a target, as the pointer makes them.
629
+ *
630
+ * @param {string} target a `rid`, `<sid>/<rid>` in a nested artboard, `row.<id>/<rid>` in a list's row
631
+ */
632
+ function tap(target) {
633
+ const types = [ 'PointerDown', 'PointerUp', 'Click' ]
634
+ // a row's targets are named `<row>|<rid>` by its list
635
+ const rowAt = target.indexOf('row.') === 0 ? target.indexOf('/') : -1
636
+ const row = rowAt < 0 ? target : target.slice(4, rowAt) + '|' + target.slice(rowAt + 1)
637
+ for (let n = 0; n < types.length; n++) {
638
+ if (nested.pointer(types[n], target) || lists.pointer(types[n], row)) continue
639
+ if (flow) flow.pointer(types[n], target)
640
+ }
641
+ }
642
+
643
+ /**
644
+ * @param {string | KeyboardEvent} key
645
+ * @param {boolean} isUp
646
+ * @returns {boolean} whether the bundle used it
647
+ */
648
+ function sendKey(key, isUp) {
649
+ if (!flow) return false
650
+ const name = typeof key === 'string' ? '' : keyFromEvent(key)
651
+ const parsed = typeof key === 'string' ? parseKey(key) : { names: keyNames(name), mods: modifiersFromEvent(key), key: name }
652
+ if (!parsed.key) return false
653
+ const repeat = typeof key !== 'string' && key.repeat
654
+ const used = focusing.press({ names: parsed.names, mods: parsed.mods, phase: isUp ? PHASES.up : repeat ? PHASES.repeat : PHASES.down })
655
+ if (!isUp && isClick(parsed.key)) {
656
+ if (!focusing.hasNodes) flow.click()
657
+ else if (focusing.current !== null) tap(focusing.current)
658
+ }
659
+ return used
660
+ }
661
+
662
+ /**
663
+ * The shapes and groups pointer listeners target: every vector animation
664
+ * records their geometry as it draws, and the pointer answers on the canvas.
665
+ *
666
+ * @param {Record<string, string[]>} own the machine's `pointerTargets`
667
+ */
668
+ function watchPointer(own) {
669
+ // the nested machines' shapes are named `<sid>/<rid>`, beside the bundle's own
670
+ /** @type {Record<string, string[]>} */
671
+ const targets = {}
672
+ for (const rid in own) targets[rid] = own[rid]
673
+ const inside = nested.pointerTargets()
674
+ for (const rid in inside) targets[rid] = inside[rid]
675
+ const rows = lists.pointerTargets()
676
+ for (const name in rows) targets[name] = rows[name]
677
+ const rids = []
678
+ for (const rid in targets) if (rid !== '' && rid.indexOf('|') < 0) rids.push(rid)
679
+ isPointerWatched = rids.length > 0
680
+ // the focus tree's nodes are watched too: the arrows move focus by where they were drawn
681
+ const nodes = focusing.nodes
682
+ // (a list row's are watched as the row draws)
683
+ for (let n = 0; n < nodes.length; n++) if (nodes[n].rid !== '' && nodes[n].rid.indexOf('row.') !== 0 && rids.indexOf(nodes[n].rid) < 0) rids.push(nodes[n].rid)
684
+
685
+ const drawn = []
686
+ for (const id in media) drawn.push(media[id].vector)
687
+ if (mixedMedia) drawn.push(mixedMedia.vector)
688
+ for (const width in mixedBands) drawn.push(mixedBands[width].vector)
689
+ for (let n = 0; n < drawn.length; n++) {
690
+ const vector = drawn[n]
691
+ if (vector) vector.hits.watch(rids)
692
+ }
693
+ pointer.listen(targets)
694
+ }
695
+
696
+ /**
697
+ * Live values into every vector animation's slots.
698
+ *
699
+ * @param {Record<string, any>} values
700
+ */
701
+ function applySlots(values) {
702
+ if (!bundle) return
703
+ const table = slotValues(bundle.manifest.data, values, (message) => emit('error', message), scripts)
704
+ slotTable = table
705
+ const data = bundle.manifest.data
706
+ const warn = (/** @type {string} */ message) => emit('error', message)
707
+ if (skins.count) skins.update((source) => sourceValue(source, values, data, warn, scripts))
708
+ for (const id in media) writeSlots(media[id].vector, table, id)
709
+ if (mixedMedia) writeSlots(mixedMedia.vector, table, '', mixedMedia)
710
+ for (const width in mixedBands) writeSlots(mixedBands[width].vector, table, '', mixedBands[width])
711
+ }
712
+
713
+ /**
714
+ * One vector animation's slots: the slot table, and the skins' paths for the animation.
715
+ *
716
+ * @param {any} vector
717
+ * @param {Record<string, any>} table
718
+ * @param {string} id the animation's id, which says what it keys in a skin
719
+ * @param {Media} [mixed] mixed media, whose skins take what each pose's animation keys in them
720
+ */
721
+ function writeSlots(vector, table, id, mixed) {
722
+ if (!vector) return
723
+ for (const sid in vector.slots) delete vector.slots[sid]
724
+ for (const sid in table) vector.slots[sid] = table[sid]
725
+ if (skins.count) {
726
+ const deformed = skins.table(id)
727
+ const mixer = mixed && mixed.mixed ? mixed.mixed.mixer : null
728
+ for (const sid in deformed) vector.slots[sid] = mixer ? mixer.mixed((animation) => skins.keyedBy(sid, animation), deformed[sid]) : deformed[sid]
729
+ }
730
+ if (objects) putOutputs(vector.slots)
731
+ }
732
+
733
+ /** Every vector animation the bundle draws, listed once. */
734
+ function vectorsOf() {
735
+ if (vectorList) return vectorList
736
+ const list = []
737
+ for (const id in media) if (media[id].vector) list.push(media[id].vector)
738
+ if (mixedMedia && mixedMedia.vector) list.push(mixedMedia.vector)
739
+ for (const width in mixedBands) if (mixedBands[width].vector) list.push(mixedBands[width].vector)
740
+ vectorList = list
741
+ return list
742
+ }
743
+
744
+ /**
745
+ * What the scripted objects drew, into a slot table.
746
+ *
747
+ * @param {Record<string, any>} slots
748
+ */
749
+ function putOutputs(slots) {
750
+ const made = /** @type {NonNullable<typeof objects>} */ (objects)
751
+ for (let n = 0; n < made.sids.length; n++) {
752
+ const value = made.outputs[made.sids[n]]
753
+ if (value !== undefined) slots[made.sids[n]] = value
754
+ }
755
+ }
756
+
757
+ /**
758
+ * Starts the bundle's scripted nodes, drawables and path effects, when the
759
+ * script engine came with it. What they write lands in the flow's values at
760
+ * once, for the script after them, and reaches the machine, the nested
761
+ * artboards and the slots together, once a frame.
762
+ *
763
+ * @param {Bundle} loaded
764
+ * @returns {import('./script/play/play.objects.js').Objects | null}
765
+ */
766
+ function startObjects(loaded) {
767
+ const list = loaded.manifest.scriptObjects
768
+ if (!scripts || !list || !list.length) return null
769
+ return scripts.objects(list, {
770
+ data: loaded.manifest.data,
771
+ values: () => (flow ? flow.values : NO_VALUES),
772
+ write: (path, value) => {
773
+ if (!flow) return
774
+ flow.values[path] = value
775
+ wrote[path] = value
776
+ hasWrites = true
777
+ },
778
+ fire: (path) => {
779
+ fired[path] = true
780
+ hasFires = true
781
+ },
782
+ commit: () => {
783
+ if (!flow || (!hasWrites && !hasFires)) return false
784
+ const isWritten = hasWrites
785
+ const isFired = hasFires
786
+ hasWrites = false
787
+ hasFires = false
788
+ isStepping = true
789
+ try {
790
+ if (isWritten) {
791
+ nested.takeInputs(wrote)
792
+ flow.changed()
793
+ }
794
+ if (isFired) {
795
+ nested.takeInputs(fired)
796
+ flow.takeInputs(fired)
797
+ }
798
+ } finally {
799
+ isStepping = false
800
+ for (const name in wrote) delete wrote[name]
801
+ for (const name in fired) delete fired[name]
802
+ }
803
+ return true
804
+ },
805
+ publish: () => {
806
+ const drawn = vectorsOf()
807
+ for (let n = 0; n < drawn.length; n++) putOutputs(drawn[n].slots)
808
+ },
809
+ probe: (sid) => {
810
+ const item = flow ? media[variantOf(flow.animation)] : null
811
+ return item && item.vector ? item.vector.probe(sid, player.frame) : undefined
812
+ },
813
+ families,
814
+ })
815
+ }
816
+
817
+ /**
818
+ * Whether media drawn in a layout band is not the band's size: the
819
+ * converter laid its animation out at the artboard size the animation holds.
820
+ *
821
+ * @param {Media} shown
822
+ */
823
+ function holdsSize(shown) {
824
+ return !!(band && shown && shown.width && shown.width !== band.width)
825
+ }
826
+
827
+ /** How the animation fits the canvas: what the caller asked for, else the bundle's, else contain and centred. */
828
+ function placement() {
829
+ const manifest = bundle ? bundle.manifest : {}
830
+ return {
831
+ fit: fitName(asked.fit !== undefined ? asked.fit : manifest.fit),
832
+ alignment: alignmentOf(asked.alignment !== undefined ? asked.alignment : manifest.alignment),
833
+ }
834
+ }
835
+
836
+ /**
837
+ * The layout band for the canvas's size, when the artboard is responsive
838
+ * and fits as `layout`; otherwise the file's own size. Sets what a cell
839
+ * that blends draws: the weights toward the neighbouring bands, and the
840
+ * artboard size (the canvas's, as Rive's `layout` fit resizes the artboard
841
+ * to it).
842
+ */
843
+ function chooseBand() {
844
+ live.t = 0
845
+ live.v = 0
846
+ blended = 0
847
+ blendedHeight = 0
848
+ cell = null
849
+ if (!bundle || !grid || placement().fit !== 'layout') return null
850
+ const ratio = window.devicePixelRatio || 1
851
+ const width = canvas.clientWidth > 0 ? canvas.clientWidth : canvas.width / ratio
852
+ const height = canvas.clientHeight > 0 ? canvas.clientHeight : canvas.height / ratio
853
+ cell = cellOf(grid, width, height)
854
+ live.t = cell.u
855
+ live.v = cell.v
856
+ blended = cell.width
857
+ blendedHeight = cell.height
858
+ return cell.corner
859
+ }
860
+
861
+ /**
862
+ * The animation that plays an animation's part in the layout band showing.
863
+ *
864
+ * @param {string} id
865
+ */
866
+ function variantOf(id) {
867
+ const variant = band && band.animations ? band.animations[id] : null
868
+ return variant && media[variant] ? variant : id
869
+ }
870
+
871
+ /** The media a mixed bundle shows: the layout band's, else the file's own size. */
872
+ function mixedFor() {
873
+ const own = /** @type {Media} */ (mixedMedia)
874
+ return band && band.animations && mixedBands[bandKey(band)] ? mixedBands[bandKey(band)] : own
875
+ }
876
+
877
+ /** Moves to the layout band the canvas now calls for, keeping the animation's time. */
878
+ function updateBand() {
879
+ const before = [ live.t, live.v, blended, blendedHeight ]
880
+ const next = chooseBand()
881
+ if (next === band) {
882
+ // inside a blending cell the picture moves with the size, and so does an artboard sized to the canvas
883
+ const isMoved = live.t !== before[0] || live.v !== before[1] || blended !== before[2] || blendedHeight !== before[3]
884
+ if (isMoved || live.t > 0 || live.v > 0) player.render()
885
+ return
886
+ }
887
+ band = next
888
+ if (isMixedShown) {
889
+ // the stage is shared, so layers, fades and blends carry on in the other band
890
+ player.replace(mixedFor())
891
+ return
892
+ }
893
+ if (flow && flow.animation && media[variantOf(flow.animation)]) player.replace(media[variantOf(flow.animation)])
894
+ }
895
+
896
+ return {
897
+ load,
898
+
899
+ /** @param {Record<string, any>} inputs view-model values; an Event input set to true fires */
900
+ setInputs(inputs) {
901
+ nested.takeInputs(inputs)
902
+ if (flow) flow.takeInputs(flattenInputs(inputs))
903
+ },
904
+
905
+ /**
906
+ * Values as `{ key, value }` strings, typed by the bundle's view model,
907
+ * as maestro-web-sdk's bindable properties arrive.
908
+ *
909
+ * @param {{ key: string, value: string }[]} pairs
910
+ */
911
+ setProperties(pairs) {
912
+ if (!flow || !bundle) return
913
+ const inputs = fromPairs(bundle.manifest.data, pairs)
914
+ nested.takeInputs(inputs)
915
+ flow.takeInputs(inputs)
916
+ },
917
+
918
+ /** @param {string} name a machine state */
919
+ setState(name) {
920
+ if (flow) flow.jumpTo(name)
921
+ },
922
+
923
+ /**
924
+ * A key press for the bundle: a Rive key name (`enter`, `up`, `tab`), a
925
+ * Roku remote name (`OK`, `back`), either with modifiers in front
926
+ * (`shift+tab`), or a DOM keyboard event. It goes to the focused target's
927
+ * keyboard listeners and those above it; Tab and the arrows move focus.
928
+ * Enter and OK also click. This is how a host that routes the remote itself
929
+ * drives focus (`keys: 'manual'`); it works with `auto` too.
930
+ *
931
+ * @param {string | KeyboardEvent} key
932
+ * @returns {boolean} whether the bundle used it: false for a Tab off the end of the focus tree, say, so the host can move on
933
+ */
934
+ key(key) {
935
+ return sendKey(key, false)
936
+ },
937
+
938
+ /**
939
+ * The release of a key, for keyboard listeners that answer it.
940
+ *
941
+ * @param {string | KeyboardEvent} key
942
+ */
943
+ keyUp(key) {
944
+ sendKey(key, true)
945
+ },
946
+
947
+ /**
948
+ * Moves Rive's focus: to a target by its `rid` or name (`motion.focused`
949
+ * reports them), or `'next'`, `'previous'`, `'up'`, `'down'`, `'left'` or
950
+ * `'right'`. It does not touch DOM focus.
951
+ *
952
+ * @param {string} target
953
+ * @returns {boolean} whether focus moved
954
+ */
955
+ focus(target) {
956
+ if (target === 'next' || target === 'previous' || target === 'up' || target === 'down' || target === 'left' || target === 'right') return focusing.move(target)
957
+ return focusing.focus(target)
958
+ },
959
+
960
+ /** Takes Rive's focus off every target; the Blur listeners run. */
961
+ blur() {
962
+ focusing.clear()
963
+ },
964
+
965
+ /** The `rid` of the target that has Rive's focus, or null */
966
+ get focused() {
967
+ return focusing.current
968
+ },
969
+
970
+ /**
971
+ * A click from the host: without a target, every Click listener answers, as
972
+ * for OK; with one (its `rid` or name), a press and release on it.
973
+ *
974
+ * @param {string} [target]
975
+ */
976
+ click(target) {
977
+ if (!flow) return
978
+ if (target === undefined) {
979
+ flow.click()
980
+ return
981
+ }
982
+ const rid = focusing.resolve(target)
983
+ tap(rid === null ? target : rid)
984
+ },
985
+
986
+ /** @param {'play' | 'loop' | 'pause' | 'stop'} command */
987
+ control(command) {
988
+ player.setControl(command)
989
+ if (command === 'pause') audio.pause()
990
+ else if (command === 'stop') audio.stop()
991
+ else audio.resume()
992
+ },
993
+
994
+ /** @param {boolean} flag silences what is playing, and plays nothing until unmuted */
995
+ setMuted(flag) {
996
+ audio.setMuted(!!flag)
997
+ },
998
+
999
+ /** With the `manual` option, moves time on by these seconds and draws. */
1000
+ advance(/** @type {number} */ seconds) {
1001
+ player.advance(seconds)
1002
+ },
1003
+
1004
+ /**
1005
+ * Changes how the animation fits the canvas, over the bundle's.
1006
+ *
1007
+ * @param {{ fit?: string, alignment?: string | number[] }} where
1008
+ */
1009
+ place(where) {
1010
+ asked = { fit: where.fit !== undefined ? where.fit : asked.fit, alignment: where.alignment !== undefined ? where.alignment : asked.alignment }
1011
+ updateBand()
1012
+ player.resize()
1013
+ },
1014
+
1015
+ /** Call when the canvas's displayed size changes. */
1016
+ resize() {
1017
+ player.resize()
1018
+ updateBand()
1019
+ },
1020
+
1021
+ /**
1022
+ * @param {MotionEvent} name
1023
+ * @param {Function} listener
1024
+ * @returns {() => void} removes the listener
1025
+ */
1026
+ on(name, listener) {
1027
+ listeners[name] = (listeners[name] || []).concat([ listener ])
1028
+ return () => {
1029
+ listeners[name] = (listeners[name] || []).filter((one) => one !== listener)
1030
+ }
1031
+ },
1032
+
1033
+ destroy() {
1034
+ player.destroy()
1035
+ audio.destroy()
1036
+ canvas.removeEventListener('click', onClick)
1037
+ pointer.destroy()
1038
+ dom.destroy()
1039
+ focusing.setup([], [])
1040
+ flow = null
1041
+ bundle = null
1042
+ skins = noSkins
1043
+ makeSkins = null
1044
+ if (objects) objects.destroy()
1045
+ objects = null
1046
+ vectorList = null
1047
+ media = {}
1048
+ nested = createNested([])
1049
+ lists = createLists([], {}, { base: '', band: bandWidth, error: () => {}, focus: () => {} })
1050
+ listVersion = 0
1051
+ mixedMedia = null
1052
+ mixedBands = {}
1053
+ grid = null
1054
+ cell = null
1055
+ },
1056
+
1057
+ get state() {
1058
+ return flow ? flow.state : ''
1059
+ },
1060
+ get values() {
1061
+ return flow ? publicValues(bundle ? bundle.manifest.data : null, flow.values) : {}
1062
+ },
1063
+ get playback() {
1064
+ return player.playback
1065
+ },
1066
+ get muted() {
1067
+ return audio.muted
1068
+ },
1069
+ get renderer() {
1070
+ return player.renderer
1071
+ },
1072
+ /** The width of the layout band showing, or 0 when the artboard is not responsive or does not fit as `layout` */
1073
+ get band() {
1074
+ return band ? band.width : 0
1075
+ },
1076
+ /**
1077
+ * The artboard's size when it is the canvas's (Rive's `layout` fit sizes
1078
+ * it to the canvas, within what the converter solved), else 0 for the
1079
+ * picture's own, and the weights toward the neighbouring layout bands.
1080
+ */
1081
+ get artboard() {
1082
+ return { width: blended, height: blendedHeight, u: live.t, v: live.v }
1083
+ },
1084
+ /** How the animation fits the canvas now: `{ fit, alignment }` */
1085
+ get placement() {
1086
+ return placement()
1087
+ },
1088
+ }
1089
+ }
1090
+
1091
+ /**
1092
+ * Whether a bundle needs the mixer: its machine has layers, a blend state, a
1093
+ * cross-fading transition, one that waits for an exact exit time, a state that plays backwards or bounces or at another speed, or animations
1094
+ * that key different properties (`holds`), so one leaves what another set.
1095
+ *
1096
+ * @param {any} manifest
1097
+ * @returns {boolean}
1098
+ */
1099
+ export function usesMixer(manifest) {
1100
+ if (!exists(manifest.machine)) return false
1101
+ if (manifest.machine.holds === true) return true
1102
+ const layers = layersOf(manifest.machine)
1103
+ if (layers.length > 1) return true
1104
+
1105
+ const states = layers[0].states
1106
+ for (let n = 0; n < states.length; n++) {
1107
+ if (states[n].blend || states[n].mode || (states[n].speed && states[n].speed !== 1)) return true
1108
+ const list = states[n].transitions || []
1109
+ for (let k = 0; k < list.length; k++) if (list[k].durationMs > 0 || list[k].exitTime) return true
1110
+ }
1111
+ return false
1112
+ }
1113
+
1114
+ /**
1115
+ * What a layout band's mixed media is kept under.
1116
+ *
1117
+ * @param {{ width: number, height: number }} corner
1118
+ * @returns {string}
1119
+ */
1120
+ function bandKey(corner) {
1121
+ return corner.width + 'x' + corner.height
1122
+ }
1123
+
1124
+ /**
1125
+ * The first animation a state names that the bundle lacks.
1126
+ *
1127
+ * @param {any} state
1128
+ * @param {Record<string, Media>} items
1129
+ * @returns {string} "" when it has them all
1130
+ */
1131
+ function unknownAnimation(state, items) {
1132
+ const ids = []
1133
+ if (state.blend) for (let n = 0; n < state.blend.animations.length; n++) ids.push(state.blend.animations[n].animation)
1134
+ else ids.push(state.animation)
1135
+ for (let n = 0; n < ids.length; n++) if (!items[ids[n]]) return ids[n]
1136
+ return ''
1137
+ }
1138
+
1139
+ /**
1140
+ * Each slot that fills a text overlay → the input it reads, as Roku's
1141
+ * Motion.Task resolves them (bundle.bs resolveTexts).
1142
+ *
1143
+ * @param {any} manifest
1144
+ */
1145
+ function inputsOfSlots(manifest) {
1146
+ /** @type {Record<string, string>} */
1147
+ const result = {}
1148
+ const slots = (manifest.data && manifest.data.slots) || []
1149
+ for (let n = 0; n < slots.length; n++) {
1150
+ const slot = slots[n]
1151
+ // the overlay reads the slot's converted value, so a converter shows in the text
1152
+ if (slot.sources && slot.sources.length) result[slot.sid] = slot.sid
1153
+ }
1154
+ return result
1155
+ }