@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
package/bands/bands.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout bands that blend. Where the converter says a band's layout can be
|
|
3
|
+
* interpolated up to the next band (`lerp`: nothing wraps, clamps or appears
|
|
4
|
+
* between their widths), every laid-out box's position and size is a straight
|
|
5
|
+
* line in the width, so a canvas between the two bands draws the mix of
|
|
6
|
+
* their variants, weighted `t = (w - w0) / (w1 - w0)`.
|
|
7
|
+
*
|
|
8
|
+
* Rive's `layout` fit sizes the artboard to the whole canvas, so a layout
|
|
9
|
+
* can depend on the height too, and the converter then writes a grid: a row
|
|
10
|
+
* of the same bands for each height band. Inside a cell of the grid where
|
|
11
|
+
* nothing reflows (`cell`) every box is affine in both the width and the
|
|
12
|
+
* height, so the canvas draws the bilinear mix of the cell's four corners,
|
|
13
|
+
* weighted (1 - u)(1 - v), u(1 - v), (1 - u)v and uv, `u` being how far the
|
|
14
|
+
* width is across the cell and `v` the height. `cellOf` finds the cell.
|
|
15
|
+
*
|
|
16
|
+
* `linkBands` does that by giving each corner's variant of an animation, per
|
|
17
|
+
* property it shares with its neighbours', an evaluator that blends their
|
|
18
|
+
* values by two live weights. The animation is then drawn as always (alone,
|
|
19
|
+
* or as the pose mixer's carrier or source), and at `u = v = 0` is exactly
|
|
20
|
+
* the corner's own. Properties tagged `rid` match between bands the way they
|
|
21
|
+
* match between the animations of one artboard.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { evaluator } from '../lottie/lottie.props.js'
|
|
25
|
+
import { mixValues } from '../mixer/mixer.mix.js'
|
|
26
|
+
|
|
27
|
+
/** @import { Evaluator } from '../lottie/lottie.props.js' */
|
|
28
|
+
|
|
29
|
+
/** No slots: a property's keyframes, with the drawn animation's own slots going over the result */
|
|
30
|
+
const NO_SLOTS = {}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} Blend
|
|
34
|
+
* @property {any} band the band the canvas has room for
|
|
35
|
+
* @property {any | null} next the band after it, when the canvas is inside a segment that blends
|
|
36
|
+
* @property {number} t next's weight, 0 to 1; 0 without a next
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Where a canvas width falls among the bands: the widest band it has room
|
|
41
|
+
* for (the narrowest when it has room for none), and how far it is toward
|
|
42
|
+
* the next, when that segment can be interpolated.
|
|
43
|
+
*
|
|
44
|
+
* @param {any[] | undefined} bands a bundle's `bands`, narrowest first
|
|
45
|
+
* @param {number} width the canvas's width in CSS pixels
|
|
46
|
+
* @returns {Blend | null} null without bands
|
|
47
|
+
*/
|
|
48
|
+
export function blendOf(bands, width) {
|
|
49
|
+
if (!bands || !bands.length) return null
|
|
50
|
+
let at = 0
|
|
51
|
+
for (let n = 1; n < bands.length; n++) if (bands[n].width <= width) at = n
|
|
52
|
+
|
|
53
|
+
const band = bands[at]
|
|
54
|
+
const next = bands[at + 1]
|
|
55
|
+
if (!band.lerp || !next || width <= band.width) return { band, next: null, t: 0 }
|
|
56
|
+
return { band, next, t: Math.min(1, (width - band.width) / (next.width - band.width)) }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A band of the layout grid, at one width and height.
|
|
61
|
+
*
|
|
62
|
+
* @typedef {object} Corner
|
|
63
|
+
* @property {number} width
|
|
64
|
+
* @property {number} height
|
|
65
|
+
* @property {Record<string, string> | undefined} animations the animation that stands for each of the bundle's own here; none at the artboard's own size
|
|
66
|
+
* @property {boolean} lerp the segment to the next, wider corner in its row blends
|
|
67
|
+
* @property {boolean} rise the segment to the next, taller corner in its column blends
|
|
68
|
+
* @property {boolean} cell the whole rectangle up to the diagonal corner blends bilinearly
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The layout grid of a bundle: its bands as one row, or the rows of its
|
|
73
|
+
* `grid` when the layout depends on the height.
|
|
74
|
+
*
|
|
75
|
+
* @typedef {object} Grid
|
|
76
|
+
* @property {Corner[][]} rows shortest first, each row narrowest first; every row holds the same widths
|
|
77
|
+
* @property {[number, number] | null} range the heights the layout was solved over, when the converter says
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {any} manifest
|
|
82
|
+
* @returns {Grid | null} null without bands
|
|
83
|
+
*/
|
|
84
|
+
export function gridOf(manifest) {
|
|
85
|
+
const bands = manifest.bands
|
|
86
|
+
if (!bands || !bands.length) return null
|
|
87
|
+
|
|
88
|
+
const range = manifest.heightRange && manifest.heightRange.length === 2 ? /** @type {[number, number]} */ ([ manifest.heightRange[0], manifest.heightRange[1] ]) : null
|
|
89
|
+
const listed = manifest.grid && manifest.grid.rows
|
|
90
|
+
if (listed && listed.length) {
|
|
91
|
+
const rows = []
|
|
92
|
+
for (let j = 0; j < listed.length; j++) {
|
|
93
|
+
const row = []
|
|
94
|
+
for (let i = 0; i < listed[j].bands.length; i++) row.push(cornerOf(listed[j].bands[i], listed[j].height))
|
|
95
|
+
rows.push(row)
|
|
96
|
+
}
|
|
97
|
+
return { rows, range }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const row = []
|
|
101
|
+
for (let i = 0; i < bands.length; i++) row.push(cornerOf(bands[i], bands[i].height || 0))
|
|
102
|
+
return { rows: [ row ], range }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {any} band
|
|
107
|
+
* @param {number} height
|
|
108
|
+
* @returns {Corner}
|
|
109
|
+
*/
|
|
110
|
+
function cornerOf(band, height) {
|
|
111
|
+
return { width: band.width, height, animations: band.animations, lerp: !!band.lerp, rise: !!band.rise, cell: !!band.cell }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Where a canvas falls in the layout grid.
|
|
116
|
+
*
|
|
117
|
+
* @typedef {object} Cell
|
|
118
|
+
* @property {Corner} corner the corner the canvas has room for: the widest, tallest band that fits, else the narrowest, shortest
|
|
119
|
+
* @property {Corner|null} right the next, wider corner, when the canvas is inside a segment that blends in the width
|
|
120
|
+
* @property {Corner|null} up the next, taller corner, when it is inside one that blends in the height
|
|
121
|
+
* @property {Corner|null} diag the corner across, when it is inside a cell that blends in both
|
|
122
|
+
* @property {number} u how far across to `right`, 0 to 1; 0 without one
|
|
123
|
+
* @property {number} v how far up to `up`, 0 to 1; 0 without one
|
|
124
|
+
* @property {number} width the artboard's width: the canvas's where the layout is exact for it, else 0 for the picture's own
|
|
125
|
+
* @property {number} height the artboard's height, likewise
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The cell of the grid for a canvas size: the corner the canvas has room for,
|
|
130
|
+
* the corners it blends toward, and the weights, where the converter says the
|
|
131
|
+
* segments or cell can be interpolated. A canvas past the grid is the edge
|
|
132
|
+
* corner, scaled by the fit. Where the layout does not depend on the height
|
|
133
|
+
* (one row) and the converter says what range it checked, the artboard is the
|
|
134
|
+
* canvas's height within that range, as Rive sizes it.
|
|
135
|
+
*
|
|
136
|
+
* @param {Grid} grid
|
|
137
|
+
* @param {number} width the canvas's width in CSS pixels
|
|
138
|
+
* @param {number} height its height
|
|
139
|
+
* @returns {Cell}
|
|
140
|
+
*/
|
|
141
|
+
export function cellOf(grid, width, height) {
|
|
142
|
+
const rows = grid.rows
|
|
143
|
+
let j = 0
|
|
144
|
+
for (let n = 1; n < rows.length; n++) if (rows[n][0].height <= height) j = n
|
|
145
|
+
const row = rows[j]
|
|
146
|
+
let i = 0
|
|
147
|
+
for (let n = 1; n < row.length; n++) if (row[n].width <= width) i = n
|
|
148
|
+
|
|
149
|
+
const corner = row[i]
|
|
150
|
+
const right = row[i + 1] || null
|
|
151
|
+
const up = rows[j + 1] ? rows[j + 1][i] : null
|
|
152
|
+
const isU = !!right && corner.lerp && width > corner.width
|
|
153
|
+
const isV = !!up && corner.rise && height > corner.height
|
|
154
|
+
|
|
155
|
+
// both ways only inside a cell the converter checked; a cell that reflows plays its corner
|
|
156
|
+
const isBoth = isU && isV
|
|
157
|
+
const canU = isU && (!isBoth || corner.cell)
|
|
158
|
+
const canV = isV && (!isBoth || corner.cell)
|
|
159
|
+
const u = canU ? Math.min(1, (width - corner.width) / (/** @type {Corner} */ (right).width - corner.width)) : 0
|
|
160
|
+
const v = canV ? Math.min(1, (height - corner.height) / (/** @type {Corner} */ (up).height - corner.height)) : 0
|
|
161
|
+
|
|
162
|
+
let artboardHeight = 0
|
|
163
|
+
if (canV) artboardHeight = height
|
|
164
|
+
else if (rows.length === 1 && grid.range) artboardHeight = Math.max(grid.range[0], Math.min(grid.range[1], height))
|
|
165
|
+
return {
|
|
166
|
+
corner,
|
|
167
|
+
right: canU ? right : null,
|
|
168
|
+
up: canV ? up : null,
|
|
169
|
+
diag: canU && canV ? rows[j + 1][i + 1] : null,
|
|
170
|
+
u,
|
|
171
|
+
v,
|
|
172
|
+
width: canU ? width : 0,
|
|
173
|
+
height: artboardHeight,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Whether any band of a bundle blends into a neighbour.
|
|
179
|
+
*
|
|
180
|
+
* @param {any} manifest
|
|
181
|
+
* @returns {boolean}
|
|
182
|
+
*/
|
|
183
|
+
export function hasBlend(manifest) {
|
|
184
|
+
const bands = manifest.bands || []
|
|
185
|
+
for (let n = 0; n < bands.length - 1; n++) if (bands[n].lerp) return true
|
|
186
|
+
const rows = (manifest.grid && manifest.grid.rows) || []
|
|
187
|
+
for (let j = 0; j < rows.length; j++) {
|
|
188
|
+
for (let i = 0; i < rows[j].bands.length; i++) if (rows[j].bands[i].lerp || rows[j].bands[i].rise) return true
|
|
189
|
+
}
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Links every blending corner's variants to its neighbours'. Call it with all
|
|
195
|
+
* the bundle's parsed Lotties loaded and none yet drawn or mixed.
|
|
196
|
+
*
|
|
197
|
+
* @param {any} manifest
|
|
198
|
+
* @param {Record<string, any>} lotties parsed Lottie JSON by animation id, changed in place
|
|
199
|
+
* @param {{ t: number, v?: number }} live the weights toward the next, wider corner (`t`) and the next, taller one (`v`); set them before a draw
|
|
200
|
+
*/
|
|
201
|
+
export function linkBands(manifest, lotties, live) {
|
|
202
|
+
const grid = gridOf(manifest)
|
|
203
|
+
if (!grid) return
|
|
204
|
+
const ids = baseIds(manifest)
|
|
205
|
+
|
|
206
|
+
/** @param {Corner | undefined} from @param {boolean} on @returns {Corner | null} */
|
|
207
|
+
const neighbour = (from, on) => (on && from ? from : null)
|
|
208
|
+
|
|
209
|
+
// every target's values are read before any is linked, since a target links onward itself
|
|
210
|
+
/** @type {Record<string, Record<string, { property: any, at: Evaluator }>>} */
|
|
211
|
+
const targets = {}
|
|
212
|
+
/** @type {{ corner: Corner, right: Corner | null, up: Corner | null, diag: Corner | null }[]} */
|
|
213
|
+
const links = []
|
|
214
|
+
const rows = grid.rows
|
|
215
|
+
for (let j = 0; j < rows.length; j++) {
|
|
216
|
+
for (let i = 0; i < rows[j].length; i++) {
|
|
217
|
+
const corner = rows[j][i]
|
|
218
|
+
const right = neighbour(rows[j][i + 1], corner.lerp || corner.cell)
|
|
219
|
+
const up = neighbour(rows[j + 1] && rows[j + 1][i], corner.rise || corner.cell)
|
|
220
|
+
const diag = neighbour(rows[j + 1] && rows[j + 1][i + 1], corner.cell)
|
|
221
|
+
if (!right && !up) continue
|
|
222
|
+
links.push({ corner, right, up, diag })
|
|
223
|
+
for (let k = 0; k < ids.length; k++) {
|
|
224
|
+
const others = [ right, up, diag ]
|
|
225
|
+
for (let m = 0; m < others.length; m++) {
|
|
226
|
+
const other = others[m]
|
|
227
|
+
if (!other) continue
|
|
228
|
+
const id = variantId(other, ids[k])
|
|
229
|
+
if (lotties[id] && !targets[id]) targets[id] = collect(lotties[id])
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (let n = 0; n < links.length; n++) {
|
|
236
|
+
const entry = links[n]
|
|
237
|
+
for (let k = 0; k < ids.length; k++) {
|
|
238
|
+
const from = lotties[variantId(entry.corner, ids[k])]
|
|
239
|
+
if (!from) continue
|
|
240
|
+
const at = (/** @type {Corner | null} */ other) => (other ? targets[variantId(other, ids[k])] : undefined)
|
|
241
|
+
link(from, { right: at(entry.right), up: at(entry.up), diag: at(entry.diag) }, live)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @typedef {Record<string, { property: any, at: Evaluator }>} Targets
|
|
248
|
+
*/
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {any} lottie a corner's variant
|
|
252
|
+
* @param {{ right?: Targets, up?: Targets, diag?: Targets }} to the neighbours', by `rid`
|
|
253
|
+
* @param {{ t: number, v?: number }} live
|
|
254
|
+
*/
|
|
255
|
+
function link(lottie, to, live) {
|
|
256
|
+
visit(lottie, (property) => {
|
|
257
|
+
const right = differing(property, to.right)
|
|
258
|
+
const up = differing(property, to.up)
|
|
259
|
+
const diag = differing(property, to.diag)
|
|
260
|
+
if (!right && !up && !diag) return
|
|
261
|
+
|
|
262
|
+
const own = evaluator(property, NO_SLOTS)
|
|
263
|
+
property.mx = (/** @type {number} */ frame) => {
|
|
264
|
+
const value = own(frame)
|
|
265
|
+
const u = live.t
|
|
266
|
+
const v = live.v || 0
|
|
267
|
+
if (u > 0 && v > 0) {
|
|
268
|
+
// bilinear: along the bottom edge and the top one, then between them
|
|
269
|
+
const across = right ? mixValues(value, right(frame), u) : value
|
|
270
|
+
const lift = up ? up(frame) : value
|
|
271
|
+
const top = diag ? mixValues(lift, diag(frame), u) : lift
|
|
272
|
+
return mixValues(across, top, v)
|
|
273
|
+
}
|
|
274
|
+
if (u > 0) return right ? mixValues(value, right(frame), u) : value
|
|
275
|
+
if (v > 0) return up ? mixValues(value, up(frame), v) : value
|
|
276
|
+
return value
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* A neighbour's evaluator for a property, when its values differ from the property's.
|
|
283
|
+
*
|
|
284
|
+
* @param {any} property
|
|
285
|
+
* @param {Targets | undefined} targets
|
|
286
|
+
* @returns {Evaluator | null}
|
|
287
|
+
*/
|
|
288
|
+
function differing(property, targets) {
|
|
289
|
+
const other = targets && targets[property.rid]
|
|
290
|
+
return other && !isSame(property, other.property) ? other.at : null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Whether two properties hold the same values, which need no blending.
|
|
295
|
+
*
|
|
296
|
+
* @param {any} a
|
|
297
|
+
* @param {any} b
|
|
298
|
+
* @returns {boolean}
|
|
299
|
+
*/
|
|
300
|
+
function isSame(a, b) {
|
|
301
|
+
return a.a === b.a && JSON.stringify(a.k) === JSON.stringify(b.k)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Every tagged property of a Lottie by its `rid`; the first of a repeated one wins.
|
|
306
|
+
*
|
|
307
|
+
* @param {any} lottie
|
|
308
|
+
* @returns {Record<string, { property: any, at: Evaluator }>}
|
|
309
|
+
*/
|
|
310
|
+
function collect(lottie) {
|
|
311
|
+
/** @type {Record<string, { property: any, at: Evaluator }>} */
|
|
312
|
+
const found = {}
|
|
313
|
+
visit(lottie, (property) => {
|
|
314
|
+
if (!found[property.rid]) found[property.rid] = { property, at: evaluator(property, NO_SLOTS) }
|
|
315
|
+
})
|
|
316
|
+
return found
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Calls back for every tagged property under a node, in assets as in layers.
|
|
321
|
+
*
|
|
322
|
+
* @param {any} node
|
|
323
|
+
* @param {(property: any) => void} found
|
|
324
|
+
*/
|
|
325
|
+
function visit(node, found) {
|
|
326
|
+
if (node === null || typeof node !== 'object') return
|
|
327
|
+
if (Array.isArray(node)) {
|
|
328
|
+
for (let n = 0; n < node.length; n++) visit(node[n], found)
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
if (typeof node.rid === 'string' && 'k' in node) found(node)
|
|
332
|
+
for (const key in node) {
|
|
333
|
+
if (key === 'k' || key === 'rid') continue
|
|
334
|
+
visit(node[key], found)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Every band the manifest lists, those of the authored height and the grid's.
|
|
340
|
+
*
|
|
341
|
+
* @param {any} manifest
|
|
342
|
+
* @returns {any[]}
|
|
343
|
+
*/
|
|
344
|
+
function everyBand(manifest) {
|
|
345
|
+
const found = (manifest.bands || []).slice()
|
|
346
|
+
const rows = (manifest.grid && manifest.grid.rows) || []
|
|
347
|
+
for (let j = 0; j < rows.length; j++) for (let i = 0; i < rows[j].bands.length; i++) found.push(rows[j].bands[i])
|
|
348
|
+
return found
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The animations that are a layout band's variant of another, not an animation of the bundle's own.
|
|
353
|
+
*
|
|
354
|
+
* @param {any} manifest
|
|
355
|
+
* @returns {Record<string, boolean>}
|
|
356
|
+
*/
|
|
357
|
+
export function variantIds(manifest) {
|
|
358
|
+
/** @type {Record<string, boolean>} */
|
|
359
|
+
const found = {}
|
|
360
|
+
const bands = everyBand(manifest)
|
|
361
|
+
for (let n = 0; n < bands.length; n++) {
|
|
362
|
+
for (const id in bands[n].animations || {}) found[bands[n].animations[id]] = true
|
|
363
|
+
}
|
|
364
|
+
return found
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The bundle's own animations: those no band lists as its variant.
|
|
369
|
+
*
|
|
370
|
+
* @param {any} manifest
|
|
371
|
+
* @returns {string[]}
|
|
372
|
+
*/
|
|
373
|
+
function baseIds(manifest) {
|
|
374
|
+
const isVariant = variantIds(manifest)
|
|
375
|
+
const ids = []
|
|
376
|
+
for (const id in manifest.animations) if (!isVariant[id]) ids.push(id)
|
|
377
|
+
return ids
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* An animation's variant in a band: the animation itself in the band at the
|
|
382
|
+
* artboard's own size.
|
|
383
|
+
*
|
|
384
|
+
* @param {{ animations?: Record<string, string> }} band
|
|
385
|
+
* @param {string} id
|
|
386
|
+
* @returns {string}
|
|
387
|
+
*/
|
|
388
|
+
function variantId(band, id) {
|
|
389
|
+
return (band.animations && band.animations[id]) || id
|
|
390
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fonts a bundle's text overlays use, registered under a family name of
|
|
3
|
+
* their own so they never shadow the page's. FontFace where the browser has
|
|
4
|
+
* it, which says when the font is ready; an @font-face rule elsewhere, which
|
|
5
|
+
* the canvas picks up once the browser has fetched it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fileUrl } from './bundle.js'
|
|
9
|
+
|
|
10
|
+
/** @import { Bundle } from './bundle.js' */
|
|
11
|
+
|
|
12
|
+
/** Families registered so far, by URL, so a bundle loaded twice registers once */
|
|
13
|
+
/** @type {Record<string, string>} */
|
|
14
|
+
const registered = {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {Bundle} bundle
|
|
18
|
+
* @returns {Promise<Record<string, string>>} font name → the family to draw with
|
|
19
|
+
*/
|
|
20
|
+
export function loadFonts(bundle) {
|
|
21
|
+
/** @type {Record<string, string>} */
|
|
22
|
+
const families = {}
|
|
23
|
+
const pending = []
|
|
24
|
+
const fonts = bundle.manifest.fonts || {}
|
|
25
|
+
for (const name in fonts) {
|
|
26
|
+
const url = fileUrl(bundle, fonts[name])
|
|
27
|
+
const family = registered[url] || 'motion-' + Object.keys(registered).length + '-' + name.replace(/[^\w-]+/g, '_')
|
|
28
|
+
families[name] = family
|
|
29
|
+
if (registered[url]) continue
|
|
30
|
+
|
|
31
|
+
registered[url] = family
|
|
32
|
+
pending.push(register(family, url))
|
|
33
|
+
}
|
|
34
|
+
return Promise.all(pending).then(() => families)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} family
|
|
39
|
+
* @param {string} url
|
|
40
|
+
* @returns {Promise<void>}
|
|
41
|
+
*/
|
|
42
|
+
function register(family, url) {
|
|
43
|
+
const FontFaceClass = /** @type {any} */ (window).FontFace
|
|
44
|
+
const fontSet = /** @type {any} */ (document).fonts
|
|
45
|
+
if (FontFaceClass && fontSet) {
|
|
46
|
+
const face = new FontFaceClass(family, 'url("' + url + '")')
|
|
47
|
+
// a font that fails to load leaves its text in the fallback font
|
|
48
|
+
return face.load().then(() => fontSet.add(face), () => undefined)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const style = document.createElement('style')
|
|
52
|
+
style.appendChild(document.createTextNode('@font-face { font-family: "' + family + '"; src: url("' + url + '"); }'))
|
|
53
|
+
document.head.appendChild(style)
|
|
54
|
+
return Promise.resolve()
|
|
55
|
+
}
|
package/bundle/bundle.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bundle over HTTP: motion.json, then the files it names, each resolved
|
|
3
|
+
* against motion.json's own folder. XMLHttpRequest rather than fetch, which
|
|
4
|
+
* some of the WebViews this runs in predate.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} Bundle
|
|
9
|
+
* @property {any} manifest motion.json
|
|
10
|
+
* @property {string} base motion.json's folder, ending in "/"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} url motion.json's URL
|
|
15
|
+
* @returns {Promise<Bundle>}
|
|
16
|
+
*/
|
|
17
|
+
export function loadBundle(url) {
|
|
18
|
+
return loadJson(url).then((manifest) => {
|
|
19
|
+
const isReadable = manifest && typeof manifest.motion === 'number' && manifest.animations
|
|
20
|
+
if (!isReadable) throw new Error(url + ' is not a Motion bundle')
|
|
21
|
+
// a file stretched to hold a nested artboard's animation (for a player that cannot run the machine) loops and ends where its own keys do
|
|
22
|
+
for (const id in manifest.animations) {
|
|
23
|
+
const entry = manifest.animations[id]
|
|
24
|
+
if (entry && entry.own > 0 && entry.own < entry.frames) entry.frames = entry.own
|
|
25
|
+
}
|
|
26
|
+
return { manifest, base: url.replace(/[^/]*$/, '') }
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {Bundle} bundle
|
|
32
|
+
* @param {string} file relative to motion.json
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function fileUrl(bundle, file) {
|
|
36
|
+
return bundle.base + file.split('/').map(encodeURIComponent).join('/')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} url
|
|
41
|
+
* @returns {Promise<any>}
|
|
42
|
+
*/
|
|
43
|
+
export function loadJson(url) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const request = new XMLHttpRequest()
|
|
46
|
+
request.open('GET', url, true)
|
|
47
|
+
request.onload = () => {
|
|
48
|
+
const isOk = request.status >= 200 && request.status < 300
|
|
49
|
+
if (!isOk) {
|
|
50
|
+
reject(new Error('cannot load ' + url + ' (HTTP ' + request.status + ')'))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
resolve(JSON.parse(request.responseText))
|
|
55
|
+
} catch (error) {
|
|
56
|
+
reject(new Error(url + ' is not JSON: ' + /** @type {Error} */ (error).message))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
request.onerror = () => reject(new Error('cannot load ' + url))
|
|
60
|
+
request.send()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} url
|
|
66
|
+
* @returns {Promise<HTMLImageElement>}
|
|
67
|
+
*/
|
|
68
|
+
export function loadImage(url) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const image = new Image()
|
|
71
|
+
image.onload = () => resolve(image)
|
|
72
|
+
image.onerror = () => reject(new Error('cannot load ' + url))
|
|
73
|
+
image.src = url
|
|
74
|
+
})
|
|
75
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directional focus, as Rive moves it for the arrow keys and the
|
|
3
|
+
* up/down/left/right focus actions: from the focused node's centre to the
|
|
4
|
+
* stop in that direction with the lowest score, where a stop is in the
|
|
5
|
+
* direction when its centre is further that way, and
|
|
6
|
+
*
|
|
7
|
+
* score = distance along the direction + weight * distance across it
|
|
8
|
+
*
|
|
9
|
+
* The weight is 2 for up and down and 36 for left and right, fitted to what
|
|
10
|
+
* `@rive-app/canvas` chooses (the parity scorecard's `focus-*` fixtures and the
|
|
11
|
+
* tests here hold it); a tie goes to the stop earlier in Tab order.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** How much a unit across the direction costs, against a unit along it */
|
|
15
|
+
const ACROSS = { up: 2, down: 2, left: 36, right: 36 }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} direction up, down, left or right
|
|
19
|
+
* @param {{ x: number, y: number }} from the focused node's centre
|
|
20
|
+
* @param {string[]} stops the candidates, in Tab order
|
|
21
|
+
* @param {(rid: string) => ({ x: number, y: number } | null)} centerOf null for a node with no geometry drawn
|
|
22
|
+
* @returns {string | null} the stop to focus, or null when none lies that way
|
|
23
|
+
*/
|
|
24
|
+
export function pickInDirection(direction, from, stops, centerOf) {
|
|
25
|
+
const weight = ACROSS[/** @type {keyof typeof ACROSS} */ (direction)]
|
|
26
|
+
if (weight === undefined) return null
|
|
27
|
+
const isVertical = direction === 'up' || direction === 'down'
|
|
28
|
+
const sign = direction === 'down' || direction === 'right' ? 1 : -1
|
|
29
|
+
|
|
30
|
+
/** @type {string | null} */
|
|
31
|
+
let best = null
|
|
32
|
+
let bestScore = Infinity
|
|
33
|
+
for (let n = 0; n < stops.length; n++) {
|
|
34
|
+
const center = centerOf(stops[n])
|
|
35
|
+
if (!center) continue
|
|
36
|
+
const along = (isVertical ? center.y - from.y : center.x - from.x) * sign
|
|
37
|
+
const across = Math.abs(isVertical ? center.x - from.x : center.y - from.y)
|
|
38
|
+
if (along <= 0) continue
|
|
39
|
+
const score = along + weight * across
|
|
40
|
+
if (score < bestScore) {
|
|
41
|
+
bestScore = score
|
|
42
|
+
best = stops[n]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return best
|
|
46
|
+
}
|