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