@px-petals/roku.motion-web 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/audio/audio.cues.js +27 -0
- package/audio/audio.js +259 -0
- package/bands/bands.js +390 -0
- package/bundle/bundle.fonts.js +55 -0
- package/bundle/bundle.js +75 -0
- package/focus/focus.direction.js +46 -0
- package/focus/focus.dom.js +158 -0
- package/focus/focus.js +277 -0
- package/focus/focus.tree.js +146 -0
- package/hit/hit.geometry.js +113 -0
- package/hit/hit.js +129 -0
- package/list/list.flex.js +181 -0
- package/list/list.items.js +80 -0
- package/list/list.js +507 -0
- package/lottie/lottie.js +124 -0
- package/lottie/lottie.layers.js +363 -0
- package/lottie/lottie.paint.js +208 -0
- package/lottie/lottie.paths.js +459 -0
- package/lottie/lottie.props.js +255 -0
- package/lottie/lottie.shapes.js +258 -0
- package/mixer/mixer.js +284 -0
- package/mixer/mixer.mix.js +90 -0
- package/mixer/mixer.stage.js +431 -0
- package/motion/motion.convert.js +373 -0
- package/motion/motion.flow.js +403 -0
- package/motion/motion.keys.js +150 -0
- package/motion/motion.machine.js +367 -0
- package/motion/motion.nested.js +345 -0
- package/motion/motion.values.js +364 -0
- package/motion.js +979 -0
- package/package.json +53 -4
- package/player/overlay/overlay.js +325 -0
- package/player/overlay/overlay.layout.js +208 -0
- package/player/player.fit.js +101 -0
- package/player/player.js +401 -0
- package/player/player.raster.js +127 -0
- package/pointer/pointer.js +292 -0
- package/readme.md +270 -0
- package/README.md +0 -3
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canvas as a focus target, for `keys: 'auto'`: it can be given DOM focus
|
|
3
|
+
* (it gets a `tabIndex`), and while it has it, its keys go to Rive's focus.
|
|
4
|
+
*
|
|
5
|
+
* Motion never takes DOM focus: nothing here calls `focus()`. The user or the
|
|
6
|
+
* host gives it (Tab, a click on the canvas, `canvas.focus()`), and this
|
|
7
|
+
* reacts, as `@rive-app/canvas` does:
|
|
8
|
+
* - DOM focus arrives by keyboard: focus enters the tree at once, at the first
|
|
9
|
+
* stop for a Tab from before the canvas, the last for a Shift+Tab from after.
|
|
10
|
+
* Arriving by pointer leaves the tree unfocused until the next Tab (or a
|
|
11
|
+
* listener's focus action), so a click does not pick a stop.
|
|
12
|
+
* - DOM focus leaves (the user moved on): Rive's focus is cleared, so Blur
|
|
13
|
+
* listeners run. Not when the window itself lost focus.
|
|
14
|
+
* - Tab from the last stop (or Shift+Tab from the first) is left to the
|
|
15
|
+
* browser, which moves DOM focus on; the keys that follow are ignored until
|
|
16
|
+
* focus is given again.
|
|
17
|
+
* - `preventDefault` only for a key Rive used: one a listener took, a Tab or
|
|
18
|
+
* arrow that moved focus or is held by a `stop` edge.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { keyFromEvent, keyNames, modifiersFromEvent } from '../motion/motion.keys.js'
|
|
22
|
+
import { PHASES } from './focus.js'
|
|
23
|
+
|
|
24
|
+
/** A focus this soon after a mouse or touch press is the pointer's */
|
|
25
|
+
const POINTER_FOCUS_MS = 600
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} DomOptions
|
|
29
|
+
* @property {ReturnType<typeof import('./focus.js').createFocus>} focus
|
|
30
|
+
* @property {(down: boolean) => void} [pressed] a key went through (for the host's `key` event)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {HTMLCanvasElement} canvas
|
|
35
|
+
* @param {DomOptions} options
|
|
36
|
+
*/
|
|
37
|
+
export function createDom(canvas, options) {
|
|
38
|
+
const focus = options.focus
|
|
39
|
+
let isBound = false
|
|
40
|
+
let hasDomFocus = typeof document !== 'undefined' && document.activeElement === canvas
|
|
41
|
+
/** Tab left the tree: the page has the keys until focus comes back */
|
|
42
|
+
let released = false
|
|
43
|
+
let lastPointer = 0
|
|
44
|
+
/** @type {{ target: any, name: string, listener: (event: any) => void }[]} */
|
|
45
|
+
const bound = []
|
|
46
|
+
|
|
47
|
+
const onPointer = () => {
|
|
48
|
+
lastPointer = new Date().getTime()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether the focus that just arrived came from the keyboard. `:focus-visible`
|
|
53
|
+
* is the browser's own answer; where it is missing, a recent press says it
|
|
54
|
+
* was the pointer.
|
|
55
|
+
*/
|
|
56
|
+
function isKeyboardFocus() {
|
|
57
|
+
try {
|
|
58
|
+
return canvas.matches(':focus-visible')
|
|
59
|
+
} catch {
|
|
60
|
+
return new Date().getTime() - lastPointer > POINTER_FOCUS_MS
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {any} from where focus came from
|
|
66
|
+
* @returns {boolean} whether that is before the canvas in the page
|
|
67
|
+
*/
|
|
68
|
+
function isFromBefore(from) {
|
|
69
|
+
if (!from || !from.compareDocumentPosition) return true
|
|
70
|
+
const position = canvas.compareDocumentPosition(from)
|
|
71
|
+
if (position & 2) return true
|
|
72
|
+
if (position & 4) return false
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {any} event */
|
|
77
|
+
function onFocus(event) {
|
|
78
|
+
hasDomFocus = true
|
|
79
|
+
released = false
|
|
80
|
+
if (focus.current !== null || !focus.hasNodes) return
|
|
81
|
+
if (isKeyboardFocus()) focus.move(isFromBefore(event.relatedTarget) ? 'next' : 'previous')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @param {any} event */
|
|
85
|
+
function onBlur(event) {
|
|
86
|
+
hasDomFocus = false
|
|
87
|
+
// the window itself lost focus (another tab, another app): the canvas keeps Rive's focus for when it returns
|
|
88
|
+
if (!event.relatedTarget && typeof document.hasFocus === 'function' && !document.hasFocus()) return
|
|
89
|
+
focus.clear()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {any} event
|
|
94
|
+
* @param {number} phase
|
|
95
|
+
* @returns {boolean} whether Rive used it
|
|
96
|
+
*/
|
|
97
|
+
function handle(event, phase) {
|
|
98
|
+
if (event.isComposing || event.keyCode === 229) return false
|
|
99
|
+
if (released) return false
|
|
100
|
+
const name = keyFromEvent(event)
|
|
101
|
+
if (!name) return false
|
|
102
|
+
const names = keyNames(name)
|
|
103
|
+
if (name === 'tab' && !focus.hasNodes) return false
|
|
104
|
+
return focus.press({ names, mods: modifiersFromEvent(event), phase })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @param {any} event */
|
|
108
|
+
function onKeyDown(event) {
|
|
109
|
+
if (!hasDomFocus && event.target !== canvas) return
|
|
110
|
+
const used = handle(event, event.repeat ? PHASES.repeat : PHASES.down)
|
|
111
|
+
if (used) {
|
|
112
|
+
if (event.preventDefault) event.preventDefault()
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
// Tab (or Shift+Tab) off the end: the page moves focus on, and Rive's keys wait until it is given back
|
|
116
|
+
if (focus.hasNodes && keyFromEvent(event) === 'tab') released = true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @param {any} event */
|
|
120
|
+
function onKeyUp(event) {
|
|
121
|
+
if (!hasDomFocus && event.target !== canvas) return
|
|
122
|
+
handle(event, PHASES.up)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @type {Record<string, (event: any) => void>} */
|
|
126
|
+
const handlers = { focus: onFocus, blur: onBlur, keydown: onKeyDown, keyup: onKeyUp, mousedown: onPointer, touchstart: onPointer, pointerdown: onPointer }
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Makes the canvas able to be given focus, and starts listening, or stops.
|
|
130
|
+
*
|
|
131
|
+
* @param {boolean} wanted whether the bundle has keys or a focus tree to answer
|
|
132
|
+
*/
|
|
133
|
+
function listen(wanted) {
|
|
134
|
+
if (wanted === isBound) return
|
|
135
|
+
isBound = wanted
|
|
136
|
+
if (wanted) {
|
|
137
|
+
// a tabIndex lets the canvas be given focus; it never takes it
|
|
138
|
+
if (canvas.tabIndex < 0 || !canvas.hasAttribute('tabindex')) canvas.tabIndex = 0
|
|
139
|
+
for (const name in handlers) {
|
|
140
|
+
bound.push({ target: canvas, name, listener: handlers[name] })
|
|
141
|
+
canvas.addEventListener(name, handlers[name], false)
|
|
142
|
+
}
|
|
143
|
+
hasDomFocus = document.activeElement === canvas
|
|
144
|
+
} else {
|
|
145
|
+
for (let n = 0; n < bound.length; n++) bound[n].target.removeEventListener(bound[n].name, bound[n].listener, false)
|
|
146
|
+
bound.length = 0
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
listen,
|
|
152
|
+
destroy: () => listen(false),
|
|
153
|
+
/** Whether the canvas has DOM focus */
|
|
154
|
+
get hasFocus() {
|
|
155
|
+
return hasDomFocus
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
}
|
package/focus/focus.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rive's focus, in a bundle: which node of its focus tree holds focus, how
|
|
3
|
+
* focus moves (Tab, the arrows, focus actions), the Focus and Blur listeners
|
|
4
|
+
* that fire as it does, and the keyboard listeners that hear a key through the
|
|
5
|
+
* focused node and the nodes above it.
|
|
6
|
+
*
|
|
7
|
+
* This is Rive's own focus, not the browser's: it says which target of the
|
|
8
|
+
* graphic the keys belong to. It never touches DOM focus. Whether keys reach
|
|
9
|
+
* it at all is `focus.dom.js` (a canvas that has DOM focus) or the host
|
|
10
|
+
* (`motion.key`). See the web readme, "Focus and keys".
|
|
11
|
+
*
|
|
12
|
+
* Rive's rules, as read from `@rive-app/canvas` and checked by the parity
|
|
13
|
+
* fixtures:
|
|
14
|
+
* - A key goes to the focused node's listeners first. If any matched, it is
|
|
15
|
+
* spent. If none did, it goes to the node above, and so on up the tree.
|
|
16
|
+
* - Focus and Blur listeners fire for every node whose subtree gains or loses
|
|
17
|
+
* the focused node: moving from a node to its child fires nothing on the
|
|
18
|
+
* node, leaving the node's subtree fires its Blur.
|
|
19
|
+
* - Tab on a key a listener took does not move focus. Tab from the last stop
|
|
20
|
+
* leaves the tree (focus is cleared). An arrow no listener took moves
|
|
21
|
+
* focus in that direction.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { pickInDirection } from './focus.direction.js'
|
|
25
|
+
import { createTree } from './focus.tree.js'
|
|
26
|
+
|
|
27
|
+
/** Key modifier bits, as a KeyboardInput's `modifiers` has them */
|
|
28
|
+
export const MODIFIERS = { shift: 1, ctrl: 2, alt: 4, meta: 8 }
|
|
29
|
+
|
|
30
|
+
/** The phases of a keystroke, as a KeyboardInput's `keyPhase` has them */
|
|
31
|
+
export const PHASES = { down: 1, repeat: 2, up: 4 }
|
|
32
|
+
|
|
33
|
+
const ARROWS = { up: 'up', down: 'down', left: 'left', right: 'right' }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {object} FocusOptions
|
|
37
|
+
* @property {(type: 'Focus' | 'Blur', rid: string) => void} fire a node gained or lost focus: its listeners run
|
|
38
|
+
* @property {(actions: any[], scope: string) => void} run runs a key binding's actions, in the machine its `scope` names ("" for the bundle's own; a nested artboard's binding has the machine's id)
|
|
39
|
+
* @property {(rid: string) => ({ x: number, y: number } | null)} centerOf where a node was drawn (canvas pixels); null while it has no geometry
|
|
40
|
+
* @property {(rid: string | null) => void} changed the focused node changed
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {FocusOptions} options
|
|
45
|
+
*/
|
|
46
|
+
export function createFocus(options) {
|
|
47
|
+
let tree = createTree([])
|
|
48
|
+
/** @type {any[]} the machine's key bindings (`remoteKeys`) */
|
|
49
|
+
let bindings = []
|
|
50
|
+
/** @type {string | null} */
|
|
51
|
+
let current = null
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {any[]} nodes the machine's `focus.nodes`
|
|
55
|
+
* @param {any[]} keys the bundle's `remoteKeys`
|
|
56
|
+
*/
|
|
57
|
+
function setup(nodes, keys) {
|
|
58
|
+
tree = createTree(nodes || [])
|
|
59
|
+
bindings = keys || []
|
|
60
|
+
current = null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Swaps in a new tree and key bindings while the page runs, as a list's rows
|
|
65
|
+
* come and go. Focus stays where it is. On a node that is gone it stays too,
|
|
66
|
+
* as in `@rive-app/canvas`: nothing hears a key, and the next Tab, which has
|
|
67
|
+
* no place to go on from, leaves the tree and clears it.
|
|
68
|
+
*
|
|
69
|
+
* @param {any[]} nodes
|
|
70
|
+
* @param {any[]} keys
|
|
71
|
+
*/
|
|
72
|
+
function retree(nodes, keys) {
|
|
73
|
+
tree = createTree(nodes || [])
|
|
74
|
+
bindings = keys || []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Moves focus to a node (or nowhere), firing the listeners of every node
|
|
79
|
+
* whose subtree lost or gained it.
|
|
80
|
+
*
|
|
81
|
+
* @param {string | null} next
|
|
82
|
+
*/
|
|
83
|
+
function set(next) {
|
|
84
|
+
if (next === current) return
|
|
85
|
+
const before = tree.chain(current)
|
|
86
|
+
const after = tree.chain(next)
|
|
87
|
+
current = next
|
|
88
|
+
for (let n = 0; n < before.length; n++) if (after.indexOf(before[n]) < 0) options.fire('Blur', before[n])
|
|
89
|
+
for (let n = after.length - 1; n >= 0; n--) if (before.indexOf(after[n]) < 0) options.fire('Focus', after[n])
|
|
90
|
+
options.changed(current)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A focus node by its `rid` or its name.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} target
|
|
97
|
+
* @returns {string | null}
|
|
98
|
+
*/
|
|
99
|
+
function resolve(target) {
|
|
100
|
+
if (tree.has(target)) return target
|
|
101
|
+
for (let n = 0; n < tree.nodes.length; n++) if (tree.nodes[n].name === target) return tree.nodes[n].rid
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} target a node's `rid` or name
|
|
107
|
+
* @returns {boolean} whether it took focus (a node that cannot focus does not)
|
|
108
|
+
*/
|
|
109
|
+
function focus(target) {
|
|
110
|
+
const rid = resolve(target)
|
|
111
|
+
if (rid === null || !tree.canFocus(rid)) return false
|
|
112
|
+
set(rid)
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Tab (`next`), Shift+Tab (`previous`), or a direction.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} kind next, previous, up, down, left or right
|
|
120
|
+
* @returns {boolean} whether focus moved
|
|
121
|
+
*/
|
|
122
|
+
function move(kind) {
|
|
123
|
+
if (kind === 'next' || kind === 'previous') {
|
|
124
|
+
const step = tree.step(current, kind === 'next')
|
|
125
|
+
if (step.stays) return false
|
|
126
|
+
set(step.to)
|
|
127
|
+
return step.to !== null
|
|
128
|
+
}
|
|
129
|
+
if (current === null || !ARROWS[/** @type {keyof typeof ARROWS} */ (kind)]) return false
|
|
130
|
+
const from = options.centerOf(current)
|
|
131
|
+
if (!from) return false
|
|
132
|
+
const to = pickInDirection(kind, from, tree.stops, options.centerOf)
|
|
133
|
+
if (to === null) return false
|
|
134
|
+
set(to)
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function clear() {
|
|
139
|
+
set(null)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The key bindings a key sets off on one node.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} rid
|
|
146
|
+
* @param {string[]} names the Rive names the key has
|
|
147
|
+
* @param {number} phase
|
|
148
|
+
* @param {number} mods
|
|
149
|
+
* @returns {any[]}
|
|
150
|
+
*/
|
|
151
|
+
function bound(rid, names, phase, mods) {
|
|
152
|
+
const found = []
|
|
153
|
+
for (let n = 0; n < bindings.length; n++) {
|
|
154
|
+
const binding = bindings[n]
|
|
155
|
+
if (binding.target !== rid) continue
|
|
156
|
+
if (((binding.phase === undefined ? PHASES.down : binding.phase) & phase) === 0) continue
|
|
157
|
+
if ((binding.modifiers || 0) !== mods) continue
|
|
158
|
+
if (matches(binding.keys, names)) found.push(binding)
|
|
159
|
+
}
|
|
160
|
+
return found
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Offers a key to the focused node's listeners, then the nodes above it; the
|
|
165
|
+
* first node with a listener for it keeps it.
|
|
166
|
+
*
|
|
167
|
+
* @param {string[]} names
|
|
168
|
+
* @param {number} phase
|
|
169
|
+
* @param {number} mods
|
|
170
|
+
* @returns {boolean} whether a listener took it
|
|
171
|
+
*/
|
|
172
|
+
function route(names, phase, mods) {
|
|
173
|
+
if (!tree.hasNodes) return runUnscoped(names, phase)
|
|
174
|
+
const chain = tree.chain(current)
|
|
175
|
+
for (let n = 0; n < chain.length; n++) {
|
|
176
|
+
const found = bound(chain[n], names, phase, mods)
|
|
177
|
+
if (!found.length) continue
|
|
178
|
+
for (let k = 0; k < found.length; k++) options.run(found[k].actions, found[k].scope || '')
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A bundle with nothing focusable has no focus to route by: its listeners
|
|
186
|
+
* hear a key press wherever it is sent, as they did before focus.
|
|
187
|
+
*
|
|
188
|
+
* @param {string[]} names
|
|
189
|
+
* @param {number} phase
|
|
190
|
+
* @returns {boolean}
|
|
191
|
+
*/
|
|
192
|
+
function runUnscoped(names, phase) {
|
|
193
|
+
if (phase !== PHASES.down) return false
|
|
194
|
+
let took = false
|
|
195
|
+
for (let n = 0; n < bindings.length; n++) {
|
|
196
|
+
if (bindings[n].phase !== undefined && bindings[n].phase !== PHASES.down) continue
|
|
197
|
+
if (!matches(bindings[n].keys, names)) continue
|
|
198
|
+
options.run(bindings[n].actions, '')
|
|
199
|
+
took = true
|
|
200
|
+
}
|
|
201
|
+
return took
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A keystroke, as Rive's canvas runtime handles it.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} key
|
|
208
|
+
* @param {string[]} key.names the Rive names the key has
|
|
209
|
+
* @param {number} key.mods MODIFIERS bits held
|
|
210
|
+
* @param {number} key.phase PHASES: down, repeat or up
|
|
211
|
+
* @returns {boolean} whether it was used (a listener took it, or focus moved or is held); false lets the page or the host have it
|
|
212
|
+
*/
|
|
213
|
+
function press({ names, mods, phase }) {
|
|
214
|
+
if (phase === PHASES.up) {
|
|
215
|
+
route(names, phase, mods)
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
const isTab = names.indexOf('tab') >= 0
|
|
219
|
+
if (isTab && tree.hasNodes) {
|
|
220
|
+
if (current !== null && route(names, phase, mods)) return true
|
|
221
|
+
move(mods & MODIFIERS.shift ? 'previous' : 'next')
|
|
222
|
+
// focus walked off the end (cleared) is a Tab for the page
|
|
223
|
+
return current !== null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let used = route(names, phase, mods)
|
|
227
|
+
if (!used && tree.hasNodes && !(mods & (MODIFIERS.ctrl | MODIFIERS.alt | MODIFIERS.meta))) {
|
|
228
|
+
for (let n = 0; n < names.length && !used; n++) {
|
|
229
|
+
if (ARROWS[/** @type {keyof typeof ARROWS} */ (names[n])]) used = move(names[n]) || current !== null
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return used
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
setup,
|
|
237
|
+
retree,
|
|
238
|
+
focus,
|
|
239
|
+
move,
|
|
240
|
+
clear,
|
|
241
|
+
press,
|
|
242
|
+
resolve,
|
|
243
|
+
/** The node that has focus (its `rid`), or null */
|
|
244
|
+
get current() {
|
|
245
|
+
return current
|
|
246
|
+
},
|
|
247
|
+
get hasNodes() {
|
|
248
|
+
return tree.hasNodes
|
|
249
|
+
},
|
|
250
|
+
/** @param {string} rid */
|
|
251
|
+
node: (rid) => tree.node(rid),
|
|
252
|
+
get nodes() {
|
|
253
|
+
return tree.nodes
|
|
254
|
+
},
|
|
255
|
+
get stops() {
|
|
256
|
+
return tree.stops
|
|
257
|
+
},
|
|
258
|
+
/** Whether any key binding could hear this key, wherever focus is: the keys a bundle uses */
|
|
259
|
+
/** @param {string[]} names */
|
|
260
|
+
uses(names) {
|
|
261
|
+
for (let n = 0; n < bindings.length; n++) if (matches(bindings[n].keys, names)) return true
|
|
262
|
+
return false
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @param {string[]} keys a binding's key names; "any" matches every key
|
|
269
|
+
* @param {string[]} names the key's names
|
|
270
|
+
* @returns {boolean}
|
|
271
|
+
*/
|
|
272
|
+
function matches(keys, names) {
|
|
273
|
+
for (let n = 0; n < (keys || []).length; n++) {
|
|
274
|
+
if (keys[n] === 'any' || names.indexOf(keys[n]) >= 0) return true
|
|
275
|
+
}
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rive's focus tree as a bundle's `machine.focus.nodes` lists it: the nodes
|
|
3
|
+
* that own a FocusData, in the order Tab visits them (a parent before its
|
|
4
|
+
* children), each with the focus node it sits inside.
|
|
5
|
+
*
|
|
6
|
+
* - A node is a Tab stop when `canFocus` and `canTraverse` are on (they are,
|
|
7
|
+
* unless the node says `false`); a node that is not is skipped and traversal
|
|
8
|
+
* goes on into its children.
|
|
9
|
+
* - `edge` says what traversal does when it would leave that node's subtree:
|
|
10
|
+
* `closedLoop` wraps within it (the node itself is part of the loop if it is
|
|
11
|
+
* a stop) and `stop` stays on the last stop. The artboard (`rid` "") is the
|
|
12
|
+
* outermost scope; by default leaving it ends focus.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} FocusNode
|
|
17
|
+
* @property {string} rid
|
|
18
|
+
* @property {string} [name]
|
|
19
|
+
* @property {string} [parent]
|
|
20
|
+
* @property {boolean} [canFocus]
|
|
21
|
+
* @property {boolean} [canTouch]
|
|
22
|
+
* @property {boolean} [canTraverse]
|
|
23
|
+
* @property {string} [edge]
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {FocusNode[]} list a machine's `focus.nodes`
|
|
28
|
+
*/
|
|
29
|
+
export function createTree(list) {
|
|
30
|
+
/** @type {Record<string, FocusNode>} */
|
|
31
|
+
const byRid = {}
|
|
32
|
+
/** @type {Record<string, number>} */
|
|
33
|
+
const position = {}
|
|
34
|
+
/** @type {string} the artboard's own edge behaviour */
|
|
35
|
+
let rootEdge = ''
|
|
36
|
+
for (let n = 0; n < list.length; n++) {
|
|
37
|
+
byRid[list[n].rid] = list[n]
|
|
38
|
+
position[list[n].rid] = n
|
|
39
|
+
if (list[n].rid === '') rootEdge = list[n].edge || ''
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @param {string} rid */
|
|
43
|
+
const isStop = (rid) => {
|
|
44
|
+
const node = byRid[rid]
|
|
45
|
+
return !!node && node.canFocus !== false && node.canTraverse !== false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The Tab stops, in order @type {string[]} */
|
|
49
|
+
const stops = []
|
|
50
|
+
for (let n = 0; n < list.length; n++) if (list[n].rid !== '' && isStop(list[n].rid)) stops.push(list[n].rid)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether `rid` is `scope` or sits inside it.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} rid
|
|
56
|
+
* @param {string} scope
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
function isInside(rid, scope) {
|
|
60
|
+
if (scope === '') return true
|
|
61
|
+
for (let at = /** @type {string | undefined} */ (rid); at !== undefined && at !== ''; at = byRid[at] ? byRid[at].parent : undefined) if (at === scope) return true
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The nearest scope around a node that has an edge behaviour: itself, or the
|
|
67
|
+
* node above it, up to the artboard.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} rid
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function scopeOf(rid) {
|
|
73
|
+
for (let at = /** @type {string | undefined} */ (rid); at !== undefined && at !== ''; at = byRid[at] ? byRid[at].parent : undefined) {
|
|
74
|
+
if (byRid[at] && (byRid[at].edge === 'closedLoop' || byRid[at].edge === 'stop')) return at
|
|
75
|
+
}
|
|
76
|
+
return ''
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {string} scope */
|
|
80
|
+
const edgeOf = (scope) => (scope === '' ? rootEdge : byRid[scope].edge)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
/** Whether the artboard has anything to focus */
|
|
84
|
+
get hasNodes() {
|
|
85
|
+
return list.length > 0
|
|
86
|
+
},
|
|
87
|
+
nodes: list,
|
|
88
|
+
stops,
|
|
89
|
+
/** @param {string} rid */
|
|
90
|
+
has: (rid) => !!byRid[rid],
|
|
91
|
+
/** @param {string} rid */
|
|
92
|
+
node: (rid) => byRid[rid] || null,
|
|
93
|
+
/**
|
|
94
|
+
* The focus nodes from a node up to the outermost, itself first.
|
|
95
|
+
*
|
|
96
|
+
* @param {string | null} rid
|
|
97
|
+
* @returns {string[]}
|
|
98
|
+
*/
|
|
99
|
+
chain(rid) {
|
|
100
|
+
/** @type {string[]} */
|
|
101
|
+
const found = []
|
|
102
|
+
for (let at = rid; at !== null && at !== undefined && at !== '' && byRid[at]; at = byRid[at].parent || '') found.push(at)
|
|
103
|
+
if (rid !== null && byRid[''] ) found.push('')
|
|
104
|
+
return found
|
|
105
|
+
},
|
|
106
|
+
/** @param {string} rid */
|
|
107
|
+
canFocus: (rid) => !!byRid[rid] && byRid[rid].canFocus !== false,
|
|
108
|
+
/**
|
|
109
|
+
* Tab, or Shift+Tab, from a node (or from nothing).
|
|
110
|
+
*
|
|
111
|
+
* @param {string | null} from
|
|
112
|
+
* @param {boolean} forward
|
|
113
|
+
* @returns {{ to: string | null, stays: boolean }} `to` the stop it lands on; null with `stays` false when it walked off the end, and `stays` true when a `stop` edge holds it where it is
|
|
114
|
+
*/
|
|
115
|
+
step(from, forward) {
|
|
116
|
+
if (!stops.length) return { to: null, stays: false }
|
|
117
|
+
if (from === null) return { to: forward ? stops[0] : stops[stops.length - 1], stays: false }
|
|
118
|
+
|
|
119
|
+
// the nearest stop after (or before) the node in the tree's order
|
|
120
|
+
const at = position[from]
|
|
121
|
+
/** @type {string | null} */
|
|
122
|
+
let candidate = null
|
|
123
|
+
if (forward) {
|
|
124
|
+
for (let n = 0; n < stops.length; n++) if (position[stops[n]] > at) { candidate = stops[n]; break }
|
|
125
|
+
} else {
|
|
126
|
+
for (let n = stops.length - 1; n >= 0; n--) if (position[stops[n]] < at) { candidate = stops[n]; break }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const scope = scopeOf(from)
|
|
130
|
+
const leaves = candidate === null || !isInside(candidate, scope)
|
|
131
|
+
if (!leaves) return { to: candidate, stays: false }
|
|
132
|
+
const edge = edgeOf(scope)
|
|
133
|
+
if (edge === 'stop') return { to: from, stays: true }
|
|
134
|
+
if (edge !== 'closedLoop') return { to: null, stays: false }
|
|
135
|
+
|
|
136
|
+
// wrap within the scope: its first (or last) stop
|
|
137
|
+
/** @type {string | null} */
|
|
138
|
+
let wrapped = null
|
|
139
|
+
for (let n = 0; n < stops.length; n++) {
|
|
140
|
+
const stop = stops[forward ? n : stops.length - 1 - n]
|
|
141
|
+
if (isInside(stop, scope)) { wrapped = stop; break }
|
|
142
|
+
}
|
|
143
|
+
return { to: wrapped, stays: false }
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Point-in-shape maths for hit tests, on the traced paths the renderer draws.
|
|
3
|
+
*
|
|
4
|
+
* `isPointInPath` would need the context's current path and transform, and
|
|
5
|
+
* re-tracing every target on each pointer move; here the traced paths are
|
|
6
|
+
* kept as they were drawn, and flattened to polygons only when a point is
|
|
7
|
+
* tested. A fill is nonzero-wound, as Lottie and Rive fill by default.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { apply } from '../lottie/lottie.paths.js'
|
|
11
|
+
|
|
12
|
+
/** @import { Matrix, Path, Point } from '../lottie/lottie.paths.js' */
|
|
13
|
+
|
|
14
|
+
/** Line segments a curve is flattened to; a 16th of a quarter circle is well inside a pixel */
|
|
15
|
+
const CURVE_STEPS = 16
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A path as a polygon in the space `matrix` maps it to. Curves are flattened,
|
|
19
|
+
* and an open path is closed, as a fill closes it.
|
|
20
|
+
*
|
|
21
|
+
* @param {Path} path
|
|
22
|
+
* @param {Matrix} matrix
|
|
23
|
+
* @returns {Point[]}
|
|
24
|
+
*/
|
|
25
|
+
export function outline(path, matrix) {
|
|
26
|
+
const count = path.points.length
|
|
27
|
+
if (!count) return []
|
|
28
|
+
|
|
29
|
+
const points = path.points.map((point) => apply(matrix, point.x, point.y))
|
|
30
|
+
const cIn = path.cIn.map((point) => apply(matrix, point.x, point.y))
|
|
31
|
+
const cOut = path.cOut.map((point) => apply(matrix, point.x, point.y))
|
|
32
|
+
|
|
33
|
+
const polygon = [ points[0] ]
|
|
34
|
+
// the segment closing the path counts when the path is closed or has a curve back to its start
|
|
35
|
+
for (let k = 1; k <= count; k++) {
|
|
36
|
+
const isClosing = k === count
|
|
37
|
+
if (isClosing && count < 2) break
|
|
38
|
+
const from = k - 1
|
|
39
|
+
const to = isClosing ? 0 : k
|
|
40
|
+
// an open path is closed by a straight line, whatever its end tangents
|
|
41
|
+
const isCurved = !isClosing || path.closed
|
|
42
|
+
appendSegment(polygon, points[from], isCurved ? cOut[from] : points[from], isCurved ? cIn[to] : points[to], points[to], isClosing)
|
|
43
|
+
}
|
|
44
|
+
return polygon
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {Point[]} polygon
|
|
49
|
+
* @param {Point} a start
|
|
50
|
+
* @param {Point} b first control
|
|
51
|
+
* @param {Point} c second control
|
|
52
|
+
* @param {Point} d end
|
|
53
|
+
* @param {boolean} isLast the segment back to the polygon's first point, which is not added again
|
|
54
|
+
*/
|
|
55
|
+
function appendSegment(polygon, a, b, c, d, isLast) {
|
|
56
|
+
const isStraight = same(a, b) && same(c, d)
|
|
57
|
+
if (!isStraight) {
|
|
58
|
+
for (let step = 1; step < CURVE_STEPS; step++) {
|
|
59
|
+
const t = step / CURVE_STEPS
|
|
60
|
+
const u = 1 - t
|
|
61
|
+
polygon.push({
|
|
62
|
+
x: u * u * u * a.x + 3 * u * u * t * b.x + 3 * u * t * t * c.x + t * t * t * d.x,
|
|
63
|
+
y: u * u * u * a.y + 3 * u * u * t * b.y + 3 * u * t * t * c.y + t * t * t * d.y,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!isLast) polygon.push(d)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {Point} a
|
|
72
|
+
* @param {Point} b
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
function same(a, b) {
|
|
76
|
+
return Math.abs(a.x - b.x) < 1e-9 && Math.abs(a.y - b.y) < 1e-9
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether a point is inside a polygon by the nonzero rule: the winding
|
|
81
|
+
* number around it is not zero.
|
|
82
|
+
*
|
|
83
|
+
* @param {Point[]} polygon
|
|
84
|
+
* @param {number} x
|
|
85
|
+
* @param {number} y
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
export function contains(polygon, x, y) {
|
|
89
|
+
let winding = 0
|
|
90
|
+
for (let k = 0, j = polygon.length - 1; k < polygon.length; j = k++) {
|
|
91
|
+
const a = polygon[j]
|
|
92
|
+
const b = polygon[k]
|
|
93
|
+
if (a.y <= y) {
|
|
94
|
+
if (b.y > y && side(a, b, x, y) > 0) winding++
|
|
95
|
+
} else if (b.y <= y && side(a, b, x, y) < 0) {
|
|
96
|
+
winding--
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return winding !== 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Which side of the line a→b the point is on: positive to its left.
|
|
104
|
+
*
|
|
105
|
+
* @param {Point} a
|
|
106
|
+
* @param {Point} b
|
|
107
|
+
* @param {number} x
|
|
108
|
+
* @param {number} y
|
|
109
|
+
* @returns {number}
|
|
110
|
+
*/
|
|
111
|
+
function side(a, b, x, y) {
|
|
112
|
+
return (b.x - a.x) * (y - a.y) - (x - a.x) * (b.y - a.y)
|
|
113
|
+
}
|