@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.
- package/LICENSE +201 -0
- package/audio/audio.cues.js +27 -0
- package/audio/audio.js +259 -0
- package/bands/bands.js +390 -0
- package/bundle/bundle.fonts.js +55 -0
- package/bundle/bundle.js +75 -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 +507 -0
- package/lottie/lottie.js +124 -0
- package/lottie/lottie.layers.js +363 -0
- package/lottie/lottie.paint.js +208 -0
- package/lottie/lottie.paths.js +459 -0
- package/lottie/lottie.props.js +255 -0
- package/lottie/lottie.shapes.js +258 -0
- package/mixer/mixer.js +284 -0
- package/mixer/mixer.mix.js +90 -0
- package/mixer/mixer.stage.js +431 -0
- package/motion/motion.convert.js +373 -0
- package/motion/motion.flow.js +403 -0
- package/motion/motion.keys.js +150 -0
- package/motion/motion.machine.js +367 -0
- package/motion/motion.nested.js +345 -0
- package/motion/motion.values.js +364 -0
- package/motion.js +979 -0
- package/package.json +53 -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 +401 -0
- package/player/player.raster.js +127 -0
- package/pointer/pointer.js +292 -0
- package/readme.md +270 -0
- package/README.md +0 -3
|
@@ -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
|
+
}
|