@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/hit/hit.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a pointer can hit: the geometry of the shapes and groups a bundle's
|
|
3
|
+
* pointer listeners target, as the last drawn frame traced it.
|
|
4
|
+
*
|
|
5
|
+
* rive2lottie tags each Lottie group built from a Rive Shape or Node with
|
|
6
|
+
* `rid`, the Rive object's index, and a listener's `target.rid` names it. While
|
|
7
|
+
* a frame draws, the renderer hands over a tagged group's paths (after the
|
|
8
|
+
* group's trims and merges, with the matrix that maps them to canvas pixels:
|
|
9
|
+
* the mixer's poses, layout and fit included). Nothing is flattened until a
|
|
10
|
+
* point is tested, so a watched target costs a few references per frame.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { multiply } from '../lottie/lottie.paths.js'
|
|
14
|
+
import { contains, outline } from './hit.geometry.js'
|
|
15
|
+
|
|
16
|
+
/** @import { Matrix } from '../lottie/lottie.paths.js' */
|
|
17
|
+
/** @import { Entry } from '../lottie/lottie.shapes.js' */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} Hits
|
|
21
|
+
* @property {(rids: string[]) => void} watch the targets to record; others are skipped
|
|
22
|
+
* @property {boolean} active whether any target is watched
|
|
23
|
+
* @property {() => void} begin a frame starts drawing: last frame's geometry goes
|
|
24
|
+
* @property {(rid: string) => boolean} wants
|
|
25
|
+
* @property {(rid: string, entries: Entry[], matrix: Matrix) => void} add a group's paths; `matrix` maps the entries' space to canvas pixels
|
|
26
|
+
* @property {(rid: string, x: number, y: number) => boolean} test whether a canvas pixel is inside the target's geometry in the last frame
|
|
27
|
+
* @property {(rid: string) => ({ x: number, y: number } | null)} center the middle of the target's bounding box in the last frame, in canvas pixels; null when it drew nothing
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** @returns {Hits} */
|
|
31
|
+
export function createHits() {
|
|
32
|
+
/** @type {Record<string, boolean>} */
|
|
33
|
+
let watched = {}
|
|
34
|
+
/** @type {Record<string, { entries: Entry[], matrix: Matrix }[]>} */
|
|
35
|
+
let recorded = {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The polygons of one recorded group, a merge's parts included.
|
|
39
|
+
*
|
|
40
|
+
* @param {Entry[]} entries
|
|
41
|
+
* @param {Matrix} matrix
|
|
42
|
+
* @param {number} x
|
|
43
|
+
* @param {number} y
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isInside(entries, matrix, x, y) {
|
|
47
|
+
for (let n = 0; n < entries.length; n++) {
|
|
48
|
+
const holder = entries[n].holder
|
|
49
|
+
const world = multiply(matrix, entries[n].matrix)
|
|
50
|
+
if (holder.merge) {
|
|
51
|
+
if (isInside(holder.merge.parts, world, x, y)) return true
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
for (let k = 0; k < holder.paths.length; k++) {
|
|
55
|
+
if (contains(outline(holder.paths[k], world), x, y)) return true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Grows a box over the polygons of one recorded group.
|
|
63
|
+
*
|
|
64
|
+
* @param {Entry[]} entries
|
|
65
|
+
* @param {Matrix} matrix
|
|
66
|
+
* @param {number[]} box [minX, minY, maxX, maxY], grown in place
|
|
67
|
+
*/
|
|
68
|
+
function grow(entries, matrix, box) {
|
|
69
|
+
for (let n = 0; n < entries.length; n++) {
|
|
70
|
+
const holder = entries[n].holder
|
|
71
|
+
const world = multiply(matrix, entries[n].matrix)
|
|
72
|
+
if (holder.merge) {
|
|
73
|
+
grow(holder.merge.parts, world, box)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
for (let k = 0; k < holder.paths.length; k++) {
|
|
77
|
+
const polygon = outline(holder.paths[k], world)
|
|
78
|
+
for (let p = 0; p < polygon.length; p++) {
|
|
79
|
+
if (polygon[p].x < box[0]) box[0] = polygon[p].x
|
|
80
|
+
if (polygon[p].y < box[1]) box[1] = polygon[p].y
|
|
81
|
+
if (polygon[p].x > box[2]) box[2] = polygon[p].x
|
|
82
|
+
if (polygon[p].y > box[3]) box[3] = polygon[p].y
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @type {Hits} */
|
|
89
|
+
const hits = {
|
|
90
|
+
active: false,
|
|
91
|
+
|
|
92
|
+
watch(rids) {
|
|
93
|
+
watched = {}
|
|
94
|
+
for (let n = 0; n < rids.length; n++) watched[rids[n]] = true
|
|
95
|
+
hits.active = rids.length > 0
|
|
96
|
+
recorded = {}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
begin() {
|
|
100
|
+
recorded = {}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
wants(rid) {
|
|
104
|
+
return watched[rid] === true
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
add(rid, entries, matrix) {
|
|
108
|
+
if (!watched[rid]) return
|
|
109
|
+
if (!recorded[rid]) recorded[rid] = []
|
|
110
|
+
recorded[rid].push({ entries, matrix })
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
center(rid) {
|
|
114
|
+
const parts = recorded[rid]
|
|
115
|
+
if (!parts) return null
|
|
116
|
+
const box = [ Infinity, Infinity, -Infinity, -Infinity ]
|
|
117
|
+
for (let n = 0; n < parts.length; n++) grow(parts[n].entries, parts[n].matrix, box)
|
|
118
|
+
return box[0] > box[2] ? null : { x: (box[0] + box[2]) / 2, y: (box[1] + box[3]) / 2 }
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
test(rid, x, y) {
|
|
122
|
+
const parts = recorded[rid]
|
|
123
|
+
if (!parts) return false
|
|
124
|
+
for (let n = 0; n < parts.length; n++) if (isInside(parts[n].entries, parts[n].matrix, x, y)) return true
|
|
125
|
+
return false
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
return hits
|
|
129
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small flex for a list's rows: the subset of Rive's layout a container
|
|
3
|
+
* of a list uses, with no Yoga in the browser. The rows are boxes of fixed
|
|
4
|
+
* size, put in a container by its direction (row or column, either one
|
|
5
|
+
* reversed), gap, padding, wrap, and main and cross alignment. A container
|
|
6
|
+
* that hugs takes the size of what it holds.
|
|
7
|
+
*
|
|
8
|
+
* Not modelled: rows that grow, shrink or stretch to fit (a row that does
|
|
9
|
+
* not fit overflows the container, as it does in Rive when it cannot shrink),
|
|
10
|
+
* and items other than the rows.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} Flex
|
|
15
|
+
* @property {string} direction `row` or `column`
|
|
16
|
+
* @property {boolean} reverse the main axis runs from the far side
|
|
17
|
+
* @property {number} wrap 0 no wrap, 1 wrap, 2 wrap reverse
|
|
18
|
+
* @property {number} justify along the main axis: 0 start, 1 centre, 2 end, in the axis's own direction
|
|
19
|
+
* @property {number} items across it, the same
|
|
20
|
+
* @property {boolean} spread the rows are spread between the ends of the main axis
|
|
21
|
+
* @property {number} gap between rows along the main axis
|
|
22
|
+
* @property {number} crossGap between wrapped lines
|
|
23
|
+
* @property {number[]} padding left, top, right, bottom
|
|
24
|
+
* @property {boolean[]} hug whether the container's width and height are its content's
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} Placed
|
|
29
|
+
* @property {number[]} x each row's left, from the container's left
|
|
30
|
+
* @property {number[]} y each row's top, from the container's top
|
|
31
|
+
* @property {number} width the container's width, which a hugging one takes from its rows
|
|
32
|
+
* @property {number} height
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where each row goes in a container.
|
|
37
|
+
*
|
|
38
|
+
* @param {Flex} flex
|
|
39
|
+
* @param {{ w: number, h: number }} box the container's size
|
|
40
|
+
* @param {{ w: number, h: number }[]} sizes each row's
|
|
41
|
+
* @returns {Placed}
|
|
42
|
+
*/
|
|
43
|
+
export function layoutRows(flex, box, sizes) {
|
|
44
|
+
const isRow = flex.direction === 'row'
|
|
45
|
+
const pad = flex.padding
|
|
46
|
+
// the padding at each end of the main and cross axes; a reversed axis starts at the far edge
|
|
47
|
+
const mainStart = isRow ? (flex.reverse ? pad[2] : pad[0]) : (flex.reverse ? pad[3] : pad[1])
|
|
48
|
+
const mainEnd = isRow ? (flex.reverse ? pad[0] : pad[2]) : (flex.reverse ? pad[1] : pad[3])
|
|
49
|
+
const crossStart = isRow ? (flex.wrap === 2 ? pad[3] : pad[1]) : (flex.wrap === 2 ? pad[2] : pad[0])
|
|
50
|
+
const crossEnd = isRow ? (flex.wrap === 2 ? pad[1] : pad[3]) : (flex.wrap === 2 ? pad[0] : pad[2])
|
|
51
|
+
|
|
52
|
+
const mains = []
|
|
53
|
+
const crosses = []
|
|
54
|
+
for (let n = 0; n < sizes.length; n++) {
|
|
55
|
+
mains.push(isRow ? sizes[n].w : sizes[n].h)
|
|
56
|
+
crosses.push(isRow ? sizes[n].h : sizes[n].w)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const hugsMain = flex.hug[isRow ? 0 : 1]
|
|
60
|
+
const hugsCross = flex.hug[isRow ? 1 : 0]
|
|
61
|
+
let mainSize = isRow ? box.w : box.h
|
|
62
|
+
let crossSize = isRow ? box.h : box.w
|
|
63
|
+
|
|
64
|
+
const lines = hugsMain ? [ range(0, sizes.length) ] : breakLines(flex.wrap, mains, flex.gap, mainSize - mainStart - mainEnd)
|
|
65
|
+
if (hugsMain) mainSize = mainStart + mainEnd + extent(lines[0], mains, flex.gap)
|
|
66
|
+
const innerMain = mainSize - mainStart - mainEnd
|
|
67
|
+
|
|
68
|
+
/** @type {number[]} */
|
|
69
|
+
const lineCross = []
|
|
70
|
+
let total = 0
|
|
71
|
+
for (let n = 0; n < lines.length; n++) {
|
|
72
|
+
let tallest = 0
|
|
73
|
+
for (let k = 0; k < lines[n].length; k++) tallest = Math.max(tallest, crosses[lines[n][k]])
|
|
74
|
+
lineCross.push(tallest)
|
|
75
|
+
total += tallest
|
|
76
|
+
}
|
|
77
|
+
total += Math.max(lines.length - 1, 0) * flex.crossGap
|
|
78
|
+
if (hugsCross) crossSize = crossStart + crossEnd + total
|
|
79
|
+
const innerCross = crossSize - crossStart - crossEnd
|
|
80
|
+
|
|
81
|
+
// one line takes the container's whole cross size; wrapped lines pack by the cross alignment
|
|
82
|
+
const isWrapped = flex.wrap !== 0 && lines.length > 0
|
|
83
|
+
if (!isWrapped && lines.length === 1 && !hugsCross) {
|
|
84
|
+
lineCross[0] = innerCross
|
|
85
|
+
total = innerCross
|
|
86
|
+
}
|
|
87
|
+
let cross = crossStart + align(flex.items, innerCross - total)
|
|
88
|
+
|
|
89
|
+
const main = []
|
|
90
|
+
const across = []
|
|
91
|
+
for (let n = 0; n < sizes.length; n++) {
|
|
92
|
+
main.push(0)
|
|
93
|
+
across.push(0)
|
|
94
|
+
}
|
|
95
|
+
for (let n = 0; n < lines.length; n++) {
|
|
96
|
+
const line = lines[n]
|
|
97
|
+
const free = innerMain - extent(line, mains, flex.gap)
|
|
98
|
+
const extra = flex.spread && line.length > 1 ? Math.max(free, 0) / (line.length - 1) : 0
|
|
99
|
+
let at = mainStart + (flex.spread ? 0 : align(flex.justify, free))
|
|
100
|
+
for (let k = 0; k < line.length; k++) {
|
|
101
|
+
const item = line[k]
|
|
102
|
+
main[item] = at
|
|
103
|
+
across[item] = cross + align(flex.items, lineCross[n] - crosses[item])
|
|
104
|
+
at += mains[item] + flex.gap + extra
|
|
105
|
+
}
|
|
106
|
+
cross += lineCross[n] + flex.crossGap
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// a reversed axis was laid out from its far edge
|
|
110
|
+
const x = []
|
|
111
|
+
const y = []
|
|
112
|
+
for (let n = 0; n < sizes.length; n++) {
|
|
113
|
+
const m = flex.reverse ? mainSize - main[n] - mains[n] : main[n]
|
|
114
|
+
const c = flex.wrap === 2 ? crossSize - across[n] - crosses[n] : across[n]
|
|
115
|
+
x.push(isRow ? m : c)
|
|
116
|
+
y.push(isRow ? c : m)
|
|
117
|
+
}
|
|
118
|
+
return { x, y, width: isRow ? mainSize : crossSize, height: isRow ? crossSize : mainSize }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The rows on each line: all of them on one line without wrap, else as many as fit.
|
|
123
|
+
*
|
|
124
|
+
* @param {number} wrap
|
|
125
|
+
* @param {number[]} mains the rows' sizes along the main axis
|
|
126
|
+
* @param {number} gap
|
|
127
|
+
* @param {number} room the main axis's inner size
|
|
128
|
+
* @returns {number[][]} row indexes, a line each
|
|
129
|
+
*/
|
|
130
|
+
function breakLines(wrap, mains, gap, room) {
|
|
131
|
+
if (!mains.length) return []
|
|
132
|
+
if (wrap === 0) return [ range(0, mains.length) ]
|
|
133
|
+
|
|
134
|
+
/** @type {number[][]} */
|
|
135
|
+
const lines = [ [] ]
|
|
136
|
+
let used = 0
|
|
137
|
+
for (let n = 0; n < mains.length; n++) {
|
|
138
|
+
const line = lines[lines.length - 1]
|
|
139
|
+
const needs = mains[n] + (line.length ? gap : 0)
|
|
140
|
+
if (line.length && used + needs > room) {
|
|
141
|
+
lines.push([ n ])
|
|
142
|
+
used = mains[n]
|
|
143
|
+
} else {
|
|
144
|
+
line.push(n)
|
|
145
|
+
used += needs
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return lines
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {number[]} line row indexes
|
|
153
|
+
* @param {number[]} mains
|
|
154
|
+
* @param {number} gap
|
|
155
|
+
* @returns {number} the line's length along the main axis
|
|
156
|
+
*/
|
|
157
|
+
function extent(line, mains, gap) {
|
|
158
|
+
let sum = 0
|
|
159
|
+
for (let n = 0; n < line.length; n++) sum += mains[line[n]]
|
|
160
|
+
return sum + Math.max(line.length - 1, 0) * gap
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {number} at 0 start, 1 centre, 2 end
|
|
165
|
+
* @param {number} free room left over
|
|
166
|
+
* @returns {number}
|
|
167
|
+
*/
|
|
168
|
+
function align(at, free) {
|
|
169
|
+
return at === 1 ? free / 2 : at === 2 ? free : 0
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {number} from
|
|
174
|
+
* @param {number} to
|
|
175
|
+
* @returns {number[]}
|
|
176
|
+
*/
|
|
177
|
+
function range(from, to) {
|
|
178
|
+
const list = []
|
|
179
|
+
for (let n = from; n < to; n++) list.push(n)
|
|
180
|
+
return list
|
|
181
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a list's items are, and the values each gives its row.
|
|
3
|
+
*
|
|
4
|
+
* An item is what the manifest's default instance holds, `{ viewModel,
|
|
5
|
+
* instance }`, or what a host passes to `setInputs({ rows: [ ... ] })`:
|
|
6
|
+
*
|
|
7
|
+
* - `{ instance: 'Two' }`: a named instance of the row's view model
|
|
8
|
+
* - `{ values: { tint: '#FF66BB6A', size: 200 } }`: values of its own, over
|
|
9
|
+
* the view model's default instance, which is the instance Rive makes for
|
|
10
|
+
* a blank one
|
|
11
|
+
* - both, the values over the instance; a property's name may also stand in
|
|
12
|
+
* the item itself (`{ size: 200 }`) unless it is `viewModel`, `instance`
|
|
13
|
+
* or `values`
|
|
14
|
+
* - `'Two'`: the same as `{ instance: 'Two' }`
|
|
15
|
+
*
|
|
16
|
+
* `viewModel` picks the row when a list draws more than one; it defaults to
|
|
17
|
+
* the first row the list has.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { inputValue } from '../motion/motion.values.js'
|
|
21
|
+
|
|
22
|
+
/** Keys of an item that are not property values */
|
|
23
|
+
const RESERVED = { viewModel: true, instance: true, values: true }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {any} item
|
|
27
|
+
* @param {Record<string, any>} rows a list's `rows`, by view-model name
|
|
28
|
+
* @returns {string | null} the view model whose row draws the item, null when none does
|
|
29
|
+
*/
|
|
30
|
+
export function rowNameOf(item, rows) {
|
|
31
|
+
const names = []
|
|
32
|
+
for (const name in rows) names.push(name)
|
|
33
|
+
if (!names.length) return null
|
|
34
|
+
|
|
35
|
+
const asked = item && typeof item === 'object' && typeof item.viewModel === 'string' ? item.viewModel : names[0]
|
|
36
|
+
return rows[asked] ? asked : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The view-model values of one item: its instance's, with its own over them.
|
|
41
|
+
*
|
|
42
|
+
* @param {any} item
|
|
43
|
+
* @param {any} data a row's `data`: its properties, slots and instances
|
|
44
|
+
* @returns {Record<string, any>}
|
|
45
|
+
*/
|
|
46
|
+
export function itemValues(item, data) {
|
|
47
|
+
/** @type {Record<string, any>} */
|
|
48
|
+
const values = {}
|
|
49
|
+
const instances = data.instances || {}
|
|
50
|
+
const named = typeof item === 'string' ? item : item && typeof item.instance === 'string' ? item.instance : data.defaultInstance
|
|
51
|
+
const base = instances[named] || instances[data.defaultInstance] || {}
|
|
52
|
+
for (const name in base) values[name] = base[name]
|
|
53
|
+
if (!item || typeof item !== 'object') return values
|
|
54
|
+
|
|
55
|
+
const own = item.values && typeof item.values === 'object' ? item.values : {}
|
|
56
|
+
for (const name in item) {
|
|
57
|
+
if (!RESERVED[/** @type {'viewModel'} */ (name)] && declared(data, name)) values[name] = inputValue(data, name, item[name])
|
|
58
|
+
}
|
|
59
|
+
for (const name in own) values[name] = inputValue(data, name, own[name])
|
|
60
|
+
return values
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {any} data
|
|
65
|
+
* @param {string} name
|
|
66
|
+
* @returns {boolean} whether the row's view model has a property of this name
|
|
67
|
+
*/
|
|
68
|
+
function declared(data, name) {
|
|
69
|
+
const properties = data.properties || []
|
|
70
|
+
for (let n = 0; n < properties.length; n++) if (properties[n].name === name) return true
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {any} item
|
|
76
|
+
* @returns {string} what makes one item the same as another
|
|
77
|
+
*/
|
|
78
|
+
export function signature(item) {
|
|
79
|
+
return typeof item === 'string' ? 's:' + item : JSON.stringify(item === undefined ? null : item)
|
|
80
|
+
}
|