@px-petals/roku.motion-web 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +201 -0
  2. package/audio/audio.cues.js +27 -0
  3. package/audio/audio.js +259 -0
  4. package/bands/bands.js +392 -0
  5. package/bundle/bundle.fonts.js +55 -0
  6. package/bundle/bundle.js +131 -0
  7. package/focus/focus.direction.js +46 -0
  8. package/focus/focus.dom.js +158 -0
  9. package/focus/focus.js +277 -0
  10. package/focus/focus.tree.js +146 -0
  11. package/hit/hit.geometry.js +113 -0
  12. package/hit/hit.js +129 -0
  13. package/list/list.flex.js +181 -0
  14. package/list/list.items.js +80 -0
  15. package/list/list.js +518 -0
  16. package/lottie/lottie.js +124 -0
  17. package/lottie/lottie.layers.js +416 -0
  18. package/lottie/lottie.paint.js +238 -0
  19. package/lottie/lottie.paths.js +474 -0
  20. package/lottie/lottie.props.js +265 -0
  21. package/lottie/lottie.shapes.js +259 -0
  22. package/mixer/mixer.js +307 -0
  23. package/mixer/mixer.mix.js +107 -0
  24. package/mixer/mixer.stage.js +431 -0
  25. package/motion/motion.convert.js +404 -0
  26. package/motion/motion.flow.js +421 -0
  27. package/motion/motion.keys.js +150 -0
  28. package/motion/motion.machine.js +382 -0
  29. package/motion/motion.nested.js +345 -0
  30. package/motion/motion.values.js +368 -0
  31. package/motion.js +1155 -0
  32. package/package.json +47 -4
  33. package/player/overlay/overlay.js +325 -0
  34. package/player/overlay/overlay.layout.js +208 -0
  35. package/player/player.fit.js +101 -0
  36. package/player/player.js +421 -0
  37. package/player/player.raster.js +127 -0
  38. package/pointer/pointer.js +292 -0
  39. package/readme.md +345 -0
  40. package/script/luau/lib/base/base.js +202 -0
  41. package/script/luau/lib/base/base.table.js +189 -0
  42. package/script/luau/lib/lib.args.js +152 -0
  43. package/script/luau/lib/lib.js +23 -0
  44. package/script/luau/lib/math/math.bit32.js +144 -0
  45. package/script/luau/lib/math/math.js +288 -0
  46. package/script/luau/lib/math/math.vector.js +125 -0
  47. package/script/luau/lib/string/string.format.js +143 -0
  48. package/script/luau/lib/string/string.js +160 -0
  49. package/script/luau/lib/string/string.pattern.js +505 -0
  50. package/script/luau/load/load.js +362 -0
  51. package/script/luau/load/load.ops.js +79 -0
  52. package/script/luau/luau.convert.js +259 -0
  53. package/script/luau/luau.js +132 -0
  54. package/script/luau/luau.md +111 -0
  55. package/script/luau/vm/vm.js +167 -0
  56. package/script/luau/vm/vm.meta.js +401 -0
  57. package/script/luau/vm/vm.num.js +236 -0
  58. package/script/luau/vm/vm.run.js +842 -0
  59. package/script/luau/vm/vm.value.js +406 -0
  60. package/script/play/play.js +199 -0
  61. package/script/play/play.objects.js +295 -0
  62. package/script/play/play.scope.js +91 -0
  63. package/script/rive/api/api.statics.js +31 -0
  64. package/script/rive/api/api.type.js +188 -0
  65. package/script/rive/api/artboard/artboard.js +89 -0
  66. package/script/rive/api/assets/assets.js +72 -0
  67. package/script/rive/api/data/data.value.js +99 -0
  68. package/script/rive/api/draw/draw.contours.js +163 -0
  69. package/script/rive/api/draw/draw.measure.js +196 -0
  70. package/script/rive/api/draw/draw.paint.js +235 -0
  71. package/script/rive/api/draw/draw.path.js +153 -0
  72. package/script/rive/api/draw/draw.renderer.js +117 -0
  73. package/script/rive/api/math/math.color.js +96 -0
  74. package/script/rive/api/math/math.mat2d.js +180 -0
  75. package/script/rive/api/math/math.vector.js +121 -0
  76. package/script/rive/api/media/media.audio.js +113 -0
  77. package/script/rive/api/media/media.draw.js +73 -0
  78. package/script/rive/api/media/media.image.js +61 -0
  79. package/script/rive/api/media/media.mesh.js +50 -0
  80. package/script/rive/api/pointer/pointer.event.js +64 -0
  81. package/script/rive/api/pointer/pointer.route.js +69 -0
  82. package/script/rive/api/text/text.font.js +129 -0
  83. package/script/rive/api/text/text.js +278 -0
  84. package/script/rive/api/text/text.layout.js +259 -0
  85. package/script/rive/context/context.js +76 -0
  86. package/script/rive/context/context.memory.js +196 -0
  87. package/script/rive/context/context.viewmodel.js +302 -0
  88. package/script/rive/context/lists/lists.js +160 -0
  89. package/script/rive/context/lists/lists.values.js +281 -0
  90. package/script/rive/host/host.globals.js +44 -0
  91. package/script/rive/host/host.js +533 -0
  92. package/script/rive/host/host.node.js +45 -0
  93. package/script/rive/rive.js +23 -0
  94. package/script/rive/rive.md +180 -0
  95. package/script/script.js +11 -0
  96. package/skin/skin.deform.js +514 -0
  97. package/skin/skin.ik.js +260 -0
  98. package/skin/skin.js +297 -0
  99. package/README.md +0 -3
