@px-petals/roku.motion-web 0.0.0-stage → 1.0.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.
@@ -0,0 +1,345 @@
1
+ /**
2
+ * The state machines of nested artboards. A bundle's `nested` lists each
3
+ * nested artboard that runs a machine of its own: the machine, where each of
4
+ * its animations sits on the precomp's timeline, and the layer's id. Each
5
+ * runs through the same flow as the bundle's own machine, with its animations
6
+ * as the flow's, and its clock moves the precomp layer's time remap, which
7
+ * the renderer reads from the slot named by the layer's `nid`.
8
+ *
9
+ * A nested machine takes the bundle's inputs of the same name, and
10
+ * is advanced by the player's clock. Its pointer listeners are watched as
11
+ * `<sid>/<rid>`: the renderer records a nested layer's groups under the
12
+ * layer's `nid`, since the nested artboard numbers its objects from 0 as the
13
+ * bundle's own does.
14
+ *
15
+ * Its focus nodes are in the host's focus tree, named the same way. A focus
16
+ * or blur on one runs that machine's Focus and Blur listeners, a key bound to
17
+ * one runs in that machine (`bindings`), and a focus action the machine runs
18
+ * goes to `options.focus`.
19
+ */
20
+
21
+ import { createStage } from '../mixer/mixer.stage.js'
22
+ import { createFlow } from './motion.flow.js'
23
+
24
+ /**
25
+ * @typedef {object} NestedEntry
26
+ * @property {string} sid the layer's id, which names its time remap slot
27
+ * @property {number} fps the precomp timeline's frame rate
28
+ * @property {any} machine a dotLottie state machine naming `animations`
29
+ * @property {Record<string, {start: number, frames: number, loop: boolean}>} animations
30
+ * @property {Record<string, any>} inputs what the instance sets on the machine's inputs
31
+ * @property {number} [speed] how fast the instance plays its machine; 1 without
32
+ * @property {string[]} [shared] the machine's inputs that are properties of the host's view model, which it is bound to: what the machine sets on them reaches the host
33
+ * @property {any[]} [remoteKeys] the machine's key listeners, each with the `target` focus node of its own artboard that hears it
34
+ */
35
+
36
+ /**
37
+ * @typedef {object} NestedOptions
38
+ * @property {(changes: Record<string, any>) => void} [write] a machine set view-model properties of the host: their names, with the values
39
+ * @property {(sid: string, values: Record<string, any>) => void} [values] a machine set values: all of its inputs, as it now has them (a list row's own machine sets properties of the row's view model)
40
+ * @property {(action: any, sid: string) => void} [focus] a machine ran a focus action (FocusTarget with its own artboard's rid, FocusMove, FocusClear); `sid` is the machine's
41
+ */
42
+
43
+ /**
44
+ * @param {NestedEntry[] | undefined} entries a bundle's `nested`
45
+ * @param {NestedOptions} [options]
46
+ * @returns {{ tick: (seconds: number) => void, takeInputs: (inputs: Record<string, any>) => void, apply: (slots: Record<string, any>, fps: number) => void, pointerTargets: () => Record<string, string[]>, pointer: (type: string, target: string) => boolean, focusEvent: (type: 'Focus' | 'Blur', sid: string, rid: string) => boolean, act: (sid: string, actions: any[]) => boolean, bindings: () => any[], flushFocus: () => void, count: number }}
47
+ */
48
+ export function createNested(entries, options) {
49
+ /** @type {any[]} */
50
+ const items = []
51
+ const list = entries || []
52
+ /** @type {{ action: any, sid: string }[] | null} focus actions the machines ran as they started, before the host's focus tree was there */
53
+ let early = []
54
+ const onFocus = (/** @type {any} */ action, /** @type {string} */ sid) => {
55
+ if (early) early.push({ action, sid })
56
+ else if (options && options.focus) options.focus(action, sid)
57
+ }
58
+ for (let n = 0; n < list.length; n++) items.push(createItem(list[n], (options && options.write) || null, onFocus, (options && options.values) || null))
59
+
60
+ return {
61
+ count: items.length,
62
+
63
+ /**
64
+ * Once the focus tree is there: the focus actions machines ran as they
65
+ * started (a state that focuses a node) take effect.
66
+ */
67
+ flushFocus() {
68
+ const queued = early || []
69
+ early = null
70
+ for (let n = 0; n < queued.length; n++) onFocus(queued[n].action, queued[n].sid)
71
+ },
72
+
73
+ /**
74
+ * The key bindings of the machines' focus nodes, named by the host's
75
+ * tree: each `target` is `<sid>/<rid>`, and `scope` is the machine the
76
+ * actions run in.
77
+ *
78
+ * @returns {any[]}
79
+ */
80
+ bindings() {
81
+ const found = []
82
+ for (let n = 0; n < items.length; n++) {
83
+ const keys = items[n].entry.remoteKeys || []
84
+ for (let k = 0; k < keys.length; k++) found.push(extend(keys[k], { target: items[n].entry.sid + '/' + keys[k].target, scope: items[n].entry.sid }))
85
+ }
86
+ return found
87
+ },
88
+
89
+ /**
90
+ * A node of a machine's artboard gained or lost focus: its Focus or Blur
91
+ * listeners run.
92
+ *
93
+ * @param {'Focus' | 'Blur'} type
94
+ * @param {string} sid the machine's, `row` for a list row's own
95
+ * @param {string} rid the node in the machine's artboard, "" for the artboard
96
+ * @returns {boolean} whether a machine owns it
97
+ */
98
+ focusEvent(type, sid, rid) {
99
+ for (let n = 0; n < items.length; n++) {
100
+ if (items[n].entry.sid !== sid) continue
101
+ items[n].flow.focusEvent(type, rid)
102
+ return true
103
+ }
104
+ return false
105
+ },
106
+
107
+ /**
108
+ * Runs the actions of a key binding in the machine it belongs to.
109
+ *
110
+ * @param {string} sid
111
+ * @param {any[]} actions
112
+ * @returns {boolean} whether a machine owns it
113
+ */
114
+ act(sid, actions) {
115
+ for (let n = 0; n < items.length; n++) {
116
+ if (items[n].entry.sid !== sid) continue
117
+ items[n].flow.act(actions)
118
+ return true
119
+ }
120
+ return false
121
+ },
122
+
123
+ /**
124
+ * Moves every nested machine's clock on.
125
+ *
126
+ * @param {number} seconds
127
+ */
128
+ tick(seconds) {
129
+ for (let n = 0; n < items.length; n++) advance(items[n], seconds)
130
+ },
131
+
132
+ /**
133
+ * Gives each machine the inputs it has by name.
134
+ *
135
+ * @param {Record<string, any>} inputs
136
+ */
137
+ takeInputs(inputs) {
138
+ for (let n = 0; n < items.length; n++) {
139
+ /** @type {Record<string, any>} */
140
+ const own = {}
141
+ let any = false
142
+ for (const name in inputs) {
143
+ if (items[n].names[name]) {
144
+ own[name] = inputs[name]
145
+ any = true
146
+ }
147
+ }
148
+ if (!any) continue
149
+ // what the host sets is what the machine already has: it is not the machine's to write back
150
+ for (const name in own) if (items[n].synced) items[n].synced[name] = own[name]
151
+ items[n].flow.takeInputs(own)
152
+ }
153
+ },
154
+
155
+ /**
156
+ * Where the machines' pointer listeners are: each target's `<sid>/<rid>`
157
+ * and the interaction types on it. A listener on the whole artboard
158
+ * is not offered by Rive, and is not watched.
159
+ *
160
+ * @returns {Record<string, string[]>}
161
+ */
162
+ pointerTargets() {
163
+ /** @type {Record<string, string[]>} */
164
+ const found = {}
165
+ for (let n = 0; n < items.length; n++) {
166
+ const targets = items[n].flow.pointerTargets()
167
+ // a list row's own machine names its shapes as the row's Lottie does; a nested artboard's are under its layer's id
168
+ const prefix = items[n].entry.sid === 'row' ? '' : items[n].entry.sid + '/'
169
+ for (const rid in targets) if (rid !== '') found[prefix + rid] = targets[rid]
170
+ }
171
+ return found
172
+ },
173
+
174
+ /**
175
+ * A pointer event on a target of `pointerTargets`.
176
+ *
177
+ * @param {string} type
178
+ * @param {string} target `<sid>/<rid>`
179
+ * @returns {boolean} whether a nested machine owns the target
180
+ */
181
+ pointer(type, target) {
182
+ const at = target.indexOf('/')
183
+ for (let n = 0; n < items.length; n++) {
184
+ const isRow = at < 0 && items[n].entry.sid === 'row'
185
+ if (!isRow && (at < 0 || items[n].entry.sid !== target.slice(0, at))) continue
186
+ items[n].flow.pointer(type, isRow ? target : target.slice(at + 1))
187
+ return true
188
+ }
189
+ return false
190
+ },
191
+
192
+ /**
193
+ * Writes where each layer's time remap is into the animation's slots,
194
+ * in seconds of an animation at `fps`.
195
+ *
196
+ * @param {Record<string, any>} slots
197
+ * @param {number} fps
198
+ */
199
+ apply(slots, fps) {
200
+ for (let n = 0; n < items.length; n++) {
201
+ const item = items[n]
202
+ if (item.stage) {
203
+ applyPoses(item, slots, fps)
204
+ continue
205
+ }
206
+ if (!item.segment) continue
207
+ const frame = item.segment.start + Math.min(item.time * item.entry.fps, item.segment.frames)
208
+ slots[item.entry.sid] = frame / fps
209
+ }
210
+ },
211
+ }
212
+ }
213
+
214
+ /**
215
+ * @param {NestedEntry} entry
216
+ * @param {((changes: Record<string, any>) => void) | null} write
217
+ * @param {(action: any, sid: string) => void} onFocus
218
+ * @param {((sid: string, values: Record<string, any>) => void) | null} onValues
219
+ */
220
+ function createItem(entry, write, onFocus, onValues) {
221
+ /** @type {Record<string, boolean>} */
222
+ const names = {}
223
+ const inputs = entry.machine.inputs || []
224
+ for (let n = 0; n < inputs.length; n++) names[inputs[n].name] = true
225
+
226
+ const shared = entry.shared || []
227
+ /** @type {any} */
228
+ const item = { entry, names, segment: null, loops: false, time: 0, isDone: false, flow: null, synced: null, stage: null }
229
+ // several layers play several animations at once, which a stage keeps the clocks of, as the host's does
230
+ const isMixed = Array.isArray(entry.machine.layers) && entry.machine.layers.length > 1
231
+ /** @type {Record<string, any>} */
232
+ const timed = {}
233
+ for (const id in entry.animations) timed[id] = { start: entry.animations[id].start, frames: entry.animations[id].frames, loop: entry.animations[id].loop, fps: entry.fps }
234
+ if (isMixed) item.stage = createStage(timed)
235
+ /** @type {any} */
236
+ const hooks = {
237
+ play(/** @type {string} */ animation, /** @type {boolean} */ loops) {
238
+ item.segment = entry.animations[animation]
239
+ item.loops = loops
240
+ item.time = 0
241
+ item.isDone = false
242
+ },
243
+ valuesChanged(/** @type {Record<string, any>} */ values) {
244
+ if (onValues) onValues(entry.sid, values)
245
+ if (!item.synced || !write) return
246
+ // a machine that sets a property of the host's view model (a listener, a state's action) tells the host
247
+ /** @type {Record<string, any>} */
248
+ const changes = {}
249
+ let any = false
250
+ for (let n = 0; n < shared.length; n++) {
251
+ const name = shared[n]
252
+ if (values[name] === undefined || values[name] === item.synced[name]) continue
253
+ item.synced[name] = values[name]
254
+ changes[name] = values[name]
255
+ any = true
256
+ }
257
+ if (any) write(changes)
258
+ },
259
+ stateChanged() {},
260
+ event() {},
261
+ error() {},
262
+ focus(/** @type {any} */ action) {
263
+ onFocus(action, entry.sid)
264
+ },
265
+ }
266
+ if (item.stage) {
267
+ hooks.layer = (/** @type {number} */ layer, /** @type {any} */ state, /** @type {number} */ fadeMs, /** @type {number[] | undefined} */ ease) => item.stage.enter(layer, state, fadeMs, ease)
268
+ hooks.elapsed = (/** @type {number} */ layer) => item.stage.elapsedMs(layer)
269
+ }
270
+ item.flow = createFlow({ machine: entry.machine, animations: isMixed ? timed : entry.animations, data: {}, remoteKeys: [] }, hooks)
271
+ item.flow.start(entry.inputs || {})
272
+ // what it starts with is the host's default instance: nothing to tell the host yet
273
+ if (shared.length) {
274
+ item.synced = {}
275
+ for (let n = 0; n < shared.length; n++) item.synced[shared[n]] = item.flow.values[shared[n]]
276
+ }
277
+ return item
278
+ }
279
+
280
+ /**
281
+ * @param {any} item
282
+ * @param {number} seconds
283
+ */
284
+ function advance(item, seconds) {
285
+ if (item.stage) {
286
+ const done = item.stage.advance(seconds * (item.entry.speed > 0 ? item.entry.speed : 1))
287
+ for (let n = 0; n < done.length; n++) item.flow.finished(done[n])
288
+ return
289
+ }
290
+ if (!item.segment) return
291
+ item.time += seconds * (item.entry.speed > 0 ? item.entry.speed : 1)
292
+ const length = item.segment.frames / item.entry.fps
293
+ if (item.time < length) return
294
+
295
+ if (item.loops) {
296
+ item.time %= length
297
+ return
298
+ }
299
+ item.time = length
300
+ if (item.isDone) return
301
+ // the animation ended: its OnComplete interactions may start another
302
+ item.isDone = true
303
+ item.flow.finished()
304
+ }
305
+
306
+ /**
307
+ * What a machine with several layers shows: the time remap follows the first
308
+ * layer, and the poses of them all are in a slot of their own, for the
309
+ * properties that say which segments key them to mix.
310
+ *
311
+ * @param {any} item
312
+ * @param {Record<string, any>} slots
313
+ * @param {number} fps
314
+ */
315
+ function applyPoses(item, slots, fps) {
316
+ const entry = item.entry
317
+ const lead = item.stage.lead()
318
+ if (lead) {
319
+ const segment = entry.animations[lead.animation]
320
+ slots[entry.sid] = (segment.start + Math.min(lead.time * entry.fps, segment.frames)) / fps
321
+ }
322
+ const poses = item.stage.poses(item.flow.values)
323
+ /** @type {{ start: number, frame: number, weight: number }[]} */
324
+ const found = []
325
+ for (let n = 0; n < poses.length; n++) {
326
+ const segment = entry.animations[poses[n].animation]
327
+ if (segment) found.push({ start: segment.start, frame: Math.min(poses[n].time * entry.fps, segment.frames), weight: poses[n].weight })
328
+ }
329
+ slots['pz:' + entry.sid] = found
330
+ }
331
+
332
+ /**
333
+ * A copy of an object with fields added over it (the 2014 baseline has no `Object.assign`).
334
+ *
335
+ * @param {Record<string, any>} base
336
+ * @param {Record<string, any>} more
337
+ * @returns {Record<string, any>}
338
+ */
339
+ export function extend(base, more) {
340
+ /** @type {Record<string, any>} */
341
+ const found = {}
342
+ for (const name in base) found[name] = base[name]
343
+ for (const name in more) found[name] = more[name]
344
+ return found
345
+ }