@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
@@ -0,0 +1,421 @@
1
+ /**
2
+ * The flow of a playing Motion, as Roku's runs it (motion.flow.bs): which
3
+ * state is active, what the inputs hold, and when live values change.
4
+ *
5
+ * With a state machine, states are entered by name, run their entry
6
+ * actions and play their animation; the machine then settles, taking any
7
+ * transitions whose guards now pass. Without one, the bundle's initial
8
+ * animation plays. The flow owns the state and the values; the player it is
9
+ * given draws.
10
+ *
11
+ * A machine may have several layers, each in a state of its own. With the
12
+ * `layer` hook the flow reports every layer's states (and the cross-fade of
13
+ * the transition that entered one) and reads how long each has played, so
14
+ * exit times are exact; without it, the first layer plays by `play` alone,
15
+ * as on Roku.
16
+ */
17
+
18
+ import { defaults, exists, find, interactionActions, isEvent, layersOf, nextExit, perform, pointerTargetsOf, takeTransition } from './motion.machine.js'
19
+ import { inputValue } from './motion.values.js'
20
+
21
+ /** Most transitions one change can chain through before the machine is taken to be cycling */
22
+ const MAX_SETTLE = 16
23
+
24
+ /**
25
+ * @typedef {object} Hooks
26
+ * @property {(animation: string, loops: boolean) => void} play show an animation
27
+ * @property {(values: Record<string, any>) => void} valuesChanged
28
+ * @property {(state: string, layer: number) => void} stateChanged
29
+ * @property {(event: string) => void} event a custom event the machine fired, or `openUrl:<url>`
30
+ * @property {(message: string) => void} error
31
+ * @property {(action: any) => void} [focus] a focus action (FocusTarget, FocusMove, FocusClear) ran
32
+ * @property {(layer: number, state: any, fadeMs: number, ease?: number[]) => void} [layer] a layer entered a state, fading in along `ease`'s curve; replaces `play`
33
+ * @property {(layer: number) => number} [elapsed] ms the layer's current state has played, unwrapped
34
+ * @property {import('../script/play/play.js').Scripts | null} [scripts] the scripts the bundle runs, read when a guard or action needs them
35
+ */
36
+
37
+ /**
38
+ * @param {any} manifest a bundle's motion.json
39
+ * @param {Hooks} hooks
40
+ */
41
+ export function createFlow(manifest, hooks) {
42
+ const graph = exists(manifest.machine) ? manifest.machine : null
43
+ const graphs = graph ? layersOf(graph) : []
44
+ const isMixed = typeof hooks.layer === 'function'
45
+ /** @type {Record<string, any>} */
46
+ let values = {}
47
+ /** @type {Record<string, boolean>} */
48
+ let events = {}
49
+ /** @type {string[]} each layer's current state */
50
+ let currents = []
51
+ /** @type {number[]} the latest exit time, in ms, a layer's current state has passed; -1 for none */
52
+ let crossed = []
53
+ let animation = ''
54
+ /** @type {Record<string, string>} */
55
+ let transitions = {}
56
+ /** Entry actions and listeners that run while a state is being entered must not settle the machine under it */
57
+ let busy = 0
58
+ /** the interaction whose actions are running, which a script's pointer event is of */
59
+ let trigger = ''
60
+
61
+ /**
62
+ * The defaults, with the caller's inputs over them; then the first state.
63
+ *
64
+ * @param {Record<string, any>} inputs
65
+ * @param {string} [requested] a state to start in instead of the machine's own
66
+ * @param {Record<string, string>} [followers] without a machine, the animation each one's end plays next
67
+ */
68
+ function start(inputs, requested, followers) {
69
+ values = defaultValues()
70
+ events = {}
71
+ transitions = followers || {}
72
+ currents = []
73
+ crossed = []
74
+ mergeInputs(inputs)
75
+ hooks.valuesChanged(values)
76
+
77
+ if (!graph) {
78
+ showAnimation(firstAnimation(requested))
79
+ return
80
+ }
81
+ busy++
82
+ for (let n = 0; n < graphs.length; n++) {
83
+ currents[n] = ''
84
+ crossed[n] = -1
85
+ enter(n, requested && find(graphs[n], requested) ? requested : graphs[n].initial, 0)
86
+ }
87
+ busy--
88
+ settle()
89
+ }
90
+
91
+ /** The view model's default instance, then the machine's own input values for anything it leaves out. */
92
+ function defaultValues() {
93
+ /** @type {Record<string, any>} */
94
+ const result = {}
95
+ const data = manifest.data || {}
96
+ const instance = data.instances && typeof data.defaultInstance === 'string' ? data.instances[data.defaultInstance] : null
97
+ if (instance) for (const name in instance) result[name] = instance[name]
98
+ if (graph) {
99
+ const inputs = defaults(graph)
100
+ for (const name in inputs) if (!(name in result)) result[name] = inputs[name]
101
+ }
102
+ return result
103
+ }
104
+
105
+ /** @param {string} [requested] */
106
+ function firstAnimation(requested) {
107
+ const candidates = [ requested, manifest.initial ]
108
+ for (let n = 0; n < candidates.length; n++) if (candidates[n] && manifest.animations[candidates[n]]) return /** @type {string} */ (candidates[n])
109
+ for (const id in manifest.animations) return id
110
+ return ''
111
+ }
112
+
113
+ // States
114
+ // ■■■■■■
115
+
116
+ /**
117
+ * The caller asked for a state: enter it directly, in the layer that has it.
118
+ *
119
+ * @param {string} name a machine state, or an animation without a machine
120
+ */
121
+ function jumpTo(name) {
122
+ if (!graph) {
123
+ showAnimation(name)
124
+ return
125
+ }
126
+ let layer = -1
127
+ for (let n = 0; n < graphs.length && layer < 0; n++) if (find(graphs[n], name)) layer = n
128
+ if (layer < 0) {
129
+ hooks.error("unknown state '" + name + "'")
130
+ return
131
+ }
132
+ enter(layer, name, 0)
133
+ settle()
134
+ }
135
+
136
+ /**
137
+ * Without a machine, a state is an animation, played once.
138
+ *
139
+ * @param {string} name
140
+ */
141
+ function showAnimation(name) {
142
+ currents = [ name ]
143
+ hooks.stateChanged(name, 0)
144
+ play(name, false)
145
+ }
146
+
147
+ /**
148
+ * Leaving the previous state runs its exit actions; entering runs the new
149
+ * one's entry actions and plays its animation.
150
+ *
151
+ * @param {number} layer
152
+ * @param {string} name
153
+ * @param {number} fadeMs how long the outgoing state fades into this one
154
+ * @param {number[]} [ease] the fade's cubic curve, `[x1, y1, x2, y2]`; linear without
155
+ */
156
+ function enter(layer, name, fadeMs, ease) {
157
+ const layerGraph = graphs[layer]
158
+ busy++
159
+ const leaving = find(layerGraph, currents[layer])
160
+ if (leaving) run(leaving.exitActions)
161
+
162
+ const state = find(layerGraph, name)
163
+ if (!state) {
164
+ busy--
165
+ return
166
+ }
167
+ currents[layer] = name
168
+ crossed[layer] = -1
169
+ hooks.stateChanged(name, layer)
170
+ run(state.entryActions)
171
+ if (isMixed) {
172
+ if (typeof state.animation === 'string' || state.blend) /** @type {NonNullable<Hooks['layer']>} */ (hooks.layer)(layer, state, fadeMs, ease)
173
+ } else if (layer === 0 && typeof state.animation === 'string') {
174
+ play(state.animation, state.loop === true)
175
+ }
176
+ busy--
177
+ }
178
+
179
+ /**
180
+ * Takes transitions until none passes, then clears the events this round
181
+ * consumed. Every layer sees the same events in a round.
182
+ */
183
+ function settle() {
184
+ if (!graph) return
185
+ for (let n = 0; n < MAX_SETTLE; n++) {
186
+ /** @type {any[]} */
187
+ const taken = []
188
+ for (let layer = 0; layer < graphs.length; layer++) taken.push(takeTransition(graphs[layer], currents[layer], values, events, clockOf(layer), hooks.scripts || null))
189
+ if (!taken.some(Boolean)) break
190
+ // an event that moved the machine is spent
191
+ events = {}
192
+ for (let layer = 0; layer < graphs.length; layer++) {
193
+ if (taken[layer]) enter(layer, taken[layer].toState, taken[layer].durationMs || 0, taken[layer].ease)
194
+ }
195
+ }
196
+ events = {}
197
+ hooks.valuesChanged(values)
198
+ }
199
+
200
+ /**
201
+ * How long a layer's state has played, for exit times; null when the
202
+ * player does not say, and exit times fall back to the whole timeline.
203
+ *
204
+ * @param {number} layer
205
+ * @returns {{ elapsed: number, length: number } | null}
206
+ */
207
+ function clockOf(layer) {
208
+ if (!hooks.elapsed) return null
209
+ return { elapsed: hooks.elapsed(layer), length: lengthOf(find(graphs[layer], currents[layer])) }
210
+ }
211
+
212
+ /**
213
+ * A state's timeline in ms: its animation's, or a blend's longest.
214
+ *
215
+ * @param {any} state
216
+ * @returns {number}
217
+ */
218
+ function lengthOf(state) {
219
+ if (!state) return 0
220
+ const ids = []
221
+ if (state.blend) for (let n = 0; n < state.blend.animations.length; n++) ids.push(state.blend.animations[n].animation)
222
+ else ids.push(state.animation)
223
+
224
+ let longest = 0
225
+ for (let n = 0; n < ids.length; n++) {
226
+ const entry = manifest.animations[ids[n]]
227
+ if (entry && entry.fps > 0) longest = Math.max(longest, (entry.frames / entry.fps) * 1000)
228
+ }
229
+ return longest
230
+ }
231
+
232
+ /**
233
+ * Time has passed: a transition waiting on an exit time may now go. Called
234
+ * as the player advances; settles only when a layer crosses an exit time.
235
+ */
236
+ function tick() {
237
+ if (!graph || !hooks.elapsed) return
238
+ for (let layer = 0; layer < graphs.length; layer++) {
239
+ const state = find(graphs[layer], currents[layer])
240
+ const next = nextExit(state, lengthOf(state), crossed[layer])
241
+ if (next < 0 || hooks.elapsed(layer) < next) continue
242
+
243
+ crossed[layer] = next
244
+ settle()
245
+ return
246
+ }
247
+ }
248
+
249
+ /**
250
+ * @param {string} id
251
+ * @param {boolean} loops
252
+ */
253
+ function play(id, loops) {
254
+ if (!manifest.animations[id]) {
255
+ hooks.error("unknown animation '" + id + "'")
256
+ return
257
+ }
258
+ animation = id
259
+ hooks.play(id, loops)
260
+ }
261
+
262
+ // Inputs and interactions
263
+ // ■■■■■■■■■■■■■■■■■■■■■■■
264
+
265
+ /**
266
+ * Live values: changed ones are held, an Event input set to true fires.
267
+ *
268
+ * @param {Record<string, any>} inputs
269
+ */
270
+ function takeInputs(inputs) {
271
+ if (!mergeInputs(inputs)) return
272
+ hooks.valuesChanged(values)
273
+ settle()
274
+ }
275
+
276
+ /**
277
+ * Values were written straight into `values` (by the bundle's scripted
278
+ * objects, which the next ones read at once): the player redraws and the
279
+ * machine settles, as for `takeInputs`.
280
+ */
281
+ function changed() {
282
+ if (graph) settle()
283
+ else hooks.valuesChanged(values)
284
+ }
285
+
286
+ /**
287
+ * @param {Record<string, any>} inputs
288
+ * @returns {boolean} whether anything changed or fired
289
+ */
290
+ function mergeInputs(inputs) {
291
+ let changed = false
292
+ for (const name in inputs || {}) {
293
+ // an enum may be given by name; the machine compares its index
294
+ const value = inputValue(manifest.data, name, inputs[name])
295
+ if (graph && isEvent(graph, name)) {
296
+ if (value === true) {
297
+ events[name] = true
298
+ changed = true
299
+ }
300
+ } else if (values[name] !== value) {
301
+ values[name] = value
302
+ changed = true
303
+ }
304
+ }
305
+ return changed
306
+ }
307
+
308
+ /**
309
+ * Runs the actions of a key binding.
310
+ *
311
+ * @param {any[]} actions
312
+ */
313
+ function act(actions) {
314
+ if (!graph) return
315
+ run(actions)
316
+ settle()
317
+ }
318
+
319
+ /**
320
+ * Focus moved onto or off a node: the Focus or Blur listeners on it run.
321
+ *
322
+ * @param {'Focus' | 'Blur'} type
323
+ * @param {string} target the node's `rid`, "" for the artboard
324
+ */
325
+ function focusEvent(type, target) {
326
+ if (!graph) return
327
+ run(interactionActions(graph, type, '', target))
328
+ if (!busy) settle()
329
+ }
330
+
331
+ /**
332
+ * A tap or click on the animation. Called by the host, it clicks every
333
+ * Click listener, as OK does; from a click on the canvas, only the listeners
334
+ * with no target of their own, which watch the whole artboard (those with
335
+ * a target are clicked by `pointer`, where the pointer hit it).
336
+ *
337
+ * @param {boolean} [isUntargeted] click only the listeners that have no target
338
+ */
339
+ function click(isUntargeted) {
340
+ if (!graph) return
341
+ trigger = 'Click'
342
+ run(interactionActions(graph, 'Click', '', isUntargeted ? '' : undefined))
343
+ trigger = ''
344
+ settle()
345
+ }
346
+
347
+ /**
348
+ * A pointer event on a target: runs the actions of the interactions of that type on it.
349
+ *
350
+ * @param {string} type Click, PointerDown, PointerUp, PointerEnter, PointerExit or PointerMove
351
+ * @param {string} target the `rid` of the shape or group, or "" for the listeners without a target
352
+ */
353
+ function pointer(type, target) {
354
+ if (!graph) return
355
+ trigger = type
356
+ run(interactionActions(graph, type, '', target))
357
+ trigger = ''
358
+ settle()
359
+ }
360
+
361
+ /**
362
+ * The pointer interactions' targets, for the player to watch.
363
+ *
364
+ * @returns {Record<string, string[]>} each target's `rid` ("" for none) → the interaction types on it
365
+ */
366
+ function pointerTargets() {
367
+ return graph ? pointerTargetsOf(graph) : {}
368
+ }
369
+
370
+ /**
371
+ * A layer's active state played to its end: its OnComplete interactions
372
+ * run, or without a machine, the animation `transitions` names for it plays.
373
+ *
374
+ * @param {number} [layer]
375
+ */
376
+ function finished(layer) {
377
+ const index = layer || 0
378
+ if (!graph) {
379
+ const next = transitions[currents[0]]
380
+ if (next) showAnimation(next)
381
+ return
382
+ }
383
+ run(interactionActions(graphs[index], 'OnComplete', currents[index]))
384
+ settle()
385
+ }
386
+
387
+ /** @param {any[]} actions */
388
+ function run(actions) {
389
+ if (!graph || !actions || !actions.length) return
390
+ const effects = perform(actions, graph, values, events, hooks.scripts || null, trigger)
391
+ for (let n = 0; n < effects.events.length; n++) hooks.event(effects.events[n])
392
+ if (effects.changed) hooks.valuesChanged(values)
393
+ if (hooks.focus) for (let n = 0; n < effects.focus.length; n++) hooks.focus(effects.focus[n])
394
+ }
395
+
396
+ return {
397
+ start,
398
+ jumpTo,
399
+ takeInputs,
400
+ changed,
401
+ act,
402
+ focusEvent,
403
+ click,
404
+ pointer,
405
+ pointerTargets,
406
+ finished,
407
+ tick,
408
+ get state() {
409
+ return currents[0] || ''
410
+ },
411
+ get states() {
412
+ return currents.slice()
413
+ },
414
+ get animation() {
415
+ return animation
416
+ },
417
+ get values() {
418
+ return values
419
+ },
420
+ }
421
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Keys and taps as a bundle's keyboard listeners name them. A bundle's
3
+ * `remoteKeys` use Rive's key names (`enter`, `space`, `up`); a DOM key
4
+ * event, or a Roku remote's name, becomes those here, so the same bundle
5
+ * answers a keyboard, a TV remote's arrows and a Roku remote alike.
6
+ */
7
+
8
+ /** Roku remote names → Rive key names, as Roku's Motion maps them (motion.flow.bs) */
9
+ const ROKU = {
10
+ OK: [ 'enter', 'kpEnter', 'space' ],
11
+ back: [ 'escape', 'backspace' ],
12
+ up: [ 'up' ],
13
+ down: [ 'down' ],
14
+ left: [ 'left' ],
15
+ right: [ 'right' ],
16
+ play: [ 'space' ],
17
+ }
18
+
19
+ /** `KeyboardEvent.key` values → Rive key names; older browsers send the short forms */
20
+ const DOM_KEYS = {
21
+ Enter: 'enter',
22
+ ' ': 'space',
23
+ Spacebar: 'space',
24
+ Escape: 'escape',
25
+ Esc: 'escape',
26
+ Backspace: 'backspace',
27
+ ArrowUp: 'up',
28
+ Up: 'up',
29
+ ArrowDown: 'down',
30
+ Down: 'down',
31
+ ArrowLeft: 'left',
32
+ Left: 'left',
33
+ ArrowRight: 'right',
34
+ Right: 'right',
35
+ Tab: 'tab',
36
+ Delete: 'deleteKey',
37
+ Del: 'deleteKey',
38
+ Insert: 'insert',
39
+ Home: 'home',
40
+ End: 'end',
41
+ PageUp: 'pageUp',
42
+ PageDown: 'pageDown',
43
+ }
44
+
45
+ /** `keyCode` → Rive key names, for browsers that predate `key` */
46
+ const KEY_CODES = { 8: 'backspace', 9: 'tab', 13: 'enter', 27: 'escape', 32: 'space', 33: 'pageUp', 34: 'pageDown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down', 45: 'insert', 46: 'deleteKey' }
47
+
48
+ /** `KeyboardEvent.code` values that are not a letter, digit or function key → Rive key names */
49
+ const DOM_CODES = {
50
+ NumpadEnter: 'kpEnter',
51
+ NumpadDecimal: 'kpDecimal',
52
+ NumpadDivide: 'kpDivide',
53
+ NumpadMultiply: 'kpMultiply',
54
+ NumpadSubtract: 'kpSubtract',
55
+ NumpadAdd: 'kpAdd',
56
+ Minus: 'minus',
57
+ Equal: 'equal',
58
+ Comma: 'comma',
59
+ Period: 'period',
60
+ Slash: 'slash',
61
+ Semicolon: 'semicolon',
62
+ Quote: 'apostrophe',
63
+ Backquote: 'graveAccent',
64
+ BracketLeft: 'leftBracket',
65
+ BracketRight: 'rightBracket',
66
+ Backslash: 'backslash',
67
+ }
68
+
69
+ /**
70
+ * The Rive key names a key stands for: a Roku remote name maps to its
71
+ * group, anything else is lowercased as given.
72
+ *
73
+ * @param {string} key
74
+ * @returns {string[]}
75
+ */
76
+ export function keyNames(key) {
77
+ const roku = ROKU[/** @type {keyof typeof ROKU} */ (key)]
78
+ return roku ? roku : [ key.toLowerCase() ]
79
+ }
80
+
81
+ /**
82
+ * The Rive key name for a DOM keyboard event, or "" for a key no bundle listens for.
83
+ * `code` (the physical key, as Rive reads it) comes first, then `key`, then
84
+ * `keyCode` for browsers that predate both.
85
+ *
86
+ * @param {{ key?: string, keyCode?: number, code?: string }} event
87
+ * @returns {string}
88
+ */
89
+ export function keyFromEvent(event) {
90
+ const code = event.code
91
+ if (code) {
92
+ const letter = /^Key([A-Z])$/.exec(code)
93
+ if (letter) return letter[1].toLowerCase()
94
+ const digit = /^(Digit|Numpad)(\d)$/.exec(code)
95
+ if (digit) return (digit[1] === 'Digit' ? 'key' : 'kp') + digit[2]
96
+ const numbered = /^F(\d{1,2})$/.exec(code)
97
+ if (numbered) return 'f' + numbered[1]
98
+ const named = DOM_CODES[/** @type {keyof typeof DOM_CODES} */ (code)]
99
+ if (named) return named
100
+ }
101
+ const byKey = event.key ? DOM_KEYS[/** @type {keyof typeof DOM_KEYS} */ (event.key)] : undefined
102
+ if (byKey) return byKey
103
+
104
+ const byCode = event.keyCode ? KEY_CODES[/** @type {keyof typeof KEY_CODES} */ (event.keyCode)] : undefined
105
+ if (byCode) return byCode
106
+
107
+ // a printable key is listened for by its own character, lowercased; a digit is `key0`
108
+ if (event.key && event.key.length === 1) return /\d/.test(event.key) ? 'key' + event.key : event.key.toLowerCase()
109
+ return ''
110
+ }
111
+
112
+ /**
113
+ * The modifiers held during a DOM keyboard event, as a KeyboardInput's mask: 1 shift, 2 ctrl, 4 alt, 8 meta.
114
+ *
115
+ * @param {{ shiftKey?: boolean, ctrlKey?: boolean, altKey?: boolean, metaKey?: boolean }} event
116
+ * @returns {number}
117
+ */
118
+ export function modifiersFromEvent(event) {
119
+ return (event.shiftKey ? 1 : 0) + (event.ctrlKey ? 2 : 0) + (event.altKey ? 4 : 0) + (event.metaKey ? 8 : 0)
120
+ }
121
+
122
+ /**
123
+ * A key the host names as a string: `tab`, `OK`, `a`, with modifiers in front
124
+ * (`shift+tab`, `ctrl+a`).
125
+ *
126
+ * @param {string} text
127
+ * @returns {{ names: string[], mods: number, key: string }}
128
+ */
129
+ export function parseKey(text) {
130
+ const parts = text.split('+')
131
+ let mods = 0
132
+ while (parts.length > 1) {
133
+ const part = parts[0].toLowerCase()
134
+ const bit = part === 'shift' ? 1 : part === 'ctrl' || part === 'control' ? 2 : part === 'alt' ? 4 : part === 'meta' || part === 'cmd' ? 8 : 0
135
+ if (!bit) break
136
+ mods += bit
137
+ parts.shift()
138
+ }
139
+ const key = parts.join('+')
140
+ return { names: keyNames(key), mods, key }
141
+ }
142
+
143
+ /**
144
+ * Whether a key clicks, as OK does on Roku: Enter, or Roku's OK.
145
+ *
146
+ * @param {string} key
147
+ */
148
+ export function isClick(key) {
149
+ return key === 'OK' || key === 'enter'
150
+ }