@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,459 @@
1
+ /**
2
+ * Geometry: Lottie's shapes as cubic bezier paths, the affine maths that
3
+ * moves them between spaces, and trimming by length.
4
+ *
5
+ * A path keeps absolute control points, so a transform maps it exactly:
6
+ * `cOut[k]` follows point k, `cIn[k]` leads into it. Rectangles, ellipses and
7
+ * stars start and wind as lottie-web builds them, since a trim depends on both.
8
+ */
9
+
10
+ /** Bezier control distance for a quarter circle */
11
+ const KAPPA = 0.5519
12
+
13
+ /**
14
+ * @typedef {{ x: number, y: number }} Point
15
+ * @typedef {{ closed: boolean, points: Point[], cIn: Point[], cOut: Point[] }} Path
16
+ * @typedef {number[]} Matrix `[a, b, c, d, e, f]`, as canvas setTransform takes it
17
+ */
18
+
19
+ export const IDENTITY = [ 1, 0, 0, 1, 0, 0 ]
20
+
21
+ /**
22
+ * `left × right`: right applies first.
23
+ *
24
+ * @param {Matrix} left
25
+ * @param {Matrix} right
26
+ * @returns {Matrix}
27
+ */
28
+ export function multiply(left, right) {
29
+ return [
30
+ left[0] * right[0] + left[2] * right[1],
31
+ left[1] * right[0] + left[3] * right[1],
32
+ left[0] * right[2] + left[2] * right[3],
33
+ left[1] * right[2] + left[3] * right[3],
34
+ left[0] * right[4] + left[2] * right[5] + left[4],
35
+ left[1] * right[4] + left[3] * right[5] + left[5],
36
+ ]
37
+ }
38
+
39
+ /**
40
+ * @param {Matrix} m
41
+ * @param {number} x
42
+ * @param {number} y
43
+ * @returns {Point}
44
+ */
45
+ export function apply(m, x, y) {
46
+ return { x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] }
47
+ }
48
+
49
+ /**
50
+ * A Lottie transform's matrix: anchor, scale, skew, rotation, then position,
51
+ * in lottie-web's order.
52
+ *
53
+ * @param {number[]} anchor
54
+ * @param {number[]} position
55
+ * @param {number[]} scale percent
56
+ * @param {number} rotation degrees, clockwise
57
+ * @param {number} skew degrees
58
+ * @param {number} skewAxis degrees
59
+ * @returns {Matrix}
60
+ */
61
+ export function transformMatrix(anchor, position, scale, rotation, skew, skewAxis) {
62
+ let m = [ 1, 0, 0, 1, -anchor[0], -anchor[1] ]
63
+ m = multiply([ scale[0] / 100, 0, 0, scale[1] / 100, 0, 0 ], m)
64
+ if (skew) {
65
+ const axis = (skewAxis * Math.PI) / 180
66
+ const shear = Math.tan((-skew * Math.PI) / 180)
67
+ const cos = Math.cos(axis)
68
+ const sin = Math.sin(axis)
69
+ m = multiply([ cos, -sin, sin, cos, 0, 0 ], m)
70
+ m = multiply([ 1, 0, shear, 1, 0, 0 ], m)
71
+ m = multiply([ cos, sin, -sin, cos, 0, 0 ], m)
72
+ }
73
+ if (rotation) {
74
+ const angle = (rotation * Math.PI) / 180
75
+ m = multiply([ Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0 ], m)
76
+ }
77
+ return multiply([ 1, 0, 0, 1, position[0], position[1] ], m)
78
+ }
79
+
80
+ /**
81
+ * @param {Path} path
82
+ * @param {Matrix} m
83
+ * @returns {Path}
84
+ */
85
+ export function transformPath(path, m) {
86
+ const map = (/** @type {Point[]} */ list) => list.map((p) => apply(m, p.x, p.y))
87
+ return { closed: path.closed, points: map(path.points), cIn: map(path.cIn), cOut: map(path.cOut) }
88
+ }
89
+
90
+ /**
91
+ * Traces a path onto the context's current path.
92
+ *
93
+ * @param {CanvasRenderingContext2D} ctx
94
+ * @param {Path} path
95
+ */
96
+ export function trace(ctx, path) {
97
+ const { points, cIn, cOut } = path
98
+ if (!points.length) return
99
+
100
+ ctx.moveTo(points[0].x, points[0].y)
101
+ for (let k = 1; k < points.length; k++) {
102
+ ctx.bezierCurveTo(cOut[k - 1].x, cOut[k - 1].y, cIn[k].x, cIn[k].y, points[k].x, points[k].y)
103
+ }
104
+ if (path.closed) {
105
+ const last = points.length - 1
106
+ ctx.bezierCurveTo(cOut[last].x, cOut[last].y, cIn[0].x, cIn[0].y, points[0].x, points[0].y)
107
+ ctx.closePath()
108
+ }
109
+ }
110
+
111
+ // Shapes
112
+ // ■■■■■■
113
+
114
+ /**
115
+ * Lottie's own `{ v, i, o, c }`, tangents relative to their vertex.
116
+ *
117
+ * @param {any} shape
118
+ * @returns {Path}
119
+ */
120
+ export function fromLottie(shape) {
121
+ const points = []
122
+ const cIn = []
123
+ const cOut = []
124
+ const vertices = shape.v || []
125
+ for (let k = 0; k < vertices.length; k++) {
126
+ const v = vertices[k]
127
+ const i = (shape.i && shape.i[k]) || [ 0, 0 ]
128
+ const o = (shape.o && shape.o[k]) || [ 0, 0 ]
129
+ points.push({ x: v[0], y: v[1] })
130
+ cIn.push({ x: v[0] + i[0], y: v[1] + i[1] })
131
+ cOut.push({ x: v[0] + o[0], y: v[1] + o[1] })
132
+ }
133
+ return { closed: !!shape.c, points, cIn, cOut }
134
+ }
135
+
136
+ /**
137
+ * A rectangle centred on `position`, its corners rounded by `roundness`.
138
+ * Winds clockwise from the top right, or the other way when `reversed`.
139
+ *
140
+ * @param {number[]} position
141
+ * @param {number[]} size
142
+ * @param {number} roundness
143
+ * @param {boolean} reversed
144
+ * @returns {Path}
145
+ */
146
+ export function rectangle(position, size, roundness, reversed) {
147
+ const w = size[0] / 2
148
+ const h = size[1] / 2
149
+ const r = Math.min(roundness || 0, w, h)
150
+ const [ x, y ] = position
151
+ const c = r * (1 - KAPPA)
152
+
153
+ /** @type {[number, number, number, number, number, number][]} point, then in and out control offsets */
154
+ const corners = r
155
+ ? [
156
+ [ x + w, y - h + r, 0, -r + c, 0, 0 ],
157
+ [ x + w, y + h - r, 0, 0, 0, r - c ],
158
+ [ x + w - r, y + h, r - c, 0, 0, 0 ],
159
+ [ x - w + r, y + h, 0, 0, -(r - c), 0 ],
160
+ [ x - w, y + h - r, 0, r - c, 0, 0 ],
161
+ [ x - w, y - h + r, 0, 0, 0, -(r - c) ],
162
+ [ x - w + r, y - h, -(r - c), 0, 0, 0 ],
163
+ [ x + w - r, y - h, 0, 0, r - c, 0 ],
164
+ ]
165
+ : [ [ x + w, y - h, 0, 0, 0, 0 ], [ x + w, y + h, 0, 0, 0, 0 ], [ x - w, y + h, 0, 0, 0, 0 ], [ x - w, y - h, 0, 0, 0, 0 ] ]
166
+
167
+ const ordered = reversed ? reverseCorners(corners) : corners
168
+ return {
169
+ closed: true,
170
+ points: ordered.map((p) => ({ x: p[0], y: p[1] })),
171
+ cIn: ordered.map((p) => ({ x: p[0] + p[2], y: p[1] + p[3] })),
172
+ cOut: ordered.map((p) => ({ x: p[0] + p[4], y: p[1] + p[5] })),
173
+ }
174
+ }
175
+
176
+ /**
177
+ * The same corners the other way round: each point's in and out swap.
178
+ *
179
+ * @param {[number, number, number, number, number, number][]} corners
180
+ */
181
+ function reverseCorners(corners) {
182
+ const reversed = []
183
+ for (let k = corners.length - 1; k >= 0; k--) {
184
+ const p = corners[k]
185
+ reversed.push(/** @type {[number, number, number, number, number, number]} */ ([ p[0], p[1], p[4], p[5], p[2], p[3] ]))
186
+ }
187
+ return reversed
188
+ }
189
+
190
+ /**
191
+ * An ellipse centred on `position`, from its top, clockwise unless `reversed`.
192
+ *
193
+ * @param {number[]} position
194
+ * @param {number[]} size
195
+ * @param {boolean} reversed
196
+ * @returns {Path}
197
+ */
198
+ export function ellipse(position, size, reversed) {
199
+ const [ x, y ] = position
200
+ const rx = size[0] / 2
201
+ const ry = size[1] / 2
202
+ const kx = rx * KAPPA
203
+ const ky = ry * KAPPA
204
+ const direction = reversed ? -1 : 1
205
+
206
+ const points = [ { x, y: y - ry }, { x: x + direction * rx, y }, { x, y: y + ry }, { x: x - direction * rx, y } ]
207
+ const cIn = [ { x: x - direction * kx, y: y - ry }, { x: x + direction * rx, y: y - ky }, { x: x + direction * kx, y: y + ry }, { x: x - direction * rx, y: y + ky } ]
208
+ const cOut = [ { x: x + direction * kx, y: y - ry }, { x: x + direction * rx, y: y + ky }, { x: x - direction * kx, y: y + ry }, { x: x - direction * rx, y: y - ky } ]
209
+ return { closed: true, points, cIn, cOut }
210
+ }
211
+
212
+ /**
213
+ * A star (`sy` 1) or polygon (`sy` 2), as lottie-web builds them.
214
+ *
215
+ * @param {{ type: number, points: number, position: number[], rotation: number, outer: number, inner: number, outerRound: number, innerRound: number, reversed: boolean }} star
216
+ * @returns {Path}
217
+ */
218
+ export function star(star) {
219
+ const isStar = star.type === 1
220
+ const count = Math.floor(star.points) * (isStar ? 2 : 1)
221
+ const angleStep = (2 * Math.PI) / count
222
+ const direction = star.reversed ? -1 : 1
223
+ let angle = (star.rotation * Math.PI) / 180 - Math.PI / 2
224
+
225
+ const points = []
226
+ const cIn = []
227
+ const cOut = []
228
+ for (let k = 0; k < count; k++) {
229
+ const isOuter = !isStar || k % 2 === 0
230
+ const radius = isOuter ? star.outer : star.inner
231
+ const round = (isOuter ? star.outerRound : star.innerRound) / 100
232
+ const perimeter = (2 * Math.PI * radius) / (count * 2)
233
+ const x = star.position[0] + radius * Math.cos(angle)
234
+ const y = star.position[1] + radius * Math.sin(angle)
235
+ const tx = -Math.sin(angle) * perimeter * round * direction
236
+ const ty = Math.cos(angle) * perimeter * round * direction
237
+ points.push({ x, y })
238
+ cIn.push({ x: x - tx, y: y - ty })
239
+ cOut.push({ x: x + tx, y: y + ty })
240
+ angle += angleStep * direction
241
+ }
242
+ return { closed: true, points, cIn, cOut }
243
+ }
244
+
245
+ // Trimming
246
+ // ■■■■■■■■
247
+
248
+ /**
249
+ * The segments of a path, each as its four bezier points.
250
+ *
251
+ * @param {Path} path
252
+ * @returns {Point[][]}
253
+ */
254
+ function segments(path) {
255
+ const list = []
256
+ const { points, cIn, cOut } = path
257
+ for (let k = 1; k < points.length; k++) list.push([ points[k - 1], cOut[k - 1], cIn[k], points[k] ])
258
+ if (path.closed && points.length > 1) {
259
+ const last = points.length - 1
260
+ list.push([ points[last], cOut[last], cIn[0], points[0] ])
261
+ }
262
+ return list
263
+ }
264
+
265
+ /**
266
+ * @param {Point[]} s four bezier points
267
+ * @param {number} t
268
+ */
269
+ function pointAt(s, t) {
270
+ const u = 1 - t
271
+ return {
272
+ x: u * u * u * s[0].x + 3 * u * u * t * s[1].x + 3 * u * t * t * s[2].x + t * t * t * s[3].x,
273
+ y: u * u * u * s[0].y + 3 * u * u * t * s[1].y + 3 * u * t * t * s[2].y + t * t * t * s[3].y,
274
+ }
275
+ }
276
+
277
+ /** Samples per segment when measuring length */
278
+ const STEPS = 24
279
+
280
+ /**
281
+ * @param {Point[]} s
282
+ * @returns {number[]} cumulative length at each of STEPS + 1 samples
283
+ */
284
+ function lengths(s) {
285
+ const table = [ 0 ]
286
+ let previous = s[0]
287
+ for (let n = 1; n <= STEPS; n++) {
288
+ const point = pointAt(s, n / STEPS)
289
+ const dx = point.x - previous.x
290
+ const dy = point.y - previous.y
291
+ table.push(table[n - 1] + Math.sqrt(dx * dx + dy * dy))
292
+ previous = point
293
+ }
294
+ return table
295
+ }
296
+
297
+ /**
298
+ * The bezier parameter at a distance along a segment.
299
+ *
300
+ * @param {number[]} table from lengths()
301
+ * @param {number} distance
302
+ */
303
+ function parameterAt(table, distance) {
304
+ const total = table[table.length - 1]
305
+ if (distance <= 0 || total === 0) return 0
306
+ if (distance >= total) return 1
307
+
308
+ for (let n = 1; n < table.length; n++) {
309
+ if (table[n] >= distance) {
310
+ const span = table[n] - table[n - 1]
311
+ const within = span > 0 ? (distance - table[n - 1]) / span : 0
312
+ return (n - 1 + within) / STEPS
313
+ }
314
+ }
315
+ return 1
316
+ }
317
+
318
+ /**
319
+ * The part of a segment between two parameters, by de Casteljau.
320
+ *
321
+ * @param {Point[]} s
322
+ * @param {number} t0
323
+ * @param {number} t1
324
+ * @returns {Point[]}
325
+ */
326
+ function subSegment(s, t0, t1) {
327
+ const split = (/** @type {Point[]} */ q, /** @type {number} */ t) => {
328
+ const mix = (/** @type {Point} */ a, /** @type {Point} */ b) => ({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t })
329
+ const ab = mix(q[0], q[1])
330
+ const bc = mix(q[1], q[2])
331
+ const cd = mix(q[2], q[3])
332
+ const abc = mix(ab, bc)
333
+ const bcd = mix(bc, cd)
334
+ const mid = mix(abc, bcd)
335
+ return { before: [ q[0], ab, abc, mid ], after: [ mid, bcd, cd, q[3] ] }
336
+ }
337
+ const tail = split(s, t0).after
338
+ const span = t0 < 1 ? (t1 - t0) / (1 - t0) : 0
339
+ return split(tail, Math.min(1, Math.max(0, span))).before
340
+ }
341
+
342
+ /**
343
+ * The stretch of a path between two distances along it, as an open path.
344
+ *
345
+ * @param {Point[][]} list the path's segments
346
+ * @param {number[][]} tables each segment's lengths()
347
+ * @param {number} from
348
+ * @param {number} to
349
+ * @returns {Path | null}
350
+ */
351
+ function between(list, tables, from, to) {
352
+ /** @type {Path} */
353
+ const path = { closed: false, points: [], cIn: [], cOut: [] }
354
+ let offset = 0
355
+ for (let n = 0; n < list.length; n++) {
356
+ const length = tables[n][tables[n].length - 1]
357
+ const start = Math.max(from, offset)
358
+ const end = Math.min(to, offset + length)
359
+ if (end > start || (length === 0 && start === offset && start <= to && start >= from)) {
360
+ const piece = subSegment(list[n], parameterAt(tables[n], start - offset), parameterAt(tables[n], end - offset))
361
+ if (!path.points.length) {
362
+ path.points.push(piece[0])
363
+ path.cIn.push(piece[0])
364
+ }
365
+ path.cOut[path.points.length - 1] = piece[1]
366
+ path.points.push(piece[3])
367
+ path.cIn.push(piece[2])
368
+ path.cOut.push(piece[3])
369
+ }
370
+ offset += length
371
+ }
372
+ return path.points.length > 1 ? path : null
373
+ }
374
+
375
+ /**
376
+ * Trim Paths: the part of each path between `start` and `end`, percent of its
377
+ * length, shifted by `offset` degrees (360 is the whole length). `together`
378
+ * (`m` 2) treats every path as one, in order; otherwise each is trimmed alone.
379
+ *
380
+ * @param {Path[]} paths
381
+ * @param {number} start
382
+ * @param {number} end
383
+ * @param {number} offset
384
+ * @param {boolean} together
385
+ * @returns {Path[][]} what each path keeps, in the same order
386
+ */
387
+ export function trimEach(paths, start, end, offset, together) {
388
+ let s = Math.min(start, end) / 100
389
+ let e = Math.max(start, end) / 100
390
+ if (e - s >= 1) return paths.map((path) => [ path ])
391
+ if (e === s) return paths.map(() => [])
392
+
393
+ const shift = (offset % 360) / 360
394
+ s += shift
395
+ e += shift
396
+ // the range may run past the end and wrap round to the start
397
+ const ranges = s >= 1 || s < 0 ? wrapRanges(s, e) : e > 1 ? [ [ s, 1 ], [ 0, e - 1 ] ] : [ [ s, e ] ]
398
+
399
+ const measured = paths.map((path) => {
400
+ const list = segments(path)
401
+ const tables = list.map(lengths)
402
+ let total = 0
403
+ for (let n = 0; n < tables.length; n++) total += tables[n][tables[n].length - 1]
404
+ return { list, tables, total }
405
+ })
406
+
407
+ let combined = 0
408
+ for (let n = 0; n < measured.length; n++) combined += measured[n].total
409
+
410
+ const kept = []
411
+ let before = 0
412
+ for (let n = 0; n < measured.length; n++) {
413
+ const m = measured[n]
414
+ // alone, a path's range is its own length; together, a share of the combined one
415
+ const scale = together ? combined : m.total
416
+ const origin = together ? before : 0
417
+ const pieces = []
418
+ for (let r = 0; r < ranges.length; r++) {
419
+ const from = ranges[r][0] * scale - origin
420
+ const to = ranges[r][1] * scale - origin
421
+ if (to <= 0 || from >= m.total) continue
422
+ const piece = between(m.list, m.tables, Math.max(0, from), Math.min(m.total, to))
423
+ if (piece) pieces.push(piece)
424
+ }
425
+ kept.push(pieces)
426
+ before += m.total
427
+ }
428
+ return kept
429
+ }
430
+
431
+ /**
432
+ * The same trim, every path's pieces in one list.
433
+ *
434
+ * @param {Path[]} paths
435
+ * @param {number} start
436
+ * @param {number} end
437
+ * @param {number} offset
438
+ * @param {boolean} together
439
+ * @returns {Path[]}
440
+ */
441
+ export function trim(paths, start, end, offset, together) {
442
+ const kept = trimEach(paths, start, end, offset, together)
443
+ const flat = []
444
+ for (let n = 0; n < kept.length; n++) for (let k = 0; k < kept[n].length; k++) flat.push(kept[n][k])
445
+ return flat
446
+ }
447
+
448
+ /**
449
+ * A range shifted wholly outside 0-1, folded back into it.
450
+ *
451
+ * @param {number} s
452
+ * @param {number} e
453
+ */
454
+ function wrapRanges(s, e) {
455
+ const shift = Math.floor(s)
456
+ const a = s - shift
457
+ const b = e - shift
458
+ return b > 1 ? [ [ a, 1 ], [ 0, b - 1 ] ] : [ [ a, b ] ]
459
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Lottie properties as functions of the frame: a static value, or keyframes
3
+ * eased by their cubic beziers. A property carrying a slot id (`sid`) reads
4
+ * the live slot table first, so data bindings change what draws without
5
+ * recompiling the animation.
6
+ *
7
+ * Both keyframe forms are read: the current one, where a segment ends at the
8
+ * next keyframe's `s`, and the older one lottie-roku's sources still use,
9
+ * where a keyframe carries its own end value in `e`.
10
+ */
11
+
12
+ import { mixValues } from '../mixer/mixer.mix.js'
13
+
14
+ /**
15
+ * @typedef {(frame: number) => any} Evaluator
16
+ */
17
+
18
+ /**
19
+ * Live slot values, by slot id; `undefined` leaves a property to its keyframes.
20
+ * @typedef {Record<string, any>} Slots
21
+ */
22
+
23
+ /**
24
+ * @param {any} property a Lottie property: `{ a, k }`, optionally with `sid`
25
+ * @param {Slots} slots read on every call, so changing it changes the result
26
+ * @param {any} [fallback] the value when the property is absent
27
+ * @returns {Evaluator}
28
+ */
29
+ export function evaluator(property, slots, fallback) {
30
+ if (!property) return () => fallback
31
+
32
+ const keyed = property.pz ? posedEvaluator(property, keyedEvaluator(property, fallback), slots) : keyedEvaluator(property, fallback)
33
+ const sid = property.sid
34
+ if (typeof sid !== 'string') return keyed
35
+
36
+ return (frame) => {
37
+ const live = slots[sid]
38
+ return live === undefined ? keyed(frame) : live
39
+ }
40
+ }
41
+
42
+ /**
43
+ * A property of a nested machine that plays several animations at once. The
44
+ * machine says which it plays, as `{ start, frame, weight }` in the slot
45
+ * `pz:<id>`: the first frame of the animation's segment on the stitched
46
+ * timeline, how far in it is, and its weight. The value is the property's at
47
+ * the frame the machine's time remap gives, with each pose that keys it
48
+ * mixed in by its weight, in order, as Rive applies animations; one that
49
+ * does not key it leaves it alone. Without poses it is the keyframes.
50
+ *
51
+ * @param {any} property `pz` is `{ n, k }`: the machine's id, and the segments that key the property
52
+ * @param {Evaluator} keyed
53
+ * @param {Slots} slots
54
+ * @returns {Evaluator}
55
+ */
56
+ function posedEvaluator(property, keyed, slots) {
57
+ const key = 'pz:' + property.pz.n
58
+ const keying = property.pz.k
59
+ return (frame) => {
60
+ const poses = slots[key]
61
+ const value = keyed(frame)
62
+ if (!poses) return value
63
+
64
+ let mixed = value
65
+ for (let n = 0; n < poses.length; n++) {
66
+ const pose = poses[n]
67
+ if (keying.indexOf(pose.start) < 0) continue
68
+ mixed = mixValues(mixed, keyed(pose.start + pose.frame), pose.weight)
69
+ }
70
+ return mixed
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Whether a property's value changes over time.
76
+ *
77
+ * @param {any} property
78
+ */
79
+ export function isAnimated(property) {
80
+ return !!property && property.a === 1 && Array.isArray(property.k) && property.k.length > 0 && typeof property.k[0] === 'object' && property.k[0] !== null && 't' in property.k[0]
81
+ }
82
+
83
+ /**
84
+ * @param {any} property
85
+ * @param {any} fallback
86
+ * @returns {Evaluator}
87
+ */
88
+ function keyedEvaluator(property, fallback) {
89
+ // the pose mixer replaces a keyed property's keyframes with its own evaluator
90
+ if (typeof property.mx === 'function') return property.mx
91
+
92
+ if (!isAnimated(property)) {
93
+ const value = property.k === undefined ? fallback : property.k
94
+ return () => value
95
+ }
96
+
97
+ const keyframes = property.k
98
+ return (frame) => valueAt(keyframes, frame)
99
+ }
100
+
101
+ /**
102
+ * @param {any[]} keyframes
103
+ * @param {number} frame
104
+ * @returns {any}
105
+ */
106
+ function valueAt(keyframes, frame) {
107
+ const first = keyframes[0]
108
+ if (frame <= first.t || keyframes.length === 1) return startValue(first)
109
+
110
+ for (let n = 0; n < keyframes.length - 1; n++) {
111
+ const from = keyframes[n]
112
+ const to = keyframes[n + 1]
113
+ if (frame >= to.t) continue
114
+
115
+ if (from.h === 1) return startValue(from)
116
+ const end = from.e !== undefined ? from.e : startValue(to)
117
+ const span = to.t - from.t
118
+ const progress = span > 0 ? (frame - from.t) / span : 1
119
+ return blend(startValue(from), end, from, progress)
120
+ }
121
+
122
+ const last = keyframes[keyframes.length - 1]
123
+ // an old-form last keyframe may hold no `s`; the one before ends where it ends
124
+ if (last.s === undefined) {
125
+ const before = keyframes[keyframes.length - 2]
126
+ return before.e !== undefined ? before.e : startValue(before)
127
+ }
128
+ return startValue(last)
129
+ }
130
+
131
+ /** @param {any} keyframe */
132
+ function startValue(keyframe) {
133
+ return keyframe.s
134
+ }
135
+
136
+ /**
137
+ * Eases each dimension by its own bezier when the keyframe gives one per
138
+ * dimension, else all by the same one.
139
+ *
140
+ * @param {any} from
141
+ * @param {any} to
142
+ * @param {any} keyframe the segment's first keyframe, holding `o` and `i`
143
+ * @param {number} progress 0-1 through the segment
144
+ */
145
+ function blend(from, to, keyframe, progress) {
146
+ if (isPath(from)) return blendPath(from, to, easeAt(keyframe, 0, progress))
147
+ if (!Array.isArray(from)) return lerp(from, to, easeAt(keyframe, 0, progress))
148
+
149
+ const result = []
150
+ for (let d = 0; d < from.length; d++) {
151
+ const target = Array.isArray(to) && d < to.length ? to[d] : from[d]
152
+ result.push(lerp(from[d], target, easeAt(keyframe, d, progress)))
153
+ }
154
+ return result
155
+ }
156
+
157
+ /**
158
+ * @param {any} value
159
+ */
160
+ function isPath(value) {
161
+ return Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' && value[0] !== null && Array.isArray(value[0].v)
162
+ }
163
+
164
+ /**
165
+ * A shape keyframe's value is `[{ i, o, v, c }]`; its vertices blend pairwise.
166
+ *
167
+ * @param {any[]} from
168
+ * @param {any[]} to
169
+ * @param {number} t
170
+ */
171
+ function blendPath(from, to, t) {
172
+ const a = from[0]
173
+ const b = Array.isArray(to) && to[0] ? to[0] : a
174
+ return [ { c: a.c, v: blendPoints(a.v, b.v, t), i: blendPoints(a.i, b.i, t), o: blendPoints(a.o, b.o, t) } ]
175
+ }
176
+
177
+ /**
178
+ * @param {number[][]} a
179
+ * @param {number[][]} b
180
+ * @param {number} t
181
+ */
182
+ function blendPoints(a, b, t) {
183
+ const points = []
184
+ for (let n = 0; n < a.length; n++) {
185
+ const q = b[n] || a[n]
186
+ points.push([ lerp(a[n][0], q[0], t), lerp(a[n][1], q[1], t) ])
187
+ }
188
+ return points
189
+ }
190
+
191
+ /**
192
+ * @param {number} a
193
+ * @param {number} b
194
+ * @param {number} t
195
+ */
196
+ function lerp(a, b, t) {
197
+ return a + (b - a) * t
198
+ }
199
+
200
+ /**
201
+ * The eased progress for one dimension.
202
+ *
203
+ * @param {any} keyframe
204
+ * @param {number} dimension
205
+ * @param {number} progress
206
+ * @returns {number}
207
+ */
208
+ function easeAt(keyframe, dimension, progress) {
209
+ const out = keyframe.o
210
+ const into = keyframe.i
211
+ if (!out || !into) return progress
212
+
213
+ const pick = (/** @type {any} */ value) => (Array.isArray(value) ? (dimension < value.length ? value[dimension] : value[0]) : value)
214
+ return cubicBezier(pick(out.x), pick(out.y), pick(into.x), pick(into.y), progress)
215
+ }
216
+
217
+ /**
218
+ * The y of a CSS-style cubic bezier easing curve (0,0)-(x1,y1)-(x2,y2)-(1,1)
219
+ * at x: Newton's method on x, bisection when it stalls.
220
+ *
221
+ * @param {number} x1
222
+ * @param {number} y1
223
+ * @param {number} x2
224
+ * @param {number} y2
225
+ * @param {number} x
226
+ * @returns {number}
227
+ */
228
+ export function cubicBezier(x1, y1, x2, y2, x) {
229
+ const isLinear = x1 === y1 && x2 === y2
230
+ if (isLinear || x <= 0 || x >= 1) return x <= 0 ? 0 : x >= 1 ? 1 : x
231
+
232
+ const curve = (/** @type {number} */ t, /** @type {number} */ p1, /** @type {number} */ p2) => 3 * (1 - t) * (1 - t) * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t
233
+ const slope = (/** @type {number} */ t, /** @type {number} */ p1, /** @type {number} */ p2) => 3 * (1 - t) * (1 - t) * p1 + 6 * (1 - t) * t * (p2 - p1) + 3 * t * t * (1 - p2)
234
+
235
+ let t = x
236
+ for (let n = 0; n < 8; n++) {
237
+ const error = curve(t, x1, x2) - x
238
+ if (Math.abs(error) < 1e-6) return curve(t, y1, y2)
239
+ const d = slope(t, x1, x2)
240
+ if (Math.abs(d) < 1e-6) break
241
+ t -= error / d
242
+ }
243
+
244
+ let low = 0
245
+ let high = 1
246
+ t = x
247
+ for (let n = 0; n < 40; n++) {
248
+ const value = curve(t, x1, x2)
249
+ if (Math.abs(value - x) < 1e-6) break
250
+ if (value < x) low = t
251
+ else high = t
252
+ t = (low + high) / 2
253
+ }
254
+ return curve(t, y1, y2)
255
+ }