@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.
- package/LICENSE +201 -0
- package/audio/audio.cues.js +27 -0
- package/audio/audio.js +259 -0
- package/bands/bands.js +392 -0
- package/bundle/bundle.fonts.js +55 -0
- package/bundle/bundle.js +131 -0
- package/focus/focus.direction.js +46 -0
- package/focus/focus.dom.js +158 -0
- package/focus/focus.js +277 -0
- package/focus/focus.tree.js +146 -0
- package/hit/hit.geometry.js +113 -0
- package/hit/hit.js +129 -0
- package/list/list.flex.js +181 -0
- package/list/list.items.js +80 -0
- package/list/list.js +518 -0
- package/lottie/lottie.js +124 -0
- package/lottie/lottie.layers.js +416 -0
- package/lottie/lottie.paint.js +238 -0
- package/lottie/lottie.paths.js +474 -0
- package/lottie/lottie.props.js +265 -0
- package/lottie/lottie.shapes.js +259 -0
- package/mixer/mixer.js +307 -0
- package/mixer/mixer.mix.js +107 -0
- package/mixer/mixer.stage.js +431 -0
- package/motion/motion.convert.js +404 -0
- package/motion/motion.flow.js +421 -0
- package/motion/motion.keys.js +150 -0
- package/motion/motion.machine.js +382 -0
- package/motion/motion.nested.js +345 -0
- package/motion/motion.values.js +368 -0
- package/motion.js +1155 -0
- package/package.json +47 -4
- package/player/overlay/overlay.js +325 -0
- package/player/overlay/overlay.layout.js +208 -0
- package/player/player.fit.js +101 -0
- package/player/player.js +421 -0
- package/player/player.raster.js +127 -0
- package/pointer/pointer.js +292 -0
- package/readme.md +345 -0
- package/script/luau/lib/base/base.js +202 -0
- package/script/luau/lib/base/base.table.js +189 -0
- package/script/luau/lib/lib.args.js +152 -0
- package/script/luau/lib/lib.js +23 -0
- package/script/luau/lib/math/math.bit32.js +144 -0
- package/script/luau/lib/math/math.js +288 -0
- package/script/luau/lib/math/math.vector.js +125 -0
- package/script/luau/lib/string/string.format.js +143 -0
- package/script/luau/lib/string/string.js +160 -0
- package/script/luau/lib/string/string.pattern.js +505 -0
- package/script/luau/load/load.js +362 -0
- package/script/luau/load/load.ops.js +79 -0
- package/script/luau/luau.convert.js +259 -0
- package/script/luau/luau.js +132 -0
- package/script/luau/luau.md +111 -0
- package/script/luau/vm/vm.js +167 -0
- package/script/luau/vm/vm.meta.js +401 -0
- package/script/luau/vm/vm.num.js +236 -0
- package/script/luau/vm/vm.run.js +842 -0
- package/script/luau/vm/vm.value.js +406 -0
- package/script/play/play.js +199 -0
- package/script/play/play.objects.js +295 -0
- package/script/play/play.scope.js +91 -0
- package/script/rive/api/api.statics.js +31 -0
- package/script/rive/api/api.type.js +188 -0
- package/script/rive/api/artboard/artboard.js +89 -0
- package/script/rive/api/assets/assets.js +72 -0
- package/script/rive/api/data/data.value.js +99 -0
- package/script/rive/api/draw/draw.contours.js +163 -0
- package/script/rive/api/draw/draw.measure.js +196 -0
- package/script/rive/api/draw/draw.paint.js +235 -0
- package/script/rive/api/draw/draw.path.js +153 -0
- package/script/rive/api/draw/draw.renderer.js +117 -0
- package/script/rive/api/math/math.color.js +96 -0
- package/script/rive/api/math/math.mat2d.js +180 -0
- package/script/rive/api/math/math.vector.js +121 -0
- package/script/rive/api/media/media.audio.js +113 -0
- package/script/rive/api/media/media.draw.js +73 -0
- package/script/rive/api/media/media.image.js +61 -0
- package/script/rive/api/media/media.mesh.js +50 -0
- package/script/rive/api/pointer/pointer.event.js +64 -0
- package/script/rive/api/pointer/pointer.route.js +69 -0
- package/script/rive/api/text/text.font.js +129 -0
- package/script/rive/api/text/text.js +278 -0
- package/script/rive/api/text/text.layout.js +259 -0
- package/script/rive/context/context.js +76 -0
- package/script/rive/context/context.memory.js +196 -0
- package/script/rive/context/context.viewmodel.js +302 -0
- package/script/rive/context/lists/lists.js +160 -0
- package/script/rive/context/lists/lists.values.js +281 -0
- package/script/rive/host/host.globals.js +44 -0
- package/script/rive/host/host.js +533 -0
- package/script/rive/host/host.node.js +45 -0
- package/script/rive/rive.js +23 -0
- package/script/rive/rive.md +180 -0
- package/script/script.js +11 -0
- package/skin/skin.deform.js +514 -0
- package/skin/skin.ik.js +260 -0
- package/skin/skin.js +297 -0
- 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
|
+
}
|