@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,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
|
+
}
|