@px-petals/roku.motion-web 1.0.0 → 1.1.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/bands/bands.js +3 -1
- package/bundle/bundle.js +56 -0
- package/list/list.js +12 -1
- package/lottie/lottie.layers.js +64 -11
- package/lottie/lottie.paint.js +30 -0
- package/lottie/lottie.paths.js +19 -4
- package/lottie/lottie.props.js +18 -8
- package/lottie/lottie.shapes.js +2 -1
- package/mixer/mixer.js +31 -8
- package/mixer/mixer.mix.js +17 -0
- package/motion/motion.convert.js +32 -1
- package/motion/motion.flow.js +20 -2
- package/motion/motion.machine.js +28 -13
- package/motion/motion.values.js +13 -9
- package/motion.js +196 -20
- package/package.json +10 -16
- package/player/player.js +25 -5
- package/readme.md +78 -3
- package/script/luau/lib/base/base.js +202 -0
- package/script/luau/lib/base/base.table.js +189 -0
- package/script/luau/lib/lib.args.js +152 -0
- package/script/luau/lib/lib.js +23 -0
- package/script/luau/lib/math/math.bit32.js +144 -0
- package/script/luau/lib/math/math.js +288 -0
- package/script/luau/lib/math/math.vector.js +125 -0
- package/script/luau/lib/string/string.format.js +143 -0
- package/script/luau/lib/string/string.js +160 -0
- package/script/luau/lib/string/string.pattern.js +505 -0
- package/script/luau/load/load.js +362 -0
- package/script/luau/load/load.ops.js +79 -0
- package/script/luau/luau.convert.js +259 -0
- package/script/luau/luau.js +132 -0
- package/script/luau/luau.md +111 -0
- package/script/luau/vm/vm.js +167 -0
- package/script/luau/vm/vm.meta.js +401 -0
- package/script/luau/vm/vm.num.js +236 -0
- package/script/luau/vm/vm.run.js +842 -0
- package/script/luau/vm/vm.value.js +406 -0
- package/script/play/play.js +199 -0
- package/script/play/play.objects.js +295 -0
- package/script/play/play.scope.js +91 -0
- package/script/rive/api/api.statics.js +31 -0
- package/script/rive/api/api.type.js +188 -0
- package/script/rive/api/artboard/artboard.js +89 -0
- package/script/rive/api/assets/assets.js +72 -0
- package/script/rive/api/data/data.value.js +99 -0
- package/script/rive/api/draw/draw.contours.js +163 -0
- package/script/rive/api/draw/draw.measure.js +196 -0
- package/script/rive/api/draw/draw.paint.js +235 -0
- package/script/rive/api/draw/draw.path.js +153 -0
- package/script/rive/api/draw/draw.renderer.js +117 -0
- package/script/rive/api/math/math.color.js +96 -0
- package/script/rive/api/math/math.mat2d.js +180 -0
- package/script/rive/api/math/math.vector.js +121 -0
- package/script/rive/api/media/media.audio.js +113 -0
- package/script/rive/api/media/media.draw.js +73 -0
- package/script/rive/api/media/media.image.js +61 -0
- package/script/rive/api/media/media.mesh.js +50 -0
- package/script/rive/api/pointer/pointer.event.js +64 -0
- package/script/rive/api/pointer/pointer.route.js +69 -0
- package/script/rive/api/text/text.font.js +129 -0
- package/script/rive/api/text/text.js +278 -0
- package/script/rive/api/text/text.layout.js +259 -0
- package/script/rive/context/context.js +76 -0
- package/script/rive/context/context.memory.js +196 -0
- package/script/rive/context/context.viewmodel.js +302 -0
- package/script/rive/context/lists/lists.js +160 -0
- package/script/rive/context/lists/lists.values.js +281 -0
- package/script/rive/host/host.globals.js +44 -0
- package/script/rive/host/host.js +533 -0
- package/script/rive/host/host.node.js +45 -0
- package/script/rive/rive.js +23 -0
- package/script/rive/rive.md +180 -0
- package/script/script.js +11 -0
- package/skin/skin.deform.js +514 -0
- package/skin/skin.ik.js +260 -0
- package/skin/skin.js +297 -0
package/bands/bands.js
CHANGED
|
@@ -329,8 +329,10 @@ function visit(node, found) {
|
|
|
329
329
|
return
|
|
330
330
|
}
|
|
331
331
|
if (typeof node.rid === 'string' && 'k' in node) found(node)
|
|
332
|
+
// a property's `k` holds its values; a gradient's `g.k` is a property itself
|
|
333
|
+
const isProperty = 'a' in node || typeof node.rid === 'string'
|
|
332
334
|
for (const key in node) {
|
|
333
|
-
if (key === 'k' || key === 'rid') continue
|
|
335
|
+
if ((key === 'k' && isProperty) || key === 'rid') continue
|
|
334
336
|
visit(node[key], found)
|
|
335
337
|
}
|
|
336
338
|
}
|
package/bundle/bundle.js
CHANGED
|
@@ -61,6 +61,25 @@ export function loadJson(url) {
|
|
|
61
61
|
})
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} url
|
|
66
|
+
* @returns {Promise<Uint8Array>}
|
|
67
|
+
*/
|
|
68
|
+
export function loadBytes(url) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const request = new XMLHttpRequest()
|
|
71
|
+
request.open('GET', url, true)
|
|
72
|
+
request.responseType = 'arraybuffer'
|
|
73
|
+
request.onload = () => {
|
|
74
|
+
const isOk = request.status >= 200 && request.status < 300
|
|
75
|
+
if (isOk) resolve(new Uint8Array(request.response))
|
|
76
|
+
else reject(new Error('cannot load ' + url + ' (HTTP ' + request.status + ')'))
|
|
77
|
+
}
|
|
78
|
+
request.onerror = () => reject(new Error('cannot load ' + url))
|
|
79
|
+
request.send()
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
/**
|
|
65
84
|
* @param {string} url
|
|
66
85
|
* @returns {Promise<HTMLImageElement>}
|
|
@@ -73,3 +92,40 @@ export function loadImage(url) {
|
|
|
73
92
|
image.src = url
|
|
74
93
|
})
|
|
75
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* What the page gives Motion to run scripts with: `script/script.js`, or a
|
|
98
|
+
* function returning it (or a promise of it), so the page fetches it only
|
|
99
|
+
* when a bundle needs it: `() => import('.../script/script.js')`.
|
|
100
|
+
*
|
|
101
|
+
* @typedef {{ createScripts: Function } | (() => any)} ScriptProvider
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Loads the script runtime and the bundle's bytecode.
|
|
106
|
+
*
|
|
107
|
+
* @param {any} manifest a bundle's motion.json
|
|
108
|
+
* @param {ScriptProvider | undefined} provider
|
|
109
|
+
* @param {(file: string) => Promise<Uint8Array>} fetchBytes a file of the bundle, as bytes
|
|
110
|
+
* @param {(message: string) => void} report
|
|
111
|
+
* @returns {Promise<import('../script/play/play.js').Scripts | null>} null for a bundle with no scripts, or with no way to run them
|
|
112
|
+
*/
|
|
113
|
+
export function loadScripts(manifest, provider, fetchBytes, report) {
|
|
114
|
+
const files = manifest.scripts
|
|
115
|
+
if (!files) return Promise.resolve(null)
|
|
116
|
+
if (!provider) {
|
|
117
|
+
report('this bundle has scripts, which run only when the scripts option gives Motion the script host; its script conditions do not pass, and its script converters pass values through')
|
|
118
|
+
return Promise.resolve(null)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const ids = Object.keys(files)
|
|
122
|
+
const loading = Promise.resolve(typeof provider === 'function' ? provider() : provider)
|
|
123
|
+
return Promise.all([ loading, Promise.all(ids.map((id) => fetchBytes(files[id]))) ]).then((loaded) => {
|
|
124
|
+
/** @type {Record<string, Uint8Array>} */
|
|
125
|
+
const bytes = {}
|
|
126
|
+
ids.forEach((id, n) => {
|
|
127
|
+
bytes[id] = loaded[1][n]
|
|
128
|
+
})
|
|
129
|
+
return loaded[0].createScripts(manifest, bytes, report)
|
|
130
|
+
})
|
|
131
|
+
}
|
package/list/list.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { createAnimation } from '../lottie/lottie.js'
|
|
22
22
|
import { multiply } from '../lottie/lottie.paths.js'
|
|
23
23
|
import { createNested, extend } from '../motion/motion.nested.js'
|
|
24
|
-
import { slotValues } from '../motion/motion.values.js'
|
|
24
|
+
import { slotValues, sourceValue } from '../motion/motion.values.js'
|
|
25
25
|
import { layoutRows } from './list.flex.js'
|
|
26
26
|
import { itemValues, rowNameOf, signature } from './list.items.js'
|
|
27
27
|
|
|
@@ -54,6 +54,7 @@ function scopeOf(id, sid) {
|
|
|
54
54
|
* @property {() => number} band the width of the layout band showing, 0 for the file's own size
|
|
55
55
|
* @property {() => number} [blended] between two bands that blend, the canvas's width, else 0: the box is then the mix of the two bands'
|
|
56
56
|
* @property {() => (import('../bands/bands.js').Cell | null)} [cell] where the canvas falls in the layout grid, while the fit is layout: the box is then the (bilinear) mix of the cell's corners'; it replaces `band` and `blended`
|
|
57
|
+
* @property {((list: any[]) => ReturnType<typeof import('../skin/skin.js').createSkins>) | null} [skins] makes the skins of a row's `data.skins`; null when the bundle has none, so the skin code is never loaded
|
|
57
58
|
* @property {(message: string) => void} error
|
|
58
59
|
* @property {(action: any, scope: string) => void} focus a row's machine ran a focus action: FocusTarget with the machine's own rid, FocusMove or FocusClear; `scope` is the machine (`row.<id>`, or `row.<id>/<sid>` for a nested artboard in the row)
|
|
59
60
|
*/
|
|
@@ -99,6 +100,15 @@ export function createLists(entries, lotties, options) {
|
|
|
99
100
|
const values = itemValues(item, entry.data)
|
|
100
101
|
const table = slotValues(entry.data, values, options.error)
|
|
101
102
|
for (const sid in table) anim.slots[sid] = table[sid]
|
|
103
|
+
// the skinned paths the row's binds move, deformed for this item: its animation is its file's name
|
|
104
|
+
const rowSkins = options.skins && entry.data && entry.data.skins ? options.skins(entry.data.skins) : null
|
|
105
|
+
const animationId = String(entry.lottie).replace(/\.json$/, '')
|
|
106
|
+
const deformRow = (/** @type {Record<string, any>} */ current) => {
|
|
107
|
+
if (!rowSkins) return
|
|
108
|
+
const deformed = rowSkins.evaluate((source) => sourceValue(source, current, entry.data, options.error), animationId)
|
|
109
|
+
for (const sid in deformed) anim.slots[sid] = deformed[sid]
|
|
110
|
+
}
|
|
111
|
+
deformRow(values)
|
|
102
112
|
|
|
103
113
|
// the row's own machine, and the machines of the nested artboards in it: each runs on the row's clock
|
|
104
114
|
const machines = entry.machine ? [ { sid: 'row', fps: entry.machine.fps, machine: entry.machine.machine, animations: entry.machine.animations, inputs: {}, remoteKeys: entry.remoteKeys } ] : []
|
|
@@ -115,6 +125,7 @@ export function createLists(entries, lotties, options) {
|
|
|
115
125
|
if (sid !== 'row') return
|
|
116
126
|
const table = slotValues(entry.data, extend(values, changed), options.error)
|
|
117
127
|
for (const slot in table) anim.slots[slot] = table[slot]
|
|
128
|
+
deformRow(extend(values, changed))
|
|
118
129
|
},
|
|
119
130
|
})
|
|
120
131
|
runner.takeInputs(values)
|
package/lottie/lottie.layers.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { drawDocument } from '../player/overlay/overlay.js'
|
|
12
|
-
import { drawOperations } from './lottie.paint.js'
|
|
12
|
+
import { clipToOperations, drawOperations } from './lottie.paint.js'
|
|
13
13
|
import { fromLottie, IDENTITY, multiply, trace, transformPath } from './lottie.paths.js'
|
|
14
14
|
import { evaluator } from './lottie.props.js'
|
|
15
15
|
import { compileItems, compileTransform, runGroup, scalar } from './lottie.shapes.js'
|
|
@@ -307,9 +307,10 @@ function clipToMasks(ctx, masks, local, matrix) {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
/**
|
|
310
|
-
* A layer through its track matte
|
|
311
|
-
*
|
|
312
|
-
*
|
|
310
|
+
* A layer through its track matte. An alpha matte that is one opaque fill
|
|
311
|
+
* clips the layer to its path; any other is drawn with the layer on scratch
|
|
312
|
+
* canvases the size of the target, the content kept where the matte is
|
|
313
|
+
* opaque (alpha) or clear (inverted). Luma mattes are read as alpha mattes.
|
|
313
314
|
*
|
|
314
315
|
* @param {CanvasRenderingContext2D} ctx
|
|
315
316
|
* @param {any} layer
|
|
@@ -321,10 +322,16 @@ function clipToMasks(ctx, masks, local, matrix) {
|
|
|
321
322
|
* @param {number} opacity
|
|
322
323
|
*/
|
|
323
324
|
function drawMatted(ctx, layer, matte, context, frame, matrix, matteMatrix, opacity) {
|
|
325
|
+
if (clipsByPath(ctx, layer, matte, frame, matteMatrix)) {
|
|
326
|
+
drawLayer(ctx, layer, context, frame, matrix, opacity)
|
|
327
|
+
ctx.restore()
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
324
331
|
const width = ctx.canvas.width
|
|
325
332
|
const height = ctx.canvas.height
|
|
326
|
-
const content =
|
|
327
|
-
const mask =
|
|
333
|
+
const content = takeScratch(width, height)
|
|
334
|
+
const mask = takeScratch(width, height)
|
|
328
335
|
if (!content || !mask) {
|
|
329
336
|
drawLayer(ctx, layer, context, frame, matrix, opacity)
|
|
330
337
|
return
|
|
@@ -340,18 +347,64 @@ function drawMatted(ctx, layer, matte, context, frame, matrix, matteMatrix, opac
|
|
|
340
347
|
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
341
348
|
ctx.drawImage(content.canvas, 0, 0)
|
|
342
349
|
ctx.restore()
|
|
350
|
+
giveScratch(mask)
|
|
351
|
+
giveScratch(content)
|
|
343
352
|
}
|
|
344
353
|
|
|
345
354
|
/**
|
|
355
|
+
* Clips to the matte's path when an alpha matte is one opaque fill, leaving
|
|
356
|
+
* the clip saved for the caller to restore.
|
|
357
|
+
*
|
|
358
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
359
|
+
* @param {any} layer
|
|
360
|
+
* @param {any} matte
|
|
361
|
+
* @param {number} frame
|
|
362
|
+
* @param {Matrix} matteMatrix
|
|
363
|
+
* @returns {boolean} whether it clipped
|
|
364
|
+
*/
|
|
365
|
+
function clipsByPath(ctx, layer, matte, frame, matteMatrix) {
|
|
366
|
+
const isPlainShapes = layer.matte === 1 && matte.shapes && !matte.masks.length && matte.blend === 'source-over'
|
|
367
|
+
if (!isPlainShapes) return false
|
|
368
|
+
|
|
369
|
+
const local = frame - matte.st
|
|
370
|
+
const run = runGroup(matte.shapes, local, IDENTITY, 1, null)
|
|
371
|
+
ctx.save()
|
|
372
|
+
if (clipToOperations(ctx, run.operations, matteMatrix, local)) return true
|
|
373
|
+
ctx.restore()
|
|
374
|
+
return false
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** scratch canvases free for a matte to draw on; a matte inside a matte takes others */
|
|
378
|
+
const SCRATCH = /** @type {CanvasRenderingContext2D[]} */ ([])
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* A cleared scratch canvas of the size, reused across frames: a matte drawn
|
|
382
|
+
* every frame would otherwise make two canvases a frame.
|
|
383
|
+
*
|
|
346
384
|
* @param {number} width
|
|
347
385
|
* @param {number} height
|
|
348
386
|
* @returns {CanvasRenderingContext2D | null}
|
|
349
387
|
*/
|
|
350
|
-
function
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
canvas
|
|
354
|
-
|
|
388
|
+
function takeScratch(width, height) {
|
|
389
|
+
const ctx = SCRATCH.pop() || document.createElement('canvas').getContext('2d')
|
|
390
|
+
if (!ctx) return null
|
|
391
|
+
const canvas = ctx.canvas
|
|
392
|
+
// setting the size clears it; a canvas already that size is cleared by hand
|
|
393
|
+
if (canvas.width !== width || canvas.height !== height) {
|
|
394
|
+
canvas.width = width
|
|
395
|
+
canvas.height = height
|
|
396
|
+
} else {
|
|
397
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
398
|
+
ctx.globalCompositeOperation = 'source-over'
|
|
399
|
+
ctx.globalAlpha = 1
|
|
400
|
+
ctx.clearRect(0, 0, width, height)
|
|
401
|
+
}
|
|
402
|
+
return ctx
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** @param {CanvasRenderingContext2D} ctx */
|
|
406
|
+
function giveScratch(ctx) {
|
|
407
|
+
SCRATCH.push(ctx)
|
|
355
408
|
}
|
|
356
409
|
|
|
357
410
|
/**
|
package/lottie/lottie.paint.js
CHANGED
|
@@ -35,6 +35,36 @@ export function drawOperations(ctx, operations, layer, opacity, frame) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Clips to what a layer's operations cover, when that is exactly one opaque
|
|
40
|
+
* solid fill: an alpha matte of it keeps the same pixels as a clip to its
|
|
41
|
+
* path, at none of a matte's offscreen cost. Anything else (strokes,
|
|
42
|
+
* gradients, transparency, several paints, merged paths) returns false and
|
|
43
|
+
* clips nothing.
|
|
44
|
+
*
|
|
45
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
46
|
+
* @param {Operation[]} operations as runGroup returns them
|
|
47
|
+
* @param {Matrix} layer the layer's space onto the canvas
|
|
48
|
+
* @param {number} frame
|
|
49
|
+
* @returns {boolean} whether it clipped
|
|
50
|
+
*/
|
|
51
|
+
export function clipToOperations(ctx, operations, layer, frame) {
|
|
52
|
+
if (operations.length !== 1) return false
|
|
53
|
+
const operation = operations[0]
|
|
54
|
+
const style = operation.paint
|
|
55
|
+
const isOpaque = operation.opacity >= 1 && scalar(style.opacity(frame)) >= 100
|
|
56
|
+
const isSolidFill = !style.isStroke && !style.isGradient && (style.color(frame).length < 4 || style.color(frame)[3] >= 1)
|
|
57
|
+
if (!isOpaque || !isSolidFill || !operation.entries.length) return false
|
|
58
|
+
for (let n = 0; n < operation.entries.length; n++) if (operation.entries[n].holder.merge) return false
|
|
59
|
+
|
|
60
|
+
const m = multiply(layer, operation.matrix)
|
|
61
|
+
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
|
|
62
|
+
ctx.beginPath()
|
|
63
|
+
for (let n = 0; n < operation.entries.length; n++) traceEntry(ctx, operation.entries[n])
|
|
64
|
+
ctx.clip(style.rule)
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
38
68
|
/**
|
|
39
69
|
* @param {CanvasRenderingContext2D} ctx
|
|
40
70
|
* @param {Operation} operation
|
package/lottie/lottie.paths.js
CHANGED
|
@@ -98,16 +98,31 @@ export function trace(ctx, path) {
|
|
|
98
98
|
if (!points.length) return
|
|
99
99
|
|
|
100
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
|
-
}
|
|
101
|
+
for (let k = 1; k < points.length; k++) segment(ctx, points[k - 1], cOut[k - 1], cIn[k], points[k])
|
|
104
102
|
if (path.closed) {
|
|
105
103
|
const last = points.length - 1
|
|
106
|
-
ctx
|
|
104
|
+
segment(ctx, points[last], cOut[last], cIn[0], points[0])
|
|
107
105
|
ctx.closePath()
|
|
108
106
|
}
|
|
109
107
|
}
|
|
110
108
|
|
|
109
|
+
/**
|
|
110
|
+
* One segment: a line where both handles sit on their points, as most of a
|
|
111
|
+
* Lottie's do, else the cubic. A line is far cheaper for the canvas to fill
|
|
112
|
+
* and stroke than a curve, and draws the same.
|
|
113
|
+
*
|
|
114
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
115
|
+
* @param {Point} from
|
|
116
|
+
* @param {Point} out from's out handle
|
|
117
|
+
* @param {Point} into to's in handle
|
|
118
|
+
* @param {Point} to
|
|
119
|
+
*/
|
|
120
|
+
function segment(ctx, from, out, into, to) {
|
|
121
|
+
const isStraight = out.x === from.x && out.y === from.y && into.x === to.x && into.y === to.y
|
|
122
|
+
if (isStraight) ctx.lineTo(to.x, to.y)
|
|
123
|
+
else ctx.bezierCurveTo(out.x, out.y, into.x, into.y, to.x, to.y)
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
// Shapes
|
|
112
127
|
// ■■■■■■
|
|
113
128
|
|
package/lottie/lottie.props.js
CHANGED
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* where a keyframe carries its own end value in `e`.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { mixValues } from '../mixer/mixer.mix.js'
|
|
12
|
+
import { mixValues, settledWeight } from '../mixer/mixer.mix.js'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @typedef {(frame: number) => any} Evaluator
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Live slot values, by slot id; `undefined` leaves a property to its keyframes.
|
|
19
|
+
* Live slot values, by slot id; `undefined` leaves a property to its keyframes, and a function gives the value at a frame.
|
|
20
20
|
* @typedef {Record<string, any>} Slots
|
|
21
21
|
*/
|
|
22
22
|
|
|
@@ -33,8 +33,12 @@ export function evaluator(property, slots, fallback) {
|
|
|
33
33
|
const sid = property.sid
|
|
34
34
|
if (typeof sid !== 'string') return keyed
|
|
35
35
|
|
|
36
|
+
// a nested machine that mixes several animations names where its poses are
|
|
37
|
+
const poseKey = property.pz ? 'pz:' + property.pz.n : ''
|
|
36
38
|
return (frame) => {
|
|
37
39
|
const live = slots[sid]
|
|
40
|
+
// a live value can depend on the frame: a skin the animation keys is asked for its path, in the poses of the machine it is drawn in
|
|
41
|
+
if (typeof live === 'function') return live(frame, poseKey ? slots[poseKey] : null)
|
|
38
42
|
return live === undefined ? keyed(frame) : live
|
|
39
43
|
}
|
|
40
44
|
}
|
|
@@ -43,10 +47,12 @@ export function evaluator(property, slots, fallback) {
|
|
|
43
47
|
* A property of a nested machine that plays several animations at once. The
|
|
44
48
|
* machine says which it plays, as `{ start, frame, weight }` in the slot
|
|
45
49
|
* `pz:<id>`: the first frame of the animation's segment on the stitched
|
|
46
|
-
* timeline, how far in it is, and its weight.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
+
* timeline, how far in it is, and its weight. Rive applies the poses in
|
|
51
|
+
* order, each mixing the value toward its own by its weight, onto what the
|
|
52
|
+
* last frame left: held, that settles where each pose that keys the property
|
|
53
|
+
* counts by its weight times what the poses after it leave (`settledWeight`),
|
|
54
|
+
* and the setup value no longer shows. One that does not key it leaves it
|
|
55
|
+
* alone; with none keying it, it is the keyframes at the time remap's frame.
|
|
50
56
|
*
|
|
51
57
|
* @param {any} property `pz` is `{ n, k }`: the machine's id, and the segments that key the property
|
|
52
58
|
* @param {Evaluator} keyed
|
|
@@ -62,10 +68,14 @@ function posedEvaluator(property, keyed, slots) {
|
|
|
62
68
|
if (!poses) return value
|
|
63
69
|
|
|
64
70
|
let mixed = value
|
|
71
|
+
let total = 0
|
|
65
72
|
for (let n = 0; n < poses.length; n++) {
|
|
66
73
|
const pose = poses[n]
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
const weight = keying.indexOf(pose.start) < 0 ? 0 : settledWeight(poses, n, keying)
|
|
75
|
+
if (weight <= 0) continue
|
|
76
|
+
// a running weighted average: each pose's share of what has been counted so far
|
|
77
|
+
total += weight
|
|
78
|
+
mixed = mixValues(mixed, keyed(pose.start + pose.frame), weight / total)
|
|
69
79
|
}
|
|
70
80
|
return mixed
|
|
71
81
|
}
|
package/lottie/lottie.shapes.js
CHANGED
|
@@ -205,7 +205,8 @@ export function runGroup(items, frame, matrix, opacity, sink) {
|
|
|
205
205
|
} else if (item.kind === 'group') {
|
|
206
206
|
const local = item.transform(frame)
|
|
207
207
|
const inner = runGroup(item.items, frame, multiply(matrix, local.matrix), opacity * local.opacity, sink)
|
|
208
|
-
|
|
208
|
+
// a group that draws nothing (a Solo's hidden child, or anything faded out) is not a place a pointer can hit
|
|
209
|
+
if (sink && item.rid && opacity * local.opacity > 0) {
|
|
209
210
|
const rid = sink.prefix + item.rid
|
|
210
211
|
if (sink.hits.wants(rid)) sink.hits.add(rid, inner.entries, multiply(sink.matrix, multiply(matrix, local.matrix)))
|
|
211
212
|
}
|
package/mixer/mixer.js
CHANGED
|
@@ -35,11 +35,14 @@ import { mixValues } from './mixer.mix.js'
|
|
|
35
35
|
* @property {number} weight 0 to 1
|
|
36
36
|
*/
|
|
37
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 */
|
|
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; `picks` for a Solo option's visibility, which an animation sets whole, never blends */
|
|
39
|
+
/** @typedef {{ at: Evaluator, keyed: boolean, axes: number[] | null, picks: boolean }} Source */
|
|
40
|
+
|
|
41
|
+
/** The `rid` of a Solo option's visibility: `<object id>.solo` */
|
|
42
|
+
const SOLO = /\.solo$/
|
|
40
43
|
|
|
41
44
|
/** 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 */
|
|
45
|
+
/** @typedef {{ animation: string, sources: Record<string, Source>, offset: number, weight: number }} Prepared */
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* What the mixing evaluators share: the poses of the draw, and each property's value after the previous frame.
|
|
@@ -59,6 +62,7 @@ const NO_SLOTS = {}
|
|
|
59
62
|
* @property {Animation} animation the mixed picture; its `slots` are live
|
|
60
63
|
* @property {string} carrier the animation whose structure it has
|
|
61
64
|
* @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
|
|
65
|
+
* @property {(read: (animation: string) => any, base: any) => Evaluator} mixed a value each animation gives on its own (a slot the player fills, such as a skin's path), mixed over the poses being drawn: each pose whose animation gives one lerps toward it by its weight
|
|
62
66
|
*/
|
|
63
67
|
|
|
64
68
|
/**
|
|
@@ -92,6 +96,18 @@ export function createMixer(lotties, options) {
|
|
|
92
96
|
}
|
|
93
97
|
animation.draw(ctx, prepare(lotties, sources, poses, current, carrier), matrix)
|
|
94
98
|
},
|
|
99
|
+
mixed(read, base) {
|
|
100
|
+
return (frame) => {
|
|
101
|
+
let value = typeof base === 'function' ? base(frame) : base
|
|
102
|
+
for (let n = 0; n < current.poses.length; n++) {
|
|
103
|
+
const pose = current.poses[n]
|
|
104
|
+
const own = read(pose.animation)
|
|
105
|
+
if (own === undefined) continue
|
|
106
|
+
value = mixValues(value, typeof own === 'function' ? own(frame + pose.offset) : own, pose.weight)
|
|
107
|
+
}
|
|
108
|
+
return value
|
|
109
|
+
}
|
|
110
|
+
},
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
|
|
@@ -126,7 +142,7 @@ function collect(lottie) {
|
|
|
126
142
|
const found = {}
|
|
127
143
|
visit(lottie, (property) => {
|
|
128
144
|
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 }
|
|
145
|
+
found[property.rid] = { at: evaluator(property, NO_SLOTS), keyed: property.ky === 1, axes: Array.isArray(property.kc) ? property.kc : null, picks: SOLO.test(property.rid) }
|
|
130
146
|
})
|
|
131
147
|
return found
|
|
132
148
|
}
|
|
@@ -144,8 +160,10 @@ function visit(node, found) {
|
|
|
144
160
|
return
|
|
145
161
|
}
|
|
146
162
|
if (typeof node.rid === 'string' && 'k' in node) found(node)
|
|
163
|
+
// a property's `k` holds its values; a gradient's `g.k` is a property itself
|
|
164
|
+
const isProperty = 'a' in node || typeof node.rid === 'string'
|
|
147
165
|
for (const key in node) {
|
|
148
|
-
if (key === 'k' || key === 'rid') continue
|
|
166
|
+
if ((key === 'k' && isProperty) || key === 'rid') continue
|
|
149
167
|
visit(node[key], found)
|
|
150
168
|
}
|
|
151
169
|
}
|
|
@@ -191,7 +209,10 @@ function baseOf(rid, sources, carrier, lottie) {
|
|
|
191
209
|
const source = sources[id][rid]
|
|
192
210
|
if (source && !source.keyed) return source.at(0)
|
|
193
211
|
}
|
|
194
|
-
|
|
212
|
+
// a property the carrier does not tag starts from the first animation that does
|
|
213
|
+
let own = sources[carrier][rid]
|
|
214
|
+
for (const id in sources) if (!own) own = sources[id][rid]
|
|
215
|
+
const first = own.at(lottie.ip || 0)
|
|
195
216
|
if (!Array.isArray(first)) return first
|
|
196
217
|
|
|
197
218
|
const result = first.slice()
|
|
@@ -242,7 +263,9 @@ function mixing(rid, base, current) {
|
|
|
242
263
|
if (!source || !source.keyed) continue
|
|
243
264
|
|
|
244
265
|
const sample = source.at(frame + pose.offset)
|
|
245
|
-
|
|
266
|
+
// a Solo's choice is an id, which Rive sets from any animation applying it, the last one winning
|
|
267
|
+
if (source.picks) value = pose.weight > 0 ? sample : value
|
|
268
|
+
else value = source.axes ? mixAxes(value, sample, pose.weight, source.axes) : mixValues(value, sample, pose.weight)
|
|
246
269
|
}
|
|
247
270
|
}
|
|
248
271
|
current.next[rid] = value
|
|
@@ -278,7 +301,7 @@ function prepare(lotties, sources, poses, current, carrier) {
|
|
|
278
301
|
isFirst = false
|
|
279
302
|
}
|
|
280
303
|
const frame = (lottie.ip || 0) + pose.time * (lottie.fr || 30)
|
|
281
|
-
current.poses.push({ sources: sources[pose.animation], offset: frame - drawn, weight: pose.weight })
|
|
304
|
+
current.poses.push({ animation: pose.animation, sources: sources[pose.animation], offset: frame - drawn, weight: pose.weight })
|
|
282
305
|
}
|
|
283
306
|
return drawn
|
|
284
307
|
}
|
package/mixer/mixer.mix.js
CHANGED
|
@@ -88,3 +88,20 @@ function mixPoints(a, b, t) {
|
|
|
88
88
|
for (let n = 0; n < a.length; n++) points.push(mixVectors(a[n], b[n], t))
|
|
89
89
|
return points
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* How much a pose counts in the value the poses settle on: its weight, times
|
|
94
|
+
* what each pose after it that keys the value too leaves of it.
|
|
95
|
+
*
|
|
96
|
+
* @param {Array<{ start: number, weight: number }>} poses
|
|
97
|
+
* @param {number} at the pose's index
|
|
98
|
+
* @param {number[]} keying the segments that key the value
|
|
99
|
+
* @returns {number}
|
|
100
|
+
*/
|
|
101
|
+
export function settledWeight(poses, at, keying) {
|
|
102
|
+
let weight = poses[at].weight
|
|
103
|
+
for (let n = at + 1; n < poses.length && weight > 0; n++) {
|
|
104
|
+
if (keying.indexOf(poses[n].start) >= 0) weight *= 1 - Math.min(1, poses[n].weight)
|
|
105
|
+
}
|
|
106
|
+
return weight
|
|
107
|
+
}
|
package/motion/motion.convert.js
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* @property {(path: string) => any} [read] a view-model property's value, by dotted path
|
|
15
15
|
* @property {string[]} [names] the names of an enum the value indexes
|
|
16
16
|
* @property {(message: string) => void} [warn]
|
|
17
|
+
* @property {import('../script/play/play.js').Scripts} [scripts] runs the bundle's script converters
|
|
18
|
+
* @property {string} [type] the bound property's type, so a script is given a colour as one
|
|
19
|
+
* @property {Record<string, any>} [values] every live value, which a script can read
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
22
|
/**
|
|
@@ -126,13 +129,20 @@ export function applyConverter(descriptor, value, context) {
|
|
|
126
129
|
return enumIndex(value, ctx.names)
|
|
127
130
|
case 'listLength':
|
|
128
131
|
return Array.isArray(value) ? value.length : 0
|
|
132
|
+
case 'solo':
|
|
133
|
+
return soloVisibility(descriptor, value)
|
|
129
134
|
case 'group': {
|
|
130
135
|
let result = value
|
|
131
136
|
const items = descriptor.items || []
|
|
132
137
|
// only the first link sees the enum names
|
|
133
|
-
for (let n = 0; n < items.length; n++) result = applyConverter(items[n], result, n === 0 ? ctx : { read: ctx.read, warn: ctx.warn })
|
|
138
|
+
for (let n = 0; n < items.length; n++) result = applyConverter(items[n], result, n === 0 ? ctx : { read: ctx.read, warn: ctx.warn, scripts: ctx.scripts, values: ctx.values })
|
|
134
139
|
return result
|
|
135
140
|
}
|
|
141
|
+
case 'script':
|
|
142
|
+
// run by the bundle's script host; without one the value passes through
|
|
143
|
+
if (ctx.scripts) return ctx.scripts.convert(descriptor, value, false, ctx.type === 'color' ? 'color' : undefined, ctx.values)
|
|
144
|
+
warnUnknown(descriptor, ctx)
|
|
145
|
+
return value
|
|
136
146
|
default:
|
|
137
147
|
warnUnknown(descriptor, ctx)
|
|
138
148
|
return value
|
|
@@ -171,6 +181,8 @@ export function reverseConverter(descriptor, value, context) {
|
|
|
171
181
|
for (let n = items.length - 1; n >= 0; n--) result = reverseConverter(items[n], result, ctx)
|
|
172
182
|
return result
|
|
173
183
|
}
|
|
184
|
+
case 'script':
|
|
185
|
+
return ctx.scripts ? ctx.scripts.convert(descriptor, value, true) : value
|
|
174
186
|
default:
|
|
175
187
|
return value
|
|
176
188
|
}
|
|
@@ -358,6 +370,25 @@ function pad(d, text) {
|
|
|
358
370
|
return d.atEnd ? result.slice(0, wanted) : result.slice(result.length - wanted)
|
|
359
371
|
}
|
|
360
372
|
|
|
373
|
+
/**
|
|
374
|
+
* A Solo option's visibility, in percent: 100 when the bound value names it,
|
|
375
|
+
* 0 otherwise. A number names an option by its place among the Solo's options
|
|
376
|
+
* (`index`), an enum or a string by name (`names`, the ones that pick the
|
|
377
|
+
* option; an enum's value is its index, which `ids` lists). A descriptor with
|
|
378
|
+
* neither is an option no value reaches.
|
|
379
|
+
*
|
|
380
|
+
* @param {any} d
|
|
381
|
+
* @param {any} value
|
|
382
|
+
*/
|
|
383
|
+
function soloVisibility(d, value) {
|
|
384
|
+
if (Array.isArray(d.names)) {
|
|
385
|
+
if (typeof value === 'string') return d.names.indexOf(value) >= 0 ? 100 : 0
|
|
386
|
+
return Array.isArray(d.ids) && typeof value === 'number' && d.ids.indexOf(value) >= 0 ? 100 : 0
|
|
387
|
+
}
|
|
388
|
+
if (typeof d.index === 'number') return Math.round(numeric(value)) === d.index ? 100 : 0
|
|
389
|
+
return 0
|
|
390
|
+
}
|
|
391
|
+
|
|
361
392
|
/**
|
|
362
393
|
* An enum value as its index, from an index or a name.
|
|
363
394
|
*
|
package/motion/motion.flow.js
CHANGED
|
@@ -31,6 +31,7 @@ const MAX_SETTLE = 16
|
|
|
31
31
|
* @property {(action: any) => void} [focus] a focus action (FocusTarget, FocusMove, FocusClear) ran
|
|
32
32
|
* @property {(layer: number, state: any, fadeMs: number, ease?: number[]) => void} [layer] a layer entered a state, fading in along `ease`'s curve; replaces `play`
|
|
33
33
|
* @property {(layer: number) => number} [elapsed] ms the layer's current state has played, unwrapped
|
|
34
|
+
* @property {import('../script/play/play.js').Scripts | null} [scripts] the scripts the bundle runs, read when a guard or action needs them
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -54,6 +55,8 @@ export function createFlow(manifest, hooks) {
|
|
|
54
55
|
let transitions = {}
|
|
55
56
|
/** Entry actions and listeners that run while a state is being entered must not settle the machine under it */
|
|
56
57
|
let busy = 0
|
|
58
|
+
/** the interaction whose actions are running, which a script's pointer event is of */
|
|
59
|
+
let trigger = ''
|
|
57
60
|
|
|
58
61
|
/**
|
|
59
62
|
* The defaults, with the caller's inputs over them; then the first state.
|
|
@@ -182,7 +185,7 @@ export function createFlow(manifest, hooks) {
|
|
|
182
185
|
for (let n = 0; n < MAX_SETTLE; n++) {
|
|
183
186
|
/** @type {any[]} */
|
|
184
187
|
const taken = []
|
|
185
|
-
for (let layer = 0; layer < graphs.length; layer++) taken.push(takeTransition(graphs[layer], currents[layer], values, events, clockOf(layer)))
|
|
188
|
+
for (let layer = 0; layer < graphs.length; layer++) taken.push(takeTransition(graphs[layer], currents[layer], values, events, clockOf(layer), hooks.scripts || null))
|
|
186
189
|
if (!taken.some(Boolean)) break
|
|
187
190
|
// an event that moved the machine is spent
|
|
188
191
|
events = {}
|
|
@@ -270,6 +273,16 @@ export function createFlow(manifest, hooks) {
|
|
|
270
273
|
settle()
|
|
271
274
|
}
|
|
272
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Values were written straight into `values` (by the bundle's scripted
|
|
278
|
+
* objects, which the next ones read at once): the player redraws and the
|
|
279
|
+
* machine settles, as for `takeInputs`.
|
|
280
|
+
*/
|
|
281
|
+
function changed() {
|
|
282
|
+
if (graph) settle()
|
|
283
|
+
else hooks.valuesChanged(values)
|
|
284
|
+
}
|
|
285
|
+
|
|
273
286
|
/**
|
|
274
287
|
* @param {Record<string, any>} inputs
|
|
275
288
|
* @returns {boolean} whether anything changed or fired
|
|
@@ -325,7 +338,9 @@ export function createFlow(manifest, hooks) {
|
|
|
325
338
|
*/
|
|
326
339
|
function click(isUntargeted) {
|
|
327
340
|
if (!graph) return
|
|
341
|
+
trigger = 'Click'
|
|
328
342
|
run(interactionActions(graph, 'Click', '', isUntargeted ? '' : undefined))
|
|
343
|
+
trigger = ''
|
|
329
344
|
settle()
|
|
330
345
|
}
|
|
331
346
|
|
|
@@ -337,7 +352,9 @@ export function createFlow(manifest, hooks) {
|
|
|
337
352
|
*/
|
|
338
353
|
function pointer(type, target) {
|
|
339
354
|
if (!graph) return
|
|
355
|
+
trigger = type
|
|
340
356
|
run(interactionActions(graph, type, '', target))
|
|
357
|
+
trigger = ''
|
|
341
358
|
settle()
|
|
342
359
|
}
|
|
343
360
|
|
|
@@ -370,7 +387,7 @@ export function createFlow(manifest, hooks) {
|
|
|
370
387
|
/** @param {any[]} actions */
|
|
371
388
|
function run(actions) {
|
|
372
389
|
if (!graph || !actions || !actions.length) return
|
|
373
|
-
const effects = perform(actions, graph, values, events)
|
|
390
|
+
const effects = perform(actions, graph, values, events, hooks.scripts || null, trigger)
|
|
374
391
|
for (let n = 0; n < effects.events.length; n++) hooks.event(effects.events[n])
|
|
375
392
|
if (effects.changed) hooks.valuesChanged(values)
|
|
376
393
|
if (hooks.focus) for (let n = 0; n < effects.focus.length; n++) hooks.focus(effects.focus[n])
|
|
@@ -380,6 +397,7 @@ export function createFlow(manifest, hooks) {
|
|
|
380
397
|
start,
|
|
381
398
|
jumpTo,
|
|
382
399
|
takeInputs,
|
|
400
|
+
changed,
|
|
383
401
|
act,
|
|
384
402
|
focusEvent,
|
|
385
403
|
click,
|