@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,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A shape layer's items, compiled once and run per frame into draw
|
|
3
|
+
* operations.
|
|
4
|
+
*
|
|
5
|
+
* Lottie's rules, in a group's item list:
|
|
6
|
+
* - earlier items are on top, so operations draw in reverse;
|
|
7
|
+
* - a paint covers every path above it in its group, and in groups above it;
|
|
8
|
+
* - a modifier (trim, merge) changes those same paths, so a subgroup's own
|
|
9
|
+
* paint draws them trimmed too.
|
|
10
|
+
*
|
|
11
|
+
* Geometry therefore lives in a shared holder: a trim rewrites the holder's
|
|
12
|
+
* paths, and every paint that captured it draws the result.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { evaluator } from './lottie.props.js'
|
|
16
|
+
import { ellipse, fromLottie, IDENTITY, multiply, rectangle, star, transformMatrix, trimEach } from './lottie.paths.js'
|
|
17
|
+
|
|
18
|
+
/** @import { Path, Matrix } from './lottie.paths.js' */
|
|
19
|
+
/** @import { Slots } from './lottie.props.js' */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Geometry gathered from items above a paint or modifier.
|
|
23
|
+
* @typedef {{ paths: Path[], merge?: { mode: number, parts: Entry[] } }} Holder
|
|
24
|
+
* @typedef {{ holder: Holder, matrix: Matrix }} Entry `matrix` maps the holder's space into the gathering group's
|
|
25
|
+
* @typedef {{ paint: any, entries: Entry[], matrix: Matrix, opacity: number }} Operation `matrix` maps the paint's group into the layer
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {any[]} items a shape layer's `shapes`, or a group's `it`
|
|
30
|
+
* @param {Slots} slots
|
|
31
|
+
* @returns {any[]} compiled items
|
|
32
|
+
*/
|
|
33
|
+
export function compileItems(items, slots) {
|
|
34
|
+
const compiled = []
|
|
35
|
+
for (let n = 0; n < (items || []).length; n++) {
|
|
36
|
+
const item = items[n]
|
|
37
|
+
if (item.hd) continue
|
|
38
|
+
const one = compileItem(item, slots)
|
|
39
|
+
if (one) compiled.push(one)
|
|
40
|
+
}
|
|
41
|
+
return compiled
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {any} item
|
|
46
|
+
* @param {Slots} slots
|
|
47
|
+
*/
|
|
48
|
+
function compileItem(item, slots) {
|
|
49
|
+
const at = (/** @type {any} */ property, /** @type {any} */ fallback) => evaluator(property, slots, fallback)
|
|
50
|
+
const reversed = item.d === 3
|
|
51
|
+
|
|
52
|
+
switch (item.ty) {
|
|
53
|
+
case 'gr':
|
|
54
|
+
return { kind: 'group', rid: typeof item.rid === 'string' ? item.rid : '', items: compileItems(item.it, slots), transform: compileTransform((item.it || []).filter((/** @type {any} */ i) => i.ty === 'tr')[0], slots) }
|
|
55
|
+
case 'sh': {
|
|
56
|
+
const shape = at(item.ks, { v: [], i: [], o: [], c: false })
|
|
57
|
+
return { kind: 'geometry', path: (/** @type {number} */ f) => fromLottie(unwrap(shape(f))) }
|
|
58
|
+
}
|
|
59
|
+
case 'rc': {
|
|
60
|
+
const p = at(item.p, [ 0, 0 ])
|
|
61
|
+
const s = at(item.s, [ 0, 0 ])
|
|
62
|
+
const r = at(item.r, 0)
|
|
63
|
+
return { kind: 'geometry', path: (/** @type {number} */ f) => rectangle(p(f), s(f), scalar(r(f)), reversed) }
|
|
64
|
+
}
|
|
65
|
+
case 'el': {
|
|
66
|
+
const p = at(item.p, [ 0, 0 ])
|
|
67
|
+
const s = at(item.s, [ 0, 0 ])
|
|
68
|
+
return { kind: 'geometry', path: (/** @type {number} */ f) => ellipse(p(f), s(f), reversed) }
|
|
69
|
+
}
|
|
70
|
+
case 'sr': {
|
|
71
|
+
const p = at(item.p, [ 0, 0 ])
|
|
72
|
+
const pt = at(item.pt, 5)
|
|
73
|
+
const r = at(item.r, 0)
|
|
74
|
+
const or = at(item.or, 0)
|
|
75
|
+
const ir = at(item.ir, 0)
|
|
76
|
+
const os = at(item.os, 0)
|
|
77
|
+
const is = at(item.is, 0)
|
|
78
|
+
return {
|
|
79
|
+
kind: 'geometry',
|
|
80
|
+
path: (/** @type {number} */ f) => star({ type: item.sy, points: scalar(pt(f)), position: p(f), rotation: scalar(r(f)), outer: scalar(or(f)), inner: scalar(ir(f)), outerRound: scalar(os(f)), innerRound: scalar(is(f)), reversed }),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
case 'tm':
|
|
84
|
+
return { kind: 'trim', start: at(item.s, 0), end: at(item.e, 100), offset: at(item.o, 0), together: item.m === 2 }
|
|
85
|
+
case 'mm':
|
|
86
|
+
return { kind: 'merge', mode: item.mm || 1 }
|
|
87
|
+
case 'fl':
|
|
88
|
+
case 'st':
|
|
89
|
+
case 'gf':
|
|
90
|
+
case 'gs':
|
|
91
|
+
return { kind: 'paint', paint: compilePaint(item, slots) }
|
|
92
|
+
default:
|
|
93
|
+
// `tr` is read by its group; repeaters, round corners and the rest are not drawn
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A shape keyframe value arrives wrapped in a one-element array.
|
|
100
|
+
*
|
|
101
|
+
* @param {any} value
|
|
102
|
+
*/
|
|
103
|
+
function unwrap(value) {
|
|
104
|
+
return Array.isArray(value) ? value[0] || { v: [] } : value
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @param {any} value */
|
|
108
|
+
export function scalar(value) {
|
|
109
|
+
return Array.isArray(value) ? value[0] : value
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {any} tr
|
|
114
|
+
* @param {Slots} slots
|
|
115
|
+
* @returns {(frame: number) => { matrix: Matrix, opacity: number }}
|
|
116
|
+
*/
|
|
117
|
+
export function compileTransform(tr, slots) {
|
|
118
|
+
if (!tr) return () => ({ matrix: IDENTITY, opacity: 1 })
|
|
119
|
+
|
|
120
|
+
const at = (/** @type {any} */ property, /** @type {any} */ fallback) => evaluator(property, slots, fallback)
|
|
121
|
+
const a = at(tr.a, [ 0, 0 ])
|
|
122
|
+
const p = splitPosition(tr.p, slots)
|
|
123
|
+
const s = at(tr.s, [ 100, 100 ])
|
|
124
|
+
const r = at(tr.r || tr.rz, 0)
|
|
125
|
+
const o = at(tr.o, 100)
|
|
126
|
+
const sk = at(tr.sk, 0)
|
|
127
|
+
const sa = at(tr.sa, 0)
|
|
128
|
+
return (frame) => ({
|
|
129
|
+
matrix: transformMatrix(a(frame), p(frame), s(frame), scalar(r(frame)), scalar(sk(frame)), scalar(sa(frame))),
|
|
130
|
+
opacity: scalar(o(frame)) / 100,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A position, given whole or as separate `x` and `y` (`s: true`).
|
|
136
|
+
*
|
|
137
|
+
* @param {any} position
|
|
138
|
+
* @param {Slots} slots
|
|
139
|
+
*/
|
|
140
|
+
function splitPosition(position, slots) {
|
|
141
|
+
if (position && position.s) {
|
|
142
|
+
const x = evaluator(position.x, slots, 0)
|
|
143
|
+
const y = evaluator(position.y, slots, 0)
|
|
144
|
+
return (/** @type {number} */ frame) => [ scalar(x(frame)), scalar(y(frame)) ]
|
|
145
|
+
}
|
|
146
|
+
return evaluator(position, slots, [ 0, 0 ])
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {any} item
|
|
151
|
+
* @param {Slots} slots
|
|
152
|
+
*/
|
|
153
|
+
function compilePaint(item, slots) {
|
|
154
|
+
const at = (/** @type {any} */ property, /** @type {any} */ fallback) => evaluator(property, slots, fallback)
|
|
155
|
+
const isStroke = item.ty === 'st' || item.ty === 'gs'
|
|
156
|
+
const isGradient = item.ty === 'gf' || item.ty === 'gs'
|
|
157
|
+
return {
|
|
158
|
+
isStroke,
|
|
159
|
+
isGradient,
|
|
160
|
+
rule: item.r === 2 ? 'evenodd' : 'nonzero',
|
|
161
|
+
opacity: at(item.o, 100),
|
|
162
|
+
color: at(item.c, [ 0, 0, 0, 1 ]),
|
|
163
|
+
width: at(item.w, 1),
|
|
164
|
+
cap: [ 'butt', 'butt', 'round', 'square' ][item.lc || 1],
|
|
165
|
+
join: [ 'miter', 'miter', 'round', 'bevel' ][item.lj || 1],
|
|
166
|
+
miter: item.ml || 4,
|
|
167
|
+
dashes: (item.d || []).map((/** @type {any} */ d) => ({ kind: d.n, value: at(d.v, 0) })),
|
|
168
|
+
gradient: isGradient
|
|
169
|
+
? { type: item.t || 1, start: at(item.s, [ 0, 0 ]), end: at(item.e, [ 0, 0 ]), stops: at(item.g && item.g.k, []), count: (item.g && item.g.p) || 0, highlight: at(item.h, 0), angle: at(item.a, 0) }
|
|
170
|
+
: null,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Running
|
|
175
|
+
// ■■■■■■■
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Where a frame's hit-test geometry goes: groups tagged with a watched `rid` hand over their paths.
|
|
179
|
+
* @typedef {object} HitSink
|
|
180
|
+
* @property {import('../hit/hit.js').Hits} hits
|
|
181
|
+
* @property {Matrix} matrix the layer's space onto the canvas
|
|
182
|
+
* @property {string} prefix what a group's `rid` is named under: a nested machine's artboard has its own, so it is `<the layer's nid>/`; "" in the animation's own
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Runs a group's items at a frame.
|
|
187
|
+
*
|
|
188
|
+
* @param {any[]} items compiled items
|
|
189
|
+
* @param {number} frame
|
|
190
|
+
* @param {Matrix} matrix the group's space into the layer's
|
|
191
|
+
* @param {number} opacity the group's opacity, compounded
|
|
192
|
+
* @param {HitSink | null} [sink] records the paths of groups a pointer listener targets
|
|
193
|
+
* @returns {{ entries: Entry[], operations: Operation[] }} the paths above, for the parent's paints, and what to draw, top first
|
|
194
|
+
*/
|
|
195
|
+
export function runGroup(items, frame, matrix, opacity, sink) {
|
|
196
|
+
/** @type {Entry[]} */
|
|
197
|
+
let entries = []
|
|
198
|
+
/** @type {Operation[]} */
|
|
199
|
+
const operations = []
|
|
200
|
+
|
|
201
|
+
for (let n = 0; n < items.length; n++) {
|
|
202
|
+
const item = items[n]
|
|
203
|
+
if (item.kind === 'geometry') {
|
|
204
|
+
entries.push({ holder: { paths: [ item.path(frame) ] }, matrix: IDENTITY })
|
|
205
|
+
} else if (item.kind === 'group') {
|
|
206
|
+
const local = item.transform(frame)
|
|
207
|
+
const inner = runGroup(item.items, frame, multiply(matrix, local.matrix), opacity * local.opacity, sink)
|
|
208
|
+
if (sink && item.rid) {
|
|
209
|
+
const rid = sink.prefix + item.rid
|
|
210
|
+
if (sink.hits.wants(rid)) sink.hits.add(rid, inner.entries, multiply(sink.matrix, multiply(matrix, local.matrix)))
|
|
211
|
+
}
|
|
212
|
+
for (let k = 0; k < inner.operations.length; k++) operations.push(inner.operations[k])
|
|
213
|
+
for (let k = 0; k < inner.entries.length; k++) entries.push({ holder: inner.entries[k].holder, matrix: multiply(local.matrix, inner.entries[k].matrix) })
|
|
214
|
+
} else if (item.kind === 'trim') {
|
|
215
|
+
trimEntries(entries, scalar(item.start(frame)), scalar(item.end(frame)), scalar(item.offset(frame)), item.together)
|
|
216
|
+
} else if (item.kind === 'merge') {
|
|
217
|
+
entries = entries.length > 1 ? [ { holder: { paths: [], merge: { mode: item.mode, parts: entries } }, matrix: IDENTITY } ] : entries
|
|
218
|
+
} else if (item.kind === 'paint') {
|
|
219
|
+
operations.push({ paint: item.paint, entries: entries.slice(), matrix, opacity })
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { entries, operations }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Trims every holder above, in its own space. Together, the holders' paths
|
|
227
|
+
* are trimmed as one list and each holder keeps its own share.
|
|
228
|
+
*
|
|
229
|
+
* @param {Entry[]} entries
|
|
230
|
+
* @param {number} start
|
|
231
|
+
* @param {number} end
|
|
232
|
+
* @param {number} offset
|
|
233
|
+
* @param {boolean} together
|
|
234
|
+
*/
|
|
235
|
+
function trimEntries(entries, start, end, offset, together) {
|
|
236
|
+
/** @type {Holder[]} */
|
|
237
|
+
const holders = []
|
|
238
|
+
for (let n = 0; n < entries.length; n++) {
|
|
239
|
+
const holder = entries[n].holder
|
|
240
|
+
if (!holder.merge) holders.push(holder)
|
|
241
|
+
else for (let k = 0; k < holder.merge.parts.length; k++) holders.push(holder.merge.parts[k].holder)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const all = []
|
|
245
|
+
const owners = []
|
|
246
|
+
for (let n = 0; n < holders.length; n++) {
|
|
247
|
+
for (let k = 0; k < holders[n].paths.length; k++) {
|
|
248
|
+
all.push(holders[n].paths[k])
|
|
249
|
+
owners.push(n)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const kept = trimEach(all, start, end, offset, together)
|
|
254
|
+
for (let n = 0; n < holders.length; n++) holders[n].paths = []
|
|
255
|
+
for (let k = 0; k < kept.length; k++) {
|
|
256
|
+
for (let j = 0; j < kept[k].length; j++) holders[owners[k]].paths.push(kept[k][j])
|
|
257
|
+
}
|
|
258
|
+
}
|
package/mixer/mixer.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pose mixer: draws several animations of one artboard at once, each at
|
|
3
|
+
* its own time and weight, as one picture.
|
|
4
|
+
*
|
|
5
|
+
* rive2lottie writes every animation of an artboard as a Lottie with the
|
|
6
|
+
* same layers and shapes, and tags each property with `rid`, the Rive object
|
|
7
|
+
* and property it came from, and `ky` when that animation keys it. The mixer
|
|
8
|
+
* takes one of them (the longest) as the picture's structure, and gives each
|
|
9
|
+
* keyed property a mixing evaluator in place of its keyframes. For a set of
|
|
10
|
+
* poses, `{ animation, time, weight }`, that evaluator reads the property in
|
|
11
|
+
* each pose's animation at the pose's time and blends the values in order,
|
|
12
|
+
* as Rive applies animations: each lerps the value so far toward its own by
|
|
13
|
+
* its weight. A pose whose animation doesn't key the property leaves it
|
|
14
|
+
* alone. The value so far starts from what the property held after the
|
|
15
|
+
* previous frame, as Rive's artboard keeps what it last applied: a property
|
|
16
|
+
* no active animation keys stays where it was, and a blend whose weights
|
|
17
|
+
* don't add up to one settles over a few frames. A pair of axes (position)
|
|
18
|
+
* keyed on one axis only is mixed on that axis only. What isn't tagged (a nested artboard's own animation) follows the
|
|
19
|
+
* first pose's time; text overlays are mixed by the player (`mixOverlays`).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createAnimation } from '../lottie/lottie.js'
|
|
23
|
+
import { evaluator } from '../lottie/lottie.props.js'
|
|
24
|
+
import { mixValues } from './mixer.mix.js'
|
|
25
|
+
|
|
26
|
+
/** @import { Animation } from '../lottie/lottie.js' */
|
|
27
|
+
/** @import { Evaluator } from '../lottie/lottie.props.js' */
|
|
28
|
+
/** @import { Matrix } from '../lottie/lottie.paths.js' */
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One animation shown at a time, with a weight.
|
|
32
|
+
* @typedef {object} Pose
|
|
33
|
+
* @property {string} animation an animation id
|
|
34
|
+
* @property {number} time seconds into it
|
|
35
|
+
* @property {number} weight 0 to 1
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** What an animation does to one property: its value at a frame, whether it keys the property, and, for a pair of axes with one keyed, which */
|
|
39
|
+
/** @typedef {{ at: Evaluator, keyed: boolean, axes: number[] | null }} Source */
|
|
40
|
+
|
|
41
|
+
/** A pose, prepared for the evaluators: where its animation's sources are, and how far its frame is from the drawn one */
|
|
42
|
+
/** @typedef {{ sources: Record<string, Source>, offset: number, weight: number }} Prepared */
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What the mixing evaluators share: the poses of the draw, and each property's value after the previous frame.
|
|
46
|
+
* @typedef {object} Current
|
|
47
|
+
* @property {Prepared[]} poses
|
|
48
|
+
* @property {Record<string, any>} last by `rid`, what each held after the previous frame
|
|
49
|
+
* @property {Record<string, any>} next by `rid`, what each holds in this one
|
|
50
|
+
* @property {number} frame the frame `next` belongs to
|
|
51
|
+
* @property {number} passes how many times a draw applies the poses
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/** No slots: a source reads its keyframes, and the drawn animation's own slots go over the result */
|
|
55
|
+
const NO_SLOTS = {}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {object} Mixer
|
|
59
|
+
* @property {Animation} animation the mixed picture; its `slots` are live
|
|
60
|
+
* @property {string} carrier the animation whose structure it has
|
|
61
|
+
* @property {(ctx: CanvasRenderingContext2D, poses: Pose[], matrix: Matrix, frame?: number, passes?: number) => void} draw `frame` counts the stage's steps: drawing the same frame again leaves each property where it was; `passes` applies the poses that many times, for a stage that has stopped but must settle
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {Record<string, any>} lotties parsed Lottie JSON by animation id; the longest is changed in place
|
|
66
|
+
* @param {object} [options]
|
|
67
|
+
* @param {string} [options.base] where images resolve from
|
|
68
|
+
* @param {() => Record<string, string>} [options.families] the bundle's fonts by name, as registered
|
|
69
|
+
* @returns {Mixer}
|
|
70
|
+
*/
|
|
71
|
+
export function createMixer(lotties, options) {
|
|
72
|
+
const carrier = longest(lotties)
|
|
73
|
+
/** @type {Record<string, Record<string, Source>>} */
|
|
74
|
+
const sources = {}
|
|
75
|
+
for (const id in lotties) sources[id] = collect(lotties[id])
|
|
76
|
+
|
|
77
|
+
/** @type {Current} */
|
|
78
|
+
const current = { poses: [], last: {}, next: {}, frame: -1, passes: 1 }
|
|
79
|
+
install(lotties[carrier], sources, carrier, current)
|
|
80
|
+
const animation = createAnimation(lotties[carrier], options)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
animation,
|
|
84
|
+
carrier,
|
|
85
|
+
draw(ctx, poses, matrix, frame, passes) {
|
|
86
|
+
current.passes = passes || 1
|
|
87
|
+
// a new frame: what the last one left is where this one starts
|
|
88
|
+
if (frame === undefined || frame !== current.frame) {
|
|
89
|
+
for (const rid in current.next) current.last[rid] = current.next[rid]
|
|
90
|
+
current.next = {}
|
|
91
|
+
current.frame = frame === undefined ? -1 : frame
|
|
92
|
+
}
|
|
93
|
+
animation.draw(ctx, prepare(lotties, sources, poses, current, carrier), matrix)
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The animation with the longest timeline, in seconds: any pose's time fits it.
|
|
100
|
+
*
|
|
101
|
+
* @param {Record<string, any>} lotties
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
function longest(lotties) {
|
|
105
|
+
let best = ''
|
|
106
|
+
let bestSeconds = -1
|
|
107
|
+
for (const id in lotties) {
|
|
108
|
+
const lottie = lotties[id]
|
|
109
|
+
const seconds = ((lottie.op || 0) - (lottie.ip || 0)) / (lottie.fr || 30)
|
|
110
|
+
if (seconds > bestSeconds) {
|
|
111
|
+
best = id
|
|
112
|
+
bestSeconds = seconds
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return best
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Every tagged property of a Lottie, by its `rid`; the first of a repeated one wins.
|
|
120
|
+
*
|
|
121
|
+
* @param {any} lottie
|
|
122
|
+
* @returns {Record<string, Source>}
|
|
123
|
+
*/
|
|
124
|
+
function collect(lottie) {
|
|
125
|
+
/** @type {Record<string, Source>} */
|
|
126
|
+
const found = {}
|
|
127
|
+
visit(lottie, (property) => {
|
|
128
|
+
if (found[property.rid]) return
|
|
129
|
+
found[property.rid] = { at: evaluator(property, NO_SLOTS), keyed: property.ky === 1, axes: Array.isArray(property.kc) ? property.kc : null }
|
|
130
|
+
})
|
|
131
|
+
return found
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Calls back for every tagged property under a node, in assets as in layers.
|
|
136
|
+
*
|
|
137
|
+
* @param {any} node
|
|
138
|
+
* @param {(property: any) => void} found
|
|
139
|
+
*/
|
|
140
|
+
function visit(node, found) {
|
|
141
|
+
if (node === null || typeof node !== 'object') return
|
|
142
|
+
if (Array.isArray(node)) {
|
|
143
|
+
for (let n = 0; n < node.length; n++) visit(node[n], found)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
if (typeof node.rid === 'string' && 'k' in node) found(node)
|
|
147
|
+
for (const key in node) {
|
|
148
|
+
if (key === 'k' || key === 'rid') continue
|
|
149
|
+
visit(node[key], found)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Gives the carrier's keyed properties their mixing evaluators.
|
|
155
|
+
*
|
|
156
|
+
* @param {any} lottie
|
|
157
|
+
* @param {Record<string, Record<string, Source>>} sources
|
|
158
|
+
* @param {string} carrier
|
|
159
|
+
* @param {Current} current
|
|
160
|
+
*/
|
|
161
|
+
function install(lottie, sources, carrier, current) {
|
|
162
|
+
/** @type {Record<string, any>} */
|
|
163
|
+
const bases = {}
|
|
164
|
+
/** @type {Record<string, boolean>} */
|
|
165
|
+
const isKeyed = {}
|
|
166
|
+
for (const id in sources) {
|
|
167
|
+
for (const rid in sources[id]) {
|
|
168
|
+
if (sources[id][rid].keyed) isKeyed[rid] = true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const rid in isKeyed) bases[rid] = baseOf(rid, sources, carrier, lottie)
|
|
172
|
+
|
|
173
|
+
visit(lottie, (property) => {
|
|
174
|
+
if (isKeyed[property.rid]) property.mx = mixing(property.rid, bases[property.rid], current)
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A property's value with no animation on it: where one animation holds it
|
|
180
|
+
* still, that value; where all key it, the carrier's first. A pair of axes
|
|
181
|
+
* takes each axis from an animation that holds that axis still.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} rid
|
|
184
|
+
* @param {Record<string, Record<string, Source>>} sources
|
|
185
|
+
* @param {string} carrier
|
|
186
|
+
* @param {any} lottie
|
|
187
|
+
* @returns {any}
|
|
188
|
+
*/
|
|
189
|
+
function baseOf(rid, sources, carrier, lottie) {
|
|
190
|
+
for (const id in sources) {
|
|
191
|
+
const source = sources[id][rid]
|
|
192
|
+
if (source && !source.keyed) return source.at(0)
|
|
193
|
+
}
|
|
194
|
+
const first = sources[carrier][rid].at(lottie.ip || 0)
|
|
195
|
+
if (!Array.isArray(first)) return first
|
|
196
|
+
|
|
197
|
+
const result = first.slice()
|
|
198
|
+
for (let axis = 0; axis < result.length; axis++) {
|
|
199
|
+
for (const id in sources) {
|
|
200
|
+
const source = sources[id][rid]
|
|
201
|
+
if (!source || !source.axes || source.axes[axis]) continue
|
|
202
|
+
const still = source.at(0)
|
|
203
|
+
if (Array.isArray(still) && typeof still[axis] === 'number') result[axis] = still[axis]
|
|
204
|
+
break
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return result
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Mixes only the keyed axes of a vector.
|
|
212
|
+
*
|
|
213
|
+
* @param {any} a the value so far
|
|
214
|
+
* @param {any} b the value being mixed in
|
|
215
|
+
* @param {number} t b's weight
|
|
216
|
+
* @param {number[]} axes 1 for each axis b moves
|
|
217
|
+
* @returns {any}
|
|
218
|
+
*/
|
|
219
|
+
function mixAxes(a, b, t, axes) {
|
|
220
|
+
if (t <= 0 || !Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return mixValues(a, b, t)
|
|
221
|
+
const result = a.slice()
|
|
222
|
+
for (let n = 0; n < result.length; n++) {
|
|
223
|
+
if (axes[n] && typeof a[n] === 'number' && typeof b[n] === 'number') result[n] = a[n] + (b[n] - a[n]) * Math.min(1, t)
|
|
224
|
+
}
|
|
225
|
+
return result
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @param {string} rid
|
|
230
|
+
* @param {any} base
|
|
231
|
+
* @param {Current} current
|
|
232
|
+
* @returns {Evaluator}
|
|
233
|
+
*/
|
|
234
|
+
function mixing(rid, base, current) {
|
|
235
|
+
return (frame) => {
|
|
236
|
+
const poses = current.poses
|
|
237
|
+
let value = rid in current.last ? current.last[rid] : base
|
|
238
|
+
for (let pass = 0; pass < current.passes; pass++) {
|
|
239
|
+
for (let n = 0; n < poses.length; n++) {
|
|
240
|
+
const pose = poses[n]
|
|
241
|
+
const source = pose.sources[rid]
|
|
242
|
+
if (!source || !source.keyed) continue
|
|
243
|
+
|
|
244
|
+
const sample = source.at(frame + pose.offset)
|
|
245
|
+
value = source.axes ? mixAxes(value, sample, pose.weight, source.axes) : mixValues(value, sample, pose.weight)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
current.next[rid] = value
|
|
249
|
+
return value
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Readies the poses for a draw.
|
|
255
|
+
*
|
|
256
|
+
* @param {Record<string, any>} lotties
|
|
257
|
+
* @param {Record<string, Record<string, Source>>} sources
|
|
258
|
+
* @param {Pose[]} poses
|
|
259
|
+
* @param {Current} current
|
|
260
|
+
* @param {string} carrier
|
|
261
|
+
* @returns {number} the carrier's frame to draw
|
|
262
|
+
*/
|
|
263
|
+
function prepare(lotties, sources, poses, current, carrier) {
|
|
264
|
+
const structure = lotties[carrier]
|
|
265
|
+
const first = structure.ip || 0
|
|
266
|
+
const last = (structure.op || 0) - 0.001
|
|
267
|
+
current.poses = []
|
|
268
|
+
let drawn = first
|
|
269
|
+
let isFirst = true
|
|
270
|
+
for (let n = 0; n < poses.length; n++) {
|
|
271
|
+
const pose = poses[n]
|
|
272
|
+
const lottie = lotties[pose.animation]
|
|
273
|
+
if (!lottie) continue
|
|
274
|
+
|
|
275
|
+
if (isFirst) {
|
|
276
|
+
// the first pose sets the frame that what isn't tagged follows
|
|
277
|
+
drawn = Math.min(Math.max(first + pose.time * (structure.fr || 30), first), last)
|
|
278
|
+
isFirst = false
|
|
279
|
+
}
|
|
280
|
+
const frame = (lottie.ip || 0) + pose.time * (lottie.fr || 30)
|
|
281
|
+
current.poses.push({ sources: sources[pose.animation], offset: frame - drawn, weight: pose.weight })
|
|
282
|
+
}
|
|
283
|
+
return drawn
|
|
284
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mixing two values of one Lottie property: numbers and vectors (positions,
|
|
3
|
+
* scales, colours) blend component by component, and paths blend vertex by
|
|
4
|
+
* vertex when they have the same vertices. Anything else can't be blended,
|
|
5
|
+
* so the heavier of the two wins.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {any} a the value so far
|
|
10
|
+
* @param {any} b the value being mixed in
|
|
11
|
+
* @param {number} t b's weight, 0 to 1: 0 is all `a`, 1 is all `b`
|
|
12
|
+
* @returns {any}
|
|
13
|
+
*/
|
|
14
|
+
export function mixValues(a, b, t) {
|
|
15
|
+
if (t <= 0 || b === undefined) return a
|
|
16
|
+
if (t >= 1 || a === undefined) return b
|
|
17
|
+
|
|
18
|
+
if (typeof a === 'number' && typeof b === 'number') return a + (b - a) * t
|
|
19
|
+
|
|
20
|
+
// keyed values arrive as [v], static ones as v: a number and a one-element vector mix as a vector
|
|
21
|
+
const left = typeof a === 'number' ? [ a ] : a
|
|
22
|
+
const right = typeof b === 'number' ? [ b ] : b
|
|
23
|
+
if (isVector(left) && isVector(right) && left.length === right.length) return mixVectors(left, right, t)
|
|
24
|
+
|
|
25
|
+
const leftPath = pathOf(a)
|
|
26
|
+
const rightPath = pathOf(b)
|
|
27
|
+
if (leftPath && rightPath && sameVertices(leftPath, rightPath)) {
|
|
28
|
+
const mixed = mixPaths(leftPath, rightPath, t)
|
|
29
|
+
return Array.isArray(a) || Array.isArray(b) ? [ mixed ] : mixed
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return t >= 0.5 ? b : a
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {any} value */
|
|
36
|
+
function isVector(value) {
|
|
37
|
+
if (!Array.isArray(value)) return false
|
|
38
|
+
for (let n = 0; n < value.length; n++) if (typeof value[n] !== 'number') return false
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {number[]} a
|
|
44
|
+
* @param {number[]} b
|
|
45
|
+
* @param {number} t
|
|
46
|
+
*/
|
|
47
|
+
function mixVectors(a, b, t) {
|
|
48
|
+
const result = []
|
|
49
|
+
for (let n = 0; n < a.length; n++) result.push(a[n] + (b[n] - a[n]) * t)
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A path value is `{ c, v, i, o }`, or that in a one-element array.
|
|
55
|
+
*
|
|
56
|
+
* @param {any} value
|
|
57
|
+
* @returns {any} the path, or null
|
|
58
|
+
*/
|
|
59
|
+
function pathOf(value) {
|
|
60
|
+
const path = Array.isArray(value) ? value[0] : value
|
|
61
|
+
return path && typeof path === 'object' && Array.isArray(path.v) ? path : null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {any} a
|
|
66
|
+
* @param {any} b
|
|
67
|
+
*/
|
|
68
|
+
function sameVertices(a, b) {
|
|
69
|
+
return a.c === b.c && a.v.length === b.v.length && a.i.length === b.i.length && a.o.length === b.o.length
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {any} a
|
|
74
|
+
* @param {any} b
|
|
75
|
+
* @param {number} t
|
|
76
|
+
*/
|
|
77
|
+
function mixPaths(a, b, t) {
|
|
78
|
+
return { c: a.c, v: mixPoints(a.v, b.v, t), i: mixPoints(a.i, b.i, t), o: mixPoints(a.o, b.o, t) }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {number[][]} a
|
|
83
|
+
* @param {number[][]} b
|
|
84
|
+
* @param {number} t
|
|
85
|
+
*/
|
|
86
|
+
function mixPoints(a, b, t) {
|
|
87
|
+
const points = []
|
|
88
|
+
for (let n = 0; n < a.length; n++) points.push(mixVectors(a[n], b[n], t))
|
|
89
|
+
return points
|
|
90
|
+
}
|