package/list/list.js ADDED
@@ -0,0 +1,518 @@
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, sourceValue } 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 {((list: any[]) => ReturnType<typeof import('../skin/skin.js').createSkins>) | null} [skins] makes the skins of a row's `data.skins`; null when the bundle has none, so the skin code is never loaded
58
+ * @property {(message: string) => void} error
59
+ * @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)
60
+ */
61
+
62
+ /**
63
+ * @param {any[] | undefined} entries a bundle's `lists`
64
+ * @param {Record<string, any>} lotties the rows' Lotties, by file name
65
+ * @param {Options} options
66
+ */
67
+ export function createLists(entries, lotties, options) {
68
+ /** @type {{ entry: any, rows: Row[] }[]} */
69
+ const lists = []
70
+ for (let n = 0; n < (entries || []).length; n++) {
71
+ /** @type {Row[]} */
72
+ const rows = []
73
+ lists.push({ entry: entries ? entries[n] : null, rows })
74
+ }
75
+ let hasMachines = false
76
+ let nextRow = 1
77
+ /** whether the rows changed since the pointer last looked: they hold its targets */
78
+ let moved = false
79
+ /** how many times the rows changed: the focus tree has their nodes */
80
+ let version = 0
81
+ /** @type {Row[]} rows made since the focus tree last took them in */
82
+ let fresh = []
83
+
84
+ /**
85
+ * @param {any} list
86
+ * @param {any} item
87
+ * @returns {Row | null}
88
+ */
89
+ function makeRow(list, item) {
90
+ const name = rowNameOf(item, list.entry.rows)
91
+ if (name === null) {
92
+ options.error("list '" + list.entry.path + "' has no row for " + signature(item))
93
+ return null
94
+ }
95
+ const entry = list.entry.rows[name]
96
+ const lottie = lotties[entry.lottie]
97
+ if (!lottie) return null
98
+
99
+ const anim = createAnimation(lottie, { base: options.base, families: options.families })
100
+ const values = itemValues(item, entry.data)
101
+ const table = slotValues(entry.data, values, options.error)
102
+ for (const sid in table) anim.slots[sid] = table[sid]
103
+ // the skinned paths the row's binds move, deformed for this item: its animation is its file's name
104
+ const rowSkins = options.skins && entry.data && entry.data.skins ? options.skins(entry.data.skins) : null
105
+ const animationId = String(entry.lottie).replace(/\.json$/, '')
106
+ const deformRow = (/** @type {Record<string, any>} */ current) => {
107
+ if (!rowSkins) return
108
+ const deformed = rowSkins.evaluate((source) => sourceValue(source, current, entry.data, options.error), animationId)
109
+ for (const sid in deformed) anim.slots[sid] = deformed[sid]
110
+ }
111
+ deformRow(values)
112
+
113
+ // the row's own machine, and the machines of the nested artboards in it: each runs on the row's clock
114
+ const machines = entry.machine ? [ { sid: 'row', fps: entry.machine.fps, machine: entry.machine.machine, animations: entry.machine.animations, inputs: {}, remoteKeys: entry.remoteKeys } ] : []
115
+ for (let n = 0; n < (entry.nested || []).length; n++) machines.push(entry.nested[n])
116
+
117
+ const id = nextRow++
118
+ /** @type {Row['runner']} */
119
+ let runner = null
120
+ if (machines.length) {
121
+ runner = createNested(machines, {
122
+ focus: (action, sid) => options.focus(action, scopeOf(id, sid)),
123
+ // the row's own machine sets properties of the row's view model (a listener, a state's action): the slots follow
124
+ values: (sid, changed) => {
125
+ if (sid !== 'row') return
126
+ const table = slotValues(entry.data, extend(values, changed), options.error)
127
+ for (const slot in table) anim.slots[slot] = table[slot]
128
+ deformRow(extend(values, changed))
129
+ },
130
+ })
131
+ runner.takeInputs(values)
132
+ hasMachines = true
133
+ }
134
+ const row = { sig: signature(item), entry, anim, runner, id }
135
+ // the shapes its pointer listeners watch, and its focus nodes, are recorded as it draws
136
+ const watched = runner ? Object.keys(runner.pointerTargets()) : []
137
+ const nodes = entry.focus ? entry.focus.nodes : []
138
+ for (let n = 0; n < nodes.length; n++) if (nodes[n].rid !== '' && watched.indexOf(nodes[n].rid) < 0) watched.push(nodes[n].rid)
139
+ if (watched.length) anim.hits.watch(watched)
140
+ fresh.push(row)
141
+ return row
142
+ }
143
+
144
+ /**
145
+ * @param {string} name `<row>|<target>`
146
+ * @returns {{ row: Row, target: string } | null}
147
+ */
148
+ function rowOf(name) {
149
+ const at = name.indexOf('|')
150
+ if (at < 0) return null
151
+ const id = Number(name.slice(0, at))
152
+ for (let n = 0; n < lists.length; n++) {
153
+ 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) }
154
+ }
155
+ return null
156
+ }
157
+
158
+ /**
159
+ * @param {string} scope `row.<id>` or `row.<id>/<sid>`
160
+ * @returns {{ row: Row & { runner: NonNullable<Row['runner']> }, sid: string } | null}
161
+ */
162
+ function machineOf(scope) {
163
+ const at = scope.indexOf('/')
164
+ const id = Number((at < 0 ? scope : scope.slice(0, at)).slice(4))
165
+ for (let n = 0; n < lists.length; n++) {
166
+ for (let k = 0; k < lists[n].rows.length; k++) {
167
+ const row = lists[n].rows[k]
168
+ if (row.id === id && row.runner) return { row: /** @type {any} */ (row), sid: at < 0 ? 'row' : scope.slice(at + 1) }
169
+ }
170
+ }
171
+ return null
172
+ }
173
+
174
+ return {
175
+ /** How many lists the bundle has */
176
+ count: lists.length,
177
+
178
+ /**
179
+ * Brings every list's rows up to date with the view-model values. A row
180
+ * whose item is unchanged is kept.
181
+ *
182
+ * @param {Record<string, any>} values
183
+ */
184
+ update(values) {
185
+ for (let n = 0; n < lists.length; n++) {
186
+ const list = lists[n]
187
+ const items = values[list.entry.path]
188
+ if (!Array.isArray(items)) continue
189
+
190
+ /** @type {Row[]} */
191
+ const next = []
192
+ for (let k = 0; k < items.length; k++) {
193
+ const kept = list.rows[k] && list.rows[k].sig === signature(items[k]) ? list.rows[k] : null
194
+ const row = kept || makeRow(list, items[k])
195
+ if (row) next.push(row)
196
+ }
197
+ if (next.length !== list.rows.length || next.some((row, k) => row !== list.rows[k])) {
198
+ moved = true
199
+ version++
200
+ }
201
+ list.rows = next
202
+ }
203
+ },
204
+
205
+ /**
206
+ * Where the pointer listeners of the rows' machines are, each row's under
207
+ * its own name: `<row>|<target>`, a target as the row's machine names it.
208
+ *
209
+ * @returns {Record<string, string[]>}
210
+ */
211
+ pointerTargets() {
212
+ moved = false
213
+ /** @type {Record<string, string[]>} */
214
+ const found = {}
215
+ for (let n = 0; n < lists.length; n++) {
216
+ for (let k = 0; k < lists[n].rows.length; k++) {
217
+ const row = lists[n].rows[k]
218
+ if (!row.runner) continue
219
+ const targets = row.runner.pointerTargets()
220
+ for (const name in targets) found[row.id + '|' + name] = targets[name]
221
+ }
222
+ }
223
+ return found
224
+ },
225
+
226
+ /** Changes whenever rows come or go, which is when the focus tree needs their nodes again */
227
+ get version() {
228
+ return version
229
+ },
230
+
231
+ /**
232
+ * A list's rows' focus nodes, named by the host's tree: `row.<id>/<rid>`
233
+ * (and `row.<id>/<sid>/<rid>` for a nested artboard in the row). Their
234
+ * topmost node is inside `marker`, the node that holds the list's place.
235
+ *
236
+ * @param {string} listId the list's id (`list.1`)
237
+ * @param {string} marker the list's node in the host's tree
238
+ * @returns {any[]}
239
+ */
240
+ nodes(listId, marker) {
241
+ const found = []
242
+ for (let n = 0; n < lists.length; n++) {
243
+ if (lists[n].entry.id !== listId) continue
244
+ for (let k = 0; k < lists[n].rows.length; k++) {
245
+ const row = lists[n].rows[k]
246
+ const own = row.entry.focus ? row.entry.focus.nodes : []
247
+ const prefix = 'row.' + row.id + '/'
248
+ for (let j = 0; j < own.length; j++) {
249
+ const node = extend(own[j], { rid: prefix + own[j].rid })
250
+ if (own[j].rid === '') {
251
+ // the row's own artboard is a node of the tree, inside the list's
252
+ node.parent = marker
253
+ node.kind = 'group'
254
+ } else {
255
+ node.parent = own[j].parent === '' ? (own.some((/** @type {any} */ one) => one.rid === '') ? prefix : marker) : prefix + own[j].parent
256
+ }
257
+ if (node.name) node.name = 'row ' + row.id + '/' + node.name
258
+ found.push(node)
259
+ }
260
+ }
261
+ }
262
+ return found
263
+ },
264
+
265
+ /**
266
+ * The key bindings of the rows' focus nodes, named as `nodes` names them,
267
+ * with the machine each runs in as `scope`.
268
+ *
269
+ * @returns {any[]}
270
+ */
271
+ bindings() {
272
+ const found = []
273
+ for (let n = 0; n < lists.length; n++) {
274
+ for (let k = 0; k < lists[n].rows.length; k++) {
275
+ const row = lists[n].rows[k]
276
+ const prefix = 'row.' + row.id + '/'
277
+ const own = row.entry.remoteKeys || []
278
+ for (let j = 0; j < own.length; j++) found.push(extend(own[j], { target: prefix + own[j].target, scope: 'row.' + row.id }))
279
+ const inside = row.entry.nested || []
280
+ for (let j = 0; j < inside.length; j++) {
281
+ const keys = inside[j].remoteKeys || []
282
+ 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 }))
283
+ }
284
+ }
285
+ }
286
+ return found
287
+ },
288
+
289
+ /**
290
+ * Focus actions the rows' machines ran as they started take effect: the
291
+ * focus tree has their nodes now.
292
+ */
293
+ flushFocus() {
294
+ const made = fresh
295
+ fresh = []
296
+ for (let n = 0; n < made.length; n++) {
297
+ const runner = made[n].runner
298
+ if (runner) runner.flushFocus()
299
+ }
300
+ },
301
+
302
+ /**
303
+ * A node of a row gained or lost focus.
304
+ *
305
+ * @param {'Focus' | 'Blur'} type
306
+ * @param {string} scope `row.<id>` or `row.<id>/<sid>`
307
+ * @param {string} rid the node in its machine's artboard
308
+ */
309
+ focusEvent(type, scope, rid) {
310
+ const found = machineOf(scope)
311
+ if (found && found.row.runner) found.row.runner.focusEvent(type, found.sid, rid)
312
+ },
313
+
314
+ /**
315
+ * Runs a key binding's actions in the machine of a row it belongs to.
316
+ *
317
+ * @param {string} scope `row.<id>` or `row.<id>/<sid>`
318
+ * @param {any[]} actions
319
+ */
320
+ act(scope, actions) {
321
+ const found = machineOf(scope)
322
+ if (found) found.row.runner.act(found.sid, actions)
323
+ },
324
+
325
+ /**
326
+ * Where a row's node was drawn, for the arrows.
327
+ *
328
+ * @param {string} rid `row.<id>/<rid>`
329
+ * @returns {{ x: number, y: number } | null}
330
+ */
331
+ center(rid) {
332
+ const at = rid.indexOf('/')
333
+ const found = at < 0 ? null : rowOf(rid.slice(4, at) + '|' + rid.slice(at + 1))
334
+ return found ? found.row.anim.hits.center(found.target) : null
335
+ },
336
+
337
+ /** Whether rows came or went since the pointer last asked for its targets */
338
+ get isPointerStale() {
339
+ return moved
340
+ },
341
+
342
+ /**
343
+ * Whether a canvas pixel is on a target of a row, in the last frame drawn.
344
+ *
345
+ * @param {string} name `<row>|<target>`
346
+ * @param {number} x
347
+ * @param {number} y
348
+ * @returns {boolean}
349
+ */
350
+ hit(name, x, y) {
351
+ const found = rowOf(name)
352
+ return !!found && found.row.anim.hits.test(found.target, x, y)
353
+ },
354
+
355
+ /**
356
+ * A pointer event on a target of a row's machine.
357
+ *
358
+ * @param {string} type
359
+ * @param {string} name `<row>|<target>`
360
+ * @returns {boolean} whether a row owns the target
361
+ */
362
+ pointer(type, name) {
363
+ const found = rowOf(name)
364
+ return !!found && !!found.row.runner && found.row.runner.pointer(type, found.target)
365
+ },
366
+
367
+ /**
368
+ * Moves the rows' machines on.
369
+ *
370
+ * @param {number} seconds
371
+ */
372
+ tick(seconds) {
373
+ for (let n = 0; n < lists.length; n++) {
374
+ for (let k = 0; k < lists[n].rows.length; k++) {
375
+ const runner = lists[n].rows[k].runner
376
+ if (runner) runner.tick(seconds)
377
+ }
378
+ }
379
+ },
380
+
381
+ /** Whether any row runs a machine, which keeps the clock going */
382
+ get isAnimated() {
383
+ return hasMachines
384
+ },
385
+
386
+ /**
387
+ * What each row of a list has now: the slot values its item gave it, and
388
+ * the frame of its Lottie it draws.
389
+ *
390
+ * @param {string} path the list's view-model path
391
+ * @returns {{ slots: Record<string, any>, frame: number }[]}
392
+ */
393
+ inspect(path) {
394
+ const found = []
395
+ for (let n = 0; n < lists.length; n++) {
396
+ if (lists[n].entry.path !== path) continue
397
+ 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]) })
398
+ }
399
+ return found
400
+ },
401
+
402
+ /**
403
+ * Where each list's rows are, in the artboard of the layout band showing.
404
+ *
405
+ * @returns {{ path: string, rows: { x: number, y: number, width: number, height: number }[] }[]}
406
+ */
407
+ layout() {
408
+ /** @type {{ path: string, rows: { x: number, y: number, width: number, height: number }[] }[]} */
409
+ const found = []
410
+ for (let n = 0; n < lists.length; n++) {
411
+ const placed = place(lists[n], options.band(), options.blended ? options.blended() : 0, options.cell ? options.cell() : null)
412
+ const rows = []
413
+ 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 })
414
+ found.push({ path: lists[n].entry.path, rows })
415
+ }
416
+ return found
417
+ },
418
+
419
+ /**
420
+ * Draws every row.
421
+ *
422
+ * @param {CanvasRenderingContext2D} ctx
423
+ * @param {import('../lottie/lottie.paths.js').Matrix} matrix the artboard's space onto the canvas
424
+ */
425
+ draw(ctx, matrix) {
426
+ for (let n = 0; n < lists.length; n++) {
427
+ const list = lists[n]
428
+ if (!list.rows.length) continue
429
+ const placed = place(list, options.band(), options.blended ? options.blended() : 0, options.cell ? options.cell() : null)
430
+ for (let k = 0; k < list.rows.length; k++) {
431
+ const row = list.rows[k]
432
+ const at = multiply(matrix, [ 1, 0, 0, 1, placed.left + placed.x[k], placed.top + placed.y[k] ])
433
+ ctx.save()
434
+ // a row is cut off at its own edge, as an artboard is
435
+ ctx.setTransform(at[0], at[1], at[2], at[3], at[4], at[5])
436
+ ctx.beginPath()
437
+ ctx.rect(0, 0, row.entry.width, row.entry.height)
438
+ ctx.clip()
439
+ row.anim.draw(ctx, rowFrame(row), at)
440
+ ctx.restore()
441
+ }
442
+ }
443
+ },
444
+ }
445
+ }
446
+
447
+ /**
448
+ * The frame of a row's Lottie to draw: the machine's, else the first. The
449
+ * machines of the nested artboards in the row set where their layers' time
450
+ * remaps are in the row's slots.
451
+ *
452
+ * @param {Row} row
453
+ * @returns {number}
454
+ */
455
+ function rowFrame(row) {
456
+ if (!row.runner) return row.anim.inPoint
457
+ row.runner.apply(row.anim.slots, row.anim.fps)
458
+ const time = row.anim.slots.row
459
+ if (time === undefined) return row.anim.inPoint
460
+ // a Lottie draws up to its out point, not at it: a finished machine holds the last frame it has
461
+ return Math.min(row.anim.inPoint + time * row.anim.fps, Math.max(row.anim.outPoint - 0.001, row.anim.inPoint))
462
+ }
463
+
464
+ /**
465
+ * The container's box at a band of the layout grid.
466
+ *
467
+ * @param {any} entry the list's description
468
+ * @param {{ width: number, height: number }} corner the band
469
+ * @returns {{ width: number, x: number, y: number, w: number, h: number } | null}
470
+ */
471
+ function boxAt(entry, corner) {
472
+ const all = entry.gridBoxes ? entry.boxes.concat(entry.gridBoxes) : entry.boxes
473
+ let found = null
474
+ // a box says its height unless the file is older than the grid
475
+ 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]
476
+ return found
477
+ }
478
+
479
+ /**
480
+ * The container's box in the band showing, and where its rows sit in it.
481
+ *
482
+ * @param {{ entry: any, rows: Row[] }} list
483
+ * @param {number} band the layout band's width, 0 for the file's own
484
+ * @param {number} blended the canvas's width between two blending bands, else 0
485
+ * @param {import('../bands/bands.js').Cell | null} [cell] where the canvas falls in the layout grid
486
+ * @returns {{ left: number, top: number, x: number[], y: number[] }}
487
+ */
488
+ function place(list, band, blended, cell) {
489
+ const boxes = list.entry.boxes
490
+ let box = boxes[0]
491
+ let at = 0
492
+ for (let n = 0; n < boxes.length; n++) {
493
+ const isOwn = band === 0 ? boxes[n].authored : boxes[n].width === band
494
+ if (isOwn) {
495
+ box = boxes[n]
496
+ at = n
497
+ }
498
+ }
499
+ const after = boxes[at + 1]
500
+ const corner = cell ? boxAt(list.entry, cell.corner) : null
501
+ if (cell && corner) {
502
+ // the container's box is bilinear in the size inside a cell that blends: a straight line along one side of it
503
+ const right = (cell.right && boxAt(list.entry, cell.right)) || corner
504
+ const up = (cell.up && boxAt(list.entry, cell.up)) || corner
505
+ const diag = (cell.diag && boxAt(list.entry, cell.diag)) || (cell.right ? right : up)
506
+ 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
507
+ box = { width: corner.width, authored: false, x: mix('x'), y: mix('y'), w: mix('w'), h: mix('h') }
508
+ } else if (blended > box.width && after && after.width > box.width) {
509
+ // the container's box is a straight line in the width between bands that blend
510
+ const t = Math.min(1, (blended - box.width) / (after.width - box.width))
511
+ const mix = (/** @type {number} */ a, /** @type {number} */ b) => a + (b - a) * t
512
+ 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) }
513
+ }
514
+ const sizes = []
515
+ for (let n = 0; n < list.rows.length; n++) sizes.push({ w: list.rows[n].entry.width, h: list.rows[n].entry.height })
516
+ const laid = layoutRows(list.entry.flex, box, sizes)
517
+ return { left: box.x, top: box.y, x: laid.x, y: laid.y }
518
+ }
@@ -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
+ }