@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
package/list/list.js
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lists: a view-model list property drawn as one instance of a row artboard
|
|
3
|
+
* for each item, as Rive's ArtboardComponentList does.
|
|
4
|
+
*
|
|
5
|
+
* A bundle's `lists` names each list: its view-model path, the row artboards
|
|
6
|
+
* it draws (a Lottie each, with the row view model's slots), the flex of the
|
|
7
|
+
* container the rows sit in, and the container's box in each layout band.
|
|
8
|
+
* This player compiles the row's Lottie once for each item, so every row
|
|
9
|
+
* has its own slots, filled from the item's instance through the bind's
|
|
10
|
+
* converters, and lays the rows out with `list.flex.js`. A row with a state
|
|
11
|
+
* machine runs it for each item, with the runner nested artboards use.
|
|
12
|
+
*
|
|
13
|
+
* The rows are drawn over the animation, after it. A host changes them with
|
|
14
|
+
* `setInputs({ rows: [ ... ] })`: a row whose item has not changed stays as
|
|
15
|
+
* it is, with its machine's state.
|
|
16
|
+
*
|
|
17
|
+
* Not done: pointer listeners and text inside a row, and the list's own
|
|
18
|
+
* transform and opacity.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createAnimation } from '../lottie/lottie.js'
|
|
22
|
+
import { multiply } from '../lottie/lottie.paths.js'
|
|
23
|
+
import { createNested, extend } from '../motion/motion.nested.js'
|
|
24
|
+
import { slotValues } from '../motion/motion.values.js'
|
|
25
|
+
import { layoutRows } from './list.flex.js'
|
|
26
|
+
import { itemValues, rowNameOf, signature } from './list.items.js'
|
|
27
|
+
|
|
28
|
+
/** @import { Animation } from '../lottie/lottie.js' */
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The scope a row's machine runs its focus in: `row.<id>` for the row's own, `row.<id>/<sid>` for a nested artboard in it.
|
|
32
|
+
*
|
|
33
|
+
* @param {number} id
|
|
34
|
+
* @param {string} sid the machine's, `row` for the row's own
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function scopeOf(id, sid) {
|
|
38
|
+
return sid === 'row' ? 'row.' + id : 'row.' + id + '/' + sid
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {object} Row
|
|
43
|
+
* @property {string} sig which item it was made for
|
|
44
|
+
* @property {any} entry the list's description of its row artboard
|
|
45
|
+
* @property {Animation} anim its own compiled copy of the row's Lottie
|
|
46
|
+
* @property {ReturnType<typeof createNested> | null} runner its machine, when the row has one
|
|
47
|
+
* @property {number} id which row it is, for the pointer's names
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {object} Options
|
|
52
|
+
* @property {string} base where the row Lotties' images resolve from
|
|
53
|
+
* @property {() => Record<string, string>} [families] the bundle's fonts by name, as registered, which a text in a row is drawn in
|
|
54
|
+
* @property {() => number} band the width of the layout band showing, 0 for the file's own size
|
|
55
|
+
* @property {() => number} [blended] between two bands that blend, the canvas's width, else 0: the box is then the mix of the two bands'
|
|
56
|
+
* @property {() => (import('../bands/bands.js').Cell | null)} [cell] where the canvas falls in the layout grid, while the fit is layout: the box is then the (bilinear) mix of the cell's corners'; it replaces `band` and `blended`
|
|
57
|
+
* @property {(message: string) => void} error
|
|
58
|
+
* @property {(action: any, scope: string) => void} focus a row's machine ran a focus action: FocusTarget with the machine's own rid, FocusMove or FocusClear; `scope` is the machine (`row.<id>`, or `row.<id>/<sid>` for a nested artboard in the row)
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {any[] | undefined} entries a bundle's `lists`
|
|
63
|
+
* @param {Record<string, any>} lotties the rows' Lotties, by file name
|
|
64
|
+
* @param {Options} options
|
|
65
|
+
*/
|
|
66
|
+
export function createLists(entries, lotties, options) {
|
|
67
|
+
/** @type {{ entry: any, rows: Row[] }[]} */
|
|
68
|
+
const lists = []
|
|
69
|
+
for (let n = 0; n < (entries || []).length; n++) {
|
|
70
|
+
/** @type {Row[]} */
|
|
71
|
+
const rows = []
|
|
72
|
+
lists.push({ entry: entries ? entries[n] : null, rows })
|
|
73
|
+
}
|
|
74
|
+
let hasMachines = false
|
|
75
|
+
let nextRow = 1
|
|
76
|
+
/** whether the rows changed since the pointer last looked: they hold its targets */
|
|
77
|
+
let moved = false
|
|
78
|
+
/** how many times the rows changed: the focus tree has their nodes */
|
|
79
|
+
let version = 0
|
|
80
|
+
/** @type {Row[]} rows made since the focus tree last took them in */
|
|
81
|
+
let fresh = []
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {any} list
|
|
85
|
+
* @param {any} item
|
|
86
|
+
* @returns {Row | null}
|
|
87
|
+
*/
|
|
88
|
+
function makeRow(list, item) {
|
|
89
|
+
const name = rowNameOf(item, list.entry.rows)
|
|
90
|
+
if (name === null) {
|
|
91
|
+
options.error("list '" + list.entry.path + "' has no row for " + signature(item))
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
const entry = list.entry.rows[name]
|
|
95
|
+
const lottie = lotties[entry.lottie]
|
|
96
|
+
if (!lottie) return null
|
|
97
|
+
|
|
98
|
+
const anim = createAnimation(lottie, { base: options.base, families: options.families })
|
|
99
|
+
const values = itemValues(item, entry.data)
|
|
100
|
+
const table = slotValues(entry.data, values, options.error)
|
|
101
|
+
for (const sid in table) anim.slots[sid] = table[sid]
|
|
102
|
+
|
|
103
|
+
// the row's own machine, and the machines of the nested artboards in it: each runs on the row's clock
|
|
104
|
+
const machines = entry.machine ? [ { sid: 'row', fps: entry.machine.fps, machine: entry.machine.machine, animations: entry.machine.animations, inputs: {}, remoteKeys: entry.remoteKeys } ] : []
|
|
105
|
+
for (let n = 0; n < (entry.nested || []).length; n++) machines.push(entry.nested[n])
|
|
106
|
+
|
|
107
|
+
const id = nextRow++
|
|
108
|
+
/** @type {Row['runner']} */
|
|
109
|
+
let runner = null
|
|
110
|
+
if (machines.length) {
|
|
111
|
+
runner = createNested(machines, {
|
|
112
|
+
focus: (action, sid) => options.focus(action, scopeOf(id, sid)),
|
|
113
|
+
// the row's own machine sets properties of the row's view model (a listener, a state's action): the slots follow
|
|
114
|
+
values: (sid, changed) => {
|
|
115
|
+
if (sid !== 'row') return
|
|
116
|
+
const table = slotValues(entry.data, extend(values, changed), options.error)
|
|
117
|
+
for (const slot in table) anim.slots[slot] = table[slot]
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
runner.takeInputs(values)
|
|
121
|
+
hasMachines = true
|
|
122
|
+
}
|
|
123
|
+
const row = { sig: signature(item), entry, anim, runner, id }
|
|
124
|
+
// the shapes its pointer listeners watch, and its focus nodes, are recorded as it draws
|
|
125
|
+
const watched = runner ? Object.keys(runner.pointerTargets()) : []
|
|
126
|
+
const nodes = entry.focus ? entry.focus.nodes : []
|
|
127
|
+
for (let n = 0; n < nodes.length; n++) if (nodes[n].rid !== '' && watched.indexOf(nodes[n].rid) < 0) watched.push(nodes[n].rid)
|
|
128
|
+
if (watched.length) anim.hits.watch(watched)
|
|
129
|
+
fresh.push(row)
|
|
130
|
+
return row
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {string} name `<row>|<target>`
|
|
135
|
+
* @returns {{ row: Row, target: string } | null}
|
|
136
|
+
*/
|
|
137
|
+
function rowOf(name) {
|
|
138
|
+
const at = name.indexOf('|')
|
|
139
|
+
if (at < 0) return null
|
|
140
|
+
const id = Number(name.slice(0, at))
|
|
141
|
+
for (let n = 0; n < lists.length; n++) {
|
|
142
|
+
for (let k = 0; k < lists[n].rows.length; k++) if (lists[n].rows[k].id === id) return { row: lists[n].rows[k], target: name.slice(at + 1) }
|
|
143
|
+
}
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} scope `row.<id>` or `row.<id>/<sid>`
|
|
149
|
+
* @returns {{ row: Row & { runner: NonNullable<Row['runner']> }, sid: string } | null}
|
|
150
|
+
*/
|
|
151
|
+
function machineOf(scope) {
|
|
152
|
+
const at = scope.indexOf('/')
|
|
153
|
+
const id = Number((at < 0 ? scope : scope.slice(0, at)).slice(4))
|
|
154
|
+
for (let n = 0; n < lists.length; n++) {
|
|
155
|
+
for (let k = 0; k < lists[n].rows.length; k++) {
|
|
156
|
+
const row = lists[n].rows[k]
|
|
157
|
+
if (row.id === id && row.runner) return { row: /** @type {any} */ (row), sid: at < 0 ? 'row' : scope.slice(at + 1) }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
/** How many lists the bundle has */
|
|
165
|
+
count: lists.length,
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Brings every list's rows up to date with the view-model values. A row
|
|
169
|
+
* whose item is unchanged is kept.
|
|
170
|
+
*
|
|
171
|
+
* @param {Record<string, any>} values
|
|
172
|
+
*/
|
|
173
|
+
update(values) {
|
|
174
|
+
for (let n = 0; n < lists.length; n++) {
|
|
175
|
+
const list = lists[n]
|
|
176
|
+
const items = values[list.entry.path]
|
|
177
|
+
if (!Array.isArray(items)) continue
|
|
178
|
+
|
|
179
|
+
/** @type {Row[]} */
|
|
180
|
+
const next = []
|
|
181
|
+
for (let k = 0; k < items.length; k++) {
|
|
182
|
+
const kept = list.rows[k] && list.rows[k].sig === signature(items[k]) ? list.rows[k] : null
|
|
183
|
+
const row = kept || makeRow(list, items[k])
|
|
184
|
+
if (row) next.push(row)
|
|
185
|
+
}
|
|
186
|
+
if (next.length !== list.rows.length || next.some((row, k) => row !== list.rows[k])) {
|
|
187
|
+
moved = true
|
|
188
|
+
version++
|
|
189
|
+
}
|
|
190
|
+
list.rows = next
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Where the pointer listeners of the rows' machines are, each row's under
|
|
196
|
+
* its own name: `<row>|<target>`, a target as the row's machine names it.
|
|
197
|
+
*
|
|
198
|
+
* @returns {Record<string, string[]>}
|
|
199
|
+
*/
|
|
200
|
+
pointerTargets() {
|
|
201
|
+
moved = false
|
|
202
|
+
/** @type {Record<string, string[]>} */
|
|
203
|
+
const found = {}
|
|
204
|
+
for (let n = 0; n < lists.length; n++) {
|
|
205
|
+
for (let k = 0; k < lists[n].rows.length; k++) {
|
|
206
|
+
const row = lists[n].rows[k]
|
|
207
|
+
if (!row.runner) continue
|
|
208
|
+
const targets = row.runner.pointerTargets()
|
|
209
|
+
for (const name in targets) found[row.id + '|' + name] = targets[name]
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return found
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/** Changes whenever rows come or go, which is when the focus tree needs their nodes again */
|
|
216
|
+
get version() {
|
|
217
|
+
return version
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A list's rows' focus nodes, named by the host's tree: `row.<id>/<rid>`
|
|
222
|
+
* (and `row.<id>/<sid>/<rid>` for a nested artboard in the row). Their
|
|
223
|
+
* topmost node is inside `marker`, the node that holds the list's place.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} listId the list's id (`list.1`)
|
|
226
|
+
* @param {string} marker the list's node in the host's tree
|
|
227
|
+
* @returns {any[]}
|
|
228
|
+
*/
|
|
229
|
+
nodes(listId, marker) {
|
|
230
|
+
const found = []
|
|
231
|
+
for (let n = 0; n < lists.length; n++) {
|
|
232
|
+
if (lists[n].entry.id !== listId) continue
|
|
233
|
+
for (let k = 0; k < lists[n].rows.length; k++) {
|
|
234
|
+
const row = lists[n].rows[k]
|
|
235
|
+
const own = row.entry.focus ? row.entry.focus.nodes : []
|
|
236
|
+
const prefix = 'row.' + row.id + '/'
|
|
237
|
+
for (let j = 0; j < own.length; j++) {
|
|
238
|
+
const node = extend(own[j], { rid: prefix + own[j].rid })
|
|
239
|
+
if (own[j].rid === '') {
|
|
240
|
+
// the row's own artboard is a node of the tree, inside the list's
|
|
241
|
+
node.parent = marker
|
|
242
|
+
node.kind = 'group'
|
|
243
|
+
} else {
|
|
244
|
+
node.parent = own[j].parent === '' ? (own.some((/** @type {any} */ one) => one.rid === '') ? prefix : marker) : prefix + own[j].parent
|
|
245
|
+
}
|
|
246
|
+
if (node.name) node.name = 'row ' + row.id + '/' + node.name
|
|
247
|
+
found.push(node)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return found
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The key bindings of the rows' focus nodes, named as `nodes` names them,
|
|
256
|
+
* with the machine each runs in as `scope`.
|
|
257
|
+
*
|
|
258
|
+
* @returns {any[]}
|
|
259
|
+
*/
|
|
260
|
+
bindings() {
|
|
261
|
+
const found = []
|
|
262
|
+
for (let n = 0; n < lists.length; n++) {
|
|
263
|
+
for (let k = 0; k < lists[n].rows.length; k++) {
|
|
264
|
+
const row = lists[n].rows[k]
|
|
265
|
+
const prefix = 'row.' + row.id + '/'
|
|
266
|
+
const own = row.entry.remoteKeys || []
|
|
267
|
+
for (let j = 0; j < own.length; j++) found.push(extend(own[j], { target: prefix + own[j].target, scope: 'row.' + row.id }))
|
|
268
|
+
const inside = row.entry.nested || []
|
|
269
|
+
for (let j = 0; j < inside.length; j++) {
|
|
270
|
+
const keys = inside[j].remoteKeys || []
|
|
271
|
+
for (let i = 0; i < keys.length; i++) found.push(extend(keys[i], { target: prefix + inside[j].sid + '/' + keys[i].target, scope: 'row.' + row.id + '/' + inside[j].sid }))
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return found
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Focus actions the rows' machines ran as they started take effect: the
|
|
280
|
+
* focus tree has their nodes now.
|
|
281
|
+
*/
|
|
282
|
+
flushFocus() {
|
|
283
|
+
const made = fresh
|
|
284
|
+
fresh = []
|
|
285
|
+
for (let n = 0; n < made.length; n++) {
|
|
286
|
+
const runner = made[n].runner
|
|
287
|
+
if (runner) runner.flushFocus()
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* A node of a row gained or lost focus.
|
|
293
|
+
*
|
|
294
|
+
* @param {'Focus' | 'Blur'} type
|
|
295
|
+
* @param {string} scope `row.<id>` or `row.<id>/<sid>`
|
|
296
|
+
* @param {string} rid the node in its machine's artboard
|
|
297
|
+
*/
|
|
298
|
+
focusEvent(type, scope, rid) {
|
|
299
|
+
const found = machineOf(scope)
|
|
300
|
+
if (found && found.row.runner) found.row.runner.focusEvent(type, found.sid, rid)
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Runs a key binding's actions in the machine of a row it belongs to.
|
|
305
|
+
*
|
|
306
|
+
* @param {string} scope `row.<id>` or `row.<id>/<sid>`
|
|
307
|
+
* @param {any[]} actions
|
|
308
|
+
*/
|
|
309
|
+
act(scope, actions) {
|
|
310
|
+
const found = machineOf(scope)
|
|
311
|
+
if (found) found.row.runner.act(found.sid, actions)
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Where a row's node was drawn, for the arrows.
|
|
316
|
+
*
|
|
317
|
+
* @param {string} rid `row.<id>/<rid>`
|
|
318
|
+
* @returns {{ x: number, y: number } | null}
|
|
319
|
+
*/
|
|
320
|
+
center(rid) {
|
|
321
|
+
const at = rid.indexOf('/')
|
|
322
|
+
const found = at < 0 ? null : rowOf(rid.slice(4, at) + '|' + rid.slice(at + 1))
|
|
323
|
+
return found ? found.row.anim.hits.center(found.target) : null
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
/** Whether rows came or went since the pointer last asked for its targets */
|
|
327
|
+
get isPointerStale() {
|
|
328
|
+
return moved
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Whether a canvas pixel is on a target of a row, in the last frame drawn.
|
|
333
|
+
*
|
|
334
|
+
* @param {string} name `<row>|<target>`
|
|
335
|
+
* @param {number} x
|
|
336
|
+
* @param {number} y
|
|
337
|
+
* @returns {boolean}
|
|
338
|
+
*/
|
|
339
|
+
hit(name, x, y) {
|
|
340
|
+
const found = rowOf(name)
|
|
341
|
+
return !!found && found.row.anim.hits.test(found.target, x, y)
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* A pointer event on a target of a row's machine.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} type
|
|
348
|
+
* @param {string} name `<row>|<target>`
|
|
349
|
+
* @returns {boolean} whether a row owns the target
|
|
350
|
+
*/
|
|
351
|
+
pointer(type, name) {
|
|
352
|
+
const found = rowOf(name)
|
|
353
|
+
return !!found && !!found.row.runner && found.row.runner.pointer(type, found.target)
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Moves the rows' machines on.
|
|
358
|
+
*
|
|
359
|
+
* @param {number} seconds
|
|
360
|
+
*/
|
|
361
|
+
tick(seconds) {
|
|
362
|
+
for (let n = 0; n < lists.length; n++) {
|
|
363
|
+
for (let k = 0; k < lists[n].rows.length; k++) {
|
|
364
|
+
const runner = lists[n].rows[k].runner
|
|
365
|
+
if (runner) runner.tick(seconds)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
/** Whether any row runs a machine, which keeps the clock going */
|
|
371
|
+
get isAnimated() {
|
|
372
|
+
return hasMachines
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* What each row of a list has now: the slot values its item gave it, and
|
|
377
|
+
* the frame of its Lottie it draws.
|
|
378
|
+
*
|
|
379
|
+
* @param {string} path the list's view-model path
|
|
380
|
+
* @returns {{ slots: Record<string, any>, frame: number }[]}
|
|
381
|
+
*/
|
|
382
|
+
inspect(path) {
|
|
383
|
+
const found = []
|
|
384
|
+
for (let n = 0; n < lists.length; n++) {
|
|
385
|
+
if (lists[n].entry.path !== path) continue
|
|
386
|
+
for (let k = 0; k < lists[n].rows.length; k++) found.push({ slots: lists[n].rows[k].anim.slots, frame: rowFrame(lists[n].rows[k]) })
|
|
387
|
+
}
|
|
388
|
+
return found
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Where each list's rows are, in the artboard of the layout band showing.
|
|
393
|
+
*
|
|
394
|
+
* @returns {{ path: string, rows: { x: number, y: number, width: number, height: number }[] }[]}
|
|
395
|
+
*/
|
|
396
|
+
layout() {
|
|
397
|
+
/** @type {{ path: string, rows: { x: number, y: number, width: number, height: number }[] }[]} */
|
|
398
|
+
const found = []
|
|
399
|
+
for (let n = 0; n < lists.length; n++) {
|
|
400
|
+
const placed = place(lists[n], options.band(), options.blended ? options.blended() : 0, options.cell ? options.cell() : null)
|
|
401
|
+
const rows = []
|
|
402
|
+
for (let k = 0; k < lists[n].rows.length; k++) rows.push({ x: placed.left + placed.x[k], y: placed.top + placed.y[k], width: lists[n].rows[k].entry.width, height: lists[n].rows[k].entry.height })
|
|
403
|
+
found.push({ path: lists[n].entry.path, rows })
|
|
404
|
+
}
|
|
405
|
+
return found
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Draws every row.
|
|
410
|
+
*
|
|
411
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
412
|
+
* @param {import('../lottie/lottie.paths.js').Matrix} matrix the artboard's space onto the canvas
|
|
413
|
+
*/
|
|
414
|
+
draw(ctx, matrix) {
|
|
415
|
+
for (let n = 0; n < lists.length; n++) {
|
|
416
|
+
const list = lists[n]
|
|
417
|
+
if (!list.rows.length) continue
|
|
418
|
+
const placed = place(list, options.band(), options.blended ? options.blended() : 0, options.cell ? options.cell() : null)
|
|
419
|
+
for (let k = 0; k < list.rows.length; k++) {
|
|
420
|
+
const row = list.rows[k]
|
|
421
|
+
const at = multiply(matrix, [ 1, 0, 0, 1, placed.left + placed.x[k], placed.top + placed.y[k] ])
|
|
422
|
+
ctx.save()
|
|
423
|
+
// a row is cut off at its own edge, as an artboard is
|
|
424
|
+
ctx.setTransform(at[0], at[1], at[2], at[3], at[4], at[5])
|
|
425
|
+
ctx.beginPath()
|
|
426
|
+
ctx.rect(0, 0, row.entry.width, row.entry.height)
|
|
427
|
+
ctx.clip()
|
|
428
|
+
row.anim.draw(ctx, rowFrame(row), at)
|
|
429
|
+
ctx.restore()
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The frame of a row's Lottie to draw: the machine's, else the first. The
|
|
438
|
+
* machines of the nested artboards in the row set where their layers' time
|
|
439
|
+
* remaps are in the row's slots.
|
|
440
|
+
*
|
|
441
|
+
* @param {Row} row
|
|
442
|
+
* @returns {number}
|
|
443
|
+
*/
|
|
444
|
+
function rowFrame(row) {
|
|
445
|
+
if (!row.runner) return row.anim.inPoint
|
|
446
|
+
row.runner.apply(row.anim.slots, row.anim.fps)
|
|
447
|
+
const time = row.anim.slots.row
|
|
448
|
+
if (time === undefined) return row.anim.inPoint
|
|
449
|
+
// a Lottie draws up to its out point, not at it: a finished machine holds the last frame it has
|
|
450
|
+
return Math.min(row.anim.inPoint + time * row.anim.fps, Math.max(row.anim.outPoint - 0.001, row.anim.inPoint))
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The container's box at a band of the layout grid.
|
|
455
|
+
*
|
|
456
|
+
* @param {any} entry the list's description
|
|
457
|
+
* @param {{ width: number, height: number }} corner the band
|
|
458
|
+
* @returns {{ width: number, x: number, y: number, w: number, h: number } | null}
|
|
459
|
+
*/
|
|
460
|
+
function boxAt(entry, corner) {
|
|
461
|
+
const all = entry.gridBoxes ? entry.boxes.concat(entry.gridBoxes) : entry.boxes
|
|
462
|
+
let found = null
|
|
463
|
+
// a box says its height unless the file is older than the grid
|
|
464
|
+
for (let n = 0; n < all.length; n++) if (all[n].width === corner.width && (all[n].height === undefined || all[n].height === corner.height)) found = all[n]
|
|
465
|
+
return found
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The container's box in the band showing, and where its rows sit in it.
|
|
470
|
+
*
|
|
471
|
+
* @param {{ entry: any, rows: Row[] }} list
|
|
472
|
+
* @param {number} band the layout band's width, 0 for the file's own
|
|
473
|
+
* @param {number} blended the canvas's width between two blending bands, else 0
|
|
474
|
+
* @param {import('../bands/bands.js').Cell | null} [cell] where the canvas falls in the layout grid
|
|
475
|
+
* @returns {{ left: number, top: number, x: number[], y: number[] }}
|
|
476
|
+
*/
|
|
477
|
+
function place(list, band, blended, cell) {
|
|
478
|
+
const boxes = list.entry.boxes
|
|
479
|
+
let box = boxes[0]
|
|
480
|
+
let at = 0
|
|
481
|
+
for (let n = 0; n < boxes.length; n++) {
|
|
482
|
+
const isOwn = band === 0 ? boxes[n].authored : boxes[n].width === band
|
|
483
|
+
if (isOwn) {
|
|
484
|
+
box = boxes[n]
|
|
485
|
+
at = n
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const after = boxes[at + 1]
|
|
489
|
+
const corner = cell ? boxAt(list.entry, cell.corner) : null
|
|
490
|
+
if (cell && corner) {
|
|
491
|
+
// the container's box is bilinear in the size inside a cell that blends: a straight line along one side of it
|
|
492
|
+
const right = (cell.right && boxAt(list.entry, cell.right)) || corner
|
|
493
|
+
const up = (cell.up && boxAt(list.entry, cell.up)) || corner
|
|
494
|
+
const diag = (cell.diag && boxAt(list.entry, cell.diag)) || (cell.right ? right : up)
|
|
495
|
+
const mix = (/** @type {'x' | 'y' | 'w' | 'h'} */ key) => (corner[key] * (1 - cell.u) + right[key] * cell.u) * (1 - cell.v) + (up[key] * (1 - cell.u) + diag[key] * cell.u) * cell.v
|
|
496
|
+
box = { width: corner.width, authored: false, x: mix('x'), y: mix('y'), w: mix('w'), h: mix('h') }
|
|
497
|
+
} else if (blended > box.width && after && after.width > box.width) {
|
|
498
|
+
// the container's box is a straight line in the width between bands that blend
|
|
499
|
+
const t = Math.min(1, (blended - box.width) / (after.width - box.width))
|
|
500
|
+
const mix = (/** @type {number} */ a, /** @type {number} */ b) => a + (b - a) * t
|
|
501
|
+
box = { width: blended, authored: false, x: mix(box.x, after.x), y: mix(box.y, after.y), w: mix(box.w, after.w), h: mix(box.h, after.h) }
|
|
502
|
+
}
|
|
503
|
+
const sizes = []
|
|
504
|
+
for (let n = 0; n < list.rows.length; n++) sizes.push({ w: list.rows[n].entry.width, h: list.rows[n].entry.height })
|
|
505
|
+
const laid = layoutRows(list.entry.flex, box, sizes)
|
|
506
|
+
return { left: box.x, top: box.y, x: laid.x, y: laid.y }
|
|
507
|
+
}
|
package/lottie/lottie.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Lottie animation drawn on a 2D canvas, for the subset Motion's bundles
|
|
3
|
+
* use: shape layers (groups, paths, rectangles, ellipses, stars, fills,
|
|
4
|
+
* strokes, gradients, dashes, trims, merges), precomps, solids, images,
|
|
5
|
+
* masks and mattes. Compiled once; each frame is drawn from scratch.
|
|
6
|
+
*
|
|
7
|
+
* `slots` is live: a slot id set there overrides the property carrying it,
|
|
8
|
+
* which is how live data reaches the animation without recompiling it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHits } from '../hit/hit.js'
|
|
12
|
+
import { compileLayers, drawLayers } from './lottie.layers.js'
|
|
13
|
+
import { evaluator } from './lottie.props.js'
|
|
14
|
+
|
|
15
|
+
/** @import { Matrix } from './lottie.paths.js' */
|
|
16
|
+
/** @import { Slots } from './lottie.props.js' */
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} Animation
|
|
20
|
+
* @property {number} width
|
|
21
|
+
* @property {number} height
|
|
22
|
+
* @property {number} fps
|
|
23
|
+
* @property {number} inPoint first frame
|
|
24
|
+
* @property {number} outPoint the frame it ends at, exclusive
|
|
25
|
+
* @property {Slots} slots slot id → live value, read every frame
|
|
26
|
+
* @property {import('../hit/hit.js').Hits} hits where the last frame drew the shapes pointer listeners target
|
|
27
|
+
* @property {Promise<void>} ready resolves once its images have loaded
|
|
28
|
+
* @property {(sid: string, frame: number) => any} probe the value the property carrying a slot id has at a frame, as drawn (a live slot value included); undefined when there is none
|
|
29
|
+
* @property {(ctx: CanvasRenderingContext2D, frame: number, matrix: Matrix) => void} draw
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {any} lottie parsed Lottie JSON
|
|
34
|
+
* @param {object} [options]
|
|
35
|
+
* @param {string} [options.base] where image paths that aren't data URIs resolve from
|
|
36
|
+
* @param {() => Record<string, string>} [options.families] the bundle's fonts by name, as registered, which a text a precomp holds is drawn in
|
|
37
|
+
* @returns {Animation}
|
|
38
|
+
*/
|
|
39
|
+
export function createAnimation(lottie, options) {
|
|
40
|
+
const base = (options && options.base) || ''
|
|
41
|
+
/** @type {Slots} */
|
|
42
|
+
const slots = {}
|
|
43
|
+
|
|
44
|
+
/** @type {Record<string, any>} */
|
|
45
|
+
const assets = {}
|
|
46
|
+
for (let n = 0; n < (lottie.assets || []).length; n++) assets[lottie.assets[n].id] = lottie.assets[n]
|
|
47
|
+
|
|
48
|
+
const loading = loadImages(lottie.assets || [], base)
|
|
49
|
+
const hits = createHits()
|
|
50
|
+
const context = { assets, images: loading.images, fps: lottie.fr || 30, slots, hits, families: (options && options.families) || null }
|
|
51
|
+
const layers = compileLayers(lottie.layers, context)
|
|
52
|
+
/** @type {Record<string, import('./lottie.props.js').Evaluator> | null} */
|
|
53
|
+
let probes = null
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
width: lottie.w || 0,
|
|
57
|
+
height: lottie.h || 0,
|
|
58
|
+
fps: lottie.fr || 30,
|
|
59
|
+
inPoint: lottie.ip || 0,
|
|
60
|
+
outPoint: lottie.op || 0,
|
|
61
|
+
slots,
|
|
62
|
+
hits,
|
|
63
|
+
ready: loading.ready,
|
|
64
|
+
probe(sid, frame) {
|
|
65
|
+
if (!probes) probes = collectProbes(lottie.layers, slots)
|
|
66
|
+
return probes[sid] ? probes[sid](frame) : undefined
|
|
67
|
+
},
|
|
68
|
+
draw(ctx, frame, matrix) {
|
|
69
|
+
hits.begin()
|
|
70
|
+
drawLayers(ctx, layers, context, frame, matrix, 1)
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* An evaluator for each slotted property in a layer list, by slot id; the
|
|
77
|
+
* first property to carry an id answers for it. Properties inside precomps
|
|
78
|
+
* run on their own clocks and are not probed.
|
|
79
|
+
*
|
|
80
|
+
* @param {any[]} layers
|
|
81
|
+
* @param {Slots} slots
|
|
82
|
+
* @returns {Record<string, import('./lottie.props.js').Evaluator>}
|
|
83
|
+
*/
|
|
84
|
+
function collectProbes(layers, slots) {
|
|
85
|
+
/** @type {Record<string, import('./lottie.props.js').Evaluator>} */
|
|
86
|
+
const found = {}
|
|
87
|
+
const visit = (/** @type {any} */ value) => {
|
|
88
|
+
if (!value || typeof value !== 'object') return
|
|
89
|
+
if (typeof value.sid === 'string' && value.k !== undefined && !found[value.sid]) found[value.sid] = evaluator(value, slots, undefined)
|
|
90
|
+
for (const key in value) visit(value[key])
|
|
91
|
+
}
|
|
92
|
+
visit(layers)
|
|
93
|
+
return found
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The image assets, loading. lottie-roku embeds every image as a data URI,
|
|
98
|
+
* so a bundle's load without a request.
|
|
99
|
+
*
|
|
100
|
+
* @param {any[]} assets
|
|
101
|
+
* @param {string} base
|
|
102
|
+
* @returns {{ images: Record<string, HTMLImageElement>, ready: Promise<void> }}
|
|
103
|
+
*/
|
|
104
|
+
function loadImages(assets, base) {
|
|
105
|
+
/** @type {Record<string, HTMLImageElement>} */
|
|
106
|
+
const images = {}
|
|
107
|
+
const pending = []
|
|
108
|
+
for (let n = 0; n < assets.length; n++) {
|
|
109
|
+
const asset = assets[n]
|
|
110
|
+
const isImage = !asset.layers && typeof asset.p === 'string'
|
|
111
|
+
if (!isImage) continue
|
|
112
|
+
|
|
113
|
+
const image = new Image()
|
|
114
|
+
const source = asset.e === 1 || asset.p.indexOf('data:') === 0 ? asset.p : base + (asset.u || '') + asset.p
|
|
115
|
+
pending.push(new Promise((resolve) => {
|
|
116
|
+
// a missing image draws nothing, as it does in the reference players
|
|
117
|
+
image.onload = () => resolve(undefined)
|
|
118
|
+
image.onerror = () => resolve(undefined)
|
|
119
|
+
}))
|
|
120
|
+
image.src = source
|
|
121
|
+
images[asset.id] = image
|
|
122
|
+
}
|
|
123
|
+
return { images, ready: Promise.all(pending).then(() => undefined) }
|
|
124
|
+
}
|