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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ }