@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/motion/motion.machine.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @typedef {Record<string, boolean>} Events pending events, name → true
|
|
12
12
|
* @typedef {{ changed: boolean, events: string[], focus: any[] }} Effects `focus`: the focus actions (FocusTarget, FocusMove, FocusClear) to run once the values are set
|
|
13
13
|
* @typedef {{ elapsed: number, length: number }} Clock how long the current state has played, and how long its timeline is, in ms
|
|
14
|
+
* @typedef {import('../script/play/play.js').Scripts} Scripts the scripts a bundle runs; without them a Script guard fails and a RunScript action does nothing
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -80,10 +81,11 @@ export function isEvent(graph, name) {
|
|
|
80
81
|
* @param {Values} values
|
|
81
82
|
* @param {Events} events
|
|
82
83
|
* @param {Clock | null} [clock] the current state's time, for exit times; without it exit times are not checked
|
|
84
|
+
* @param {Scripts | null} [scripts]
|
|
83
85
|
* @returns {string} the next state, or ""
|
|
84
86
|
*/
|
|
85
|
-
export function nextState(graph, current, values, events, clock) {
|
|
86
|
-
const transition = takeTransition(graph, current, values, events, clock || null)
|
|
87
|
+
export function nextState(graph, current, values, events, clock, scripts) {
|
|
88
|
+
const transition = takeTransition(graph, current, values, events, clock || null, scripts || null)
|
|
87
89
|
return transition ? transition.toState : ''
|
|
88
90
|
}
|
|
89
91
|
|
|
@@ -96,18 +98,19 @@ export function nextState(graph, current, values, events, clock) {
|
|
|
96
98
|
* @param {Values} values
|
|
97
99
|
* @param {Events} events
|
|
98
100
|
* @param {Clock | null} clock
|
|
101
|
+
* @param {Scripts | null} [scripts]
|
|
99
102
|
* @returns {any} the transition, or null
|
|
100
103
|
*/
|
|
101
|
-
export function takeTransition(graph, current, values, events, clock) {
|
|
104
|
+
export function takeTransition(graph, current, values, events, clock, scripts) {
|
|
102
105
|
for (let n = 0; n < graph.states.length; n++) {
|
|
103
106
|
const state = graph.states[n]
|
|
104
107
|
if (state.type !== 'GlobalState') continue
|
|
105
|
-
const transition = firstPassing(state.transitions, values, events, current, clock)
|
|
108
|
+
const transition = firstPassing(state.transitions, values, events, current, clock, scripts || null)
|
|
106
109
|
if (transition) return transition
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
const state = find(graph, current)
|
|
110
|
-
return state ? firstPassing(state.transitions, values, events, '', clock) : null
|
|
113
|
+
return state ? firstPassing(state.transitions, values, events, '', clock, scripts || null) : null
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
/**
|
|
@@ -148,8 +151,9 @@ export function nextExit(state, length, after) {
|
|
|
148
151
|
* @param {Events} events
|
|
149
152
|
* @param {string} skip a target to pass over, or ""
|
|
150
153
|
* @param {Clock | null} clock
|
|
154
|
+
* @param {Scripts | null} scripts
|
|
151
155
|
*/
|
|
152
|
-
function firstPassing(transitions, values, events, skip, clock) {
|
|
156
|
+
function firstPassing(transitions, values, events, skip, clock, scripts) {
|
|
153
157
|
const list = transitions || []
|
|
154
158
|
for (let n = 0; n < list.length; n++) {
|
|
155
159
|
const transition = list[n]
|
|
@@ -158,7 +162,7 @@ function firstPassing(transitions, values, events, skip, clock) {
|
|
|
158
162
|
// with a clock, an exit time is checked exactly and replaces the guard that waits for the whole timeline
|
|
159
163
|
const isTimed = !!clock && !!transition.exitTime
|
|
160
164
|
if (isTimed && clock.elapsed < exitMs(transition, clock.length)) continue
|
|
161
|
-
if (guardsPass(transition.guards, values, events, isTimed)) return transition
|
|
165
|
+
if (guardsPass(transition.guards, values, events, isTimed, scripts)) return transition
|
|
162
166
|
}
|
|
163
167
|
return null
|
|
164
168
|
}
|
|
@@ -168,12 +172,13 @@ function firstPassing(transitions, values, events, skip, clock) {
|
|
|
168
172
|
* @param {Values} values
|
|
169
173
|
* @param {Events} events
|
|
170
174
|
* @param {boolean} isTimed the transition's exit time is checked apart; its `exit` guard is skipped
|
|
175
|
+
* @param {Scripts | null} scripts
|
|
171
176
|
*/
|
|
172
|
-
function guardsPass(guards, values, events, isTimed) {
|
|
177
|
+
function guardsPass(guards, values, events, isTimed, scripts) {
|
|
173
178
|
const list = guards || []
|
|
174
179
|
for (let n = 0; n < list.length; n++) {
|
|
175
180
|
if (isTimed && list[n].exit === true) continue
|
|
176
|
-
if (!guardPasses(list[n], values, events)) return false
|
|
181
|
+
if (!guardPasses(list[n], values, events, scripts)) return false
|
|
177
182
|
}
|
|
178
183
|
return true
|
|
179
184
|
}
|
|
@@ -184,9 +189,12 @@ function guardsPass(guards, values, events, isTimed) {
|
|
|
184
189
|
* @param {any} guard
|
|
185
190
|
* @param {Values} values
|
|
186
191
|
* @param {Events} events
|
|
192
|
+
* @param {Scripts | null} scripts
|
|
187
193
|
*/
|
|
188
|
-
function guardPasses(guard, values, events) {
|
|
194
|
+
function guardPasses(guard, values, events, scripts) {
|
|
189
195
|
if (guard.type === 'Event') return events[guard.inputName] === true
|
|
196
|
+
// a scripted condition: the script says; with no way to run it, it does not pass
|
|
197
|
+
if (guard.type === 'Script') return !!scripts && scripts.evaluate(guard, values)
|
|
190
198
|
|
|
191
199
|
const value = values[guard.inputName]
|
|
192
200
|
const expected = resolve(guard.compareTo, values)
|
|
@@ -229,13 +237,15 @@ function compare(a, condition, b) {
|
|
|
229
237
|
* @param {Graph} graph
|
|
230
238
|
* @param {Values} values updated in place
|
|
231
239
|
* @param {Events} events updated in place: Fire adds a pending event
|
|
240
|
+
* @param {Scripts | null} [scripts] runs a RunScript action
|
|
241
|
+
* @param {string} [trigger] the interaction the actions run for, which a script's pointer event is of
|
|
232
242
|
* @returns {Effects}
|
|
233
243
|
*/
|
|
234
|
-
export function perform(actions, graph, values, events) {
|
|
244
|
+
export function perform(actions, graph, values, events, scripts, trigger) {
|
|
235
245
|
/** @type {Effects} */
|
|
236
246
|
const effects = { changed: false, events: [], focus: [] }
|
|
237
247
|
const list = actions || []
|
|
238
|
-
for (let n = 0; n < list.length; n++) runOne(list[n], graph, values, events, effects)
|
|
248
|
+
for (let n = 0; n < list.length; n++) runOne(list[n], graph, values, events, effects, scripts || null, trigger || '')
|
|
239
249
|
return effects
|
|
240
250
|
}
|
|
241
251
|
|
|
@@ -245,8 +255,10 @@ export function perform(actions, graph, values, events) {
|
|
|
245
255
|
* @param {Values} values
|
|
246
256
|
* @param {Events} events
|
|
247
257
|
* @param {Effects} effects
|
|
258
|
+
* @param {Scripts | null} scripts
|
|
259
|
+
* @param {string} trigger
|
|
248
260
|
*/
|
|
249
|
-
function runOne(action, graph, values, events, effects) {
|
|
261
|
+
function runOne(action, graph, values, events, effects, scripts, trigger) {
|
|
250
262
|
const name = action.inputName
|
|
251
263
|
const current = values[name]
|
|
252
264
|
switch (action.type) {
|
|
@@ -264,6 +276,9 @@ function runOne(action, graph, values, events, effects) {
|
|
|
264
276
|
case 'OpenUrl':
|
|
265
277
|
effects.events.push('openUrl:' + action.url)
|
|
266
278
|
break
|
|
279
|
+
case 'RunScript':
|
|
280
|
+
if (scripts && scripts.perform(action, trigger, values, events)) effects.changed = true
|
|
281
|
+
break
|
|
267
282
|
case 'Toggle':
|
|
268
283
|
if (typeof current === 'boolean') setValue(values, name, !current, effects)
|
|
269
284
|
break
|
package/motion/motion.values.js
CHANGED
|
@@ -88,15 +88,16 @@ function assetId(value, property) {
|
|
|
88
88
|
* @param {Data} data
|
|
89
89
|
* @param {Record<string, any>} values
|
|
90
90
|
* @param {(message: string) => void} [warn] a converter this player does not know
|
|
91
|
+
* @param {import('../script/play/play.js').Scripts | null} [scripts] runs the bundle's script converters
|
|
91
92
|
* @returns {Record<string, any>}
|
|
92
93
|
*/
|
|
93
|
-
export function slotValues(data, values, warn) {
|
|
94
|
+
export function slotValues(data, values, warn, scripts) {
|
|
94
95
|
/** @type {Record<string, any>} */
|
|
95
96
|
const table = {}
|
|
96
97
|
const slots = (data && data.slots) || []
|
|
97
98
|
for (let n = 0; n < slots.length; n++) {
|
|
98
99
|
const slot = slots[n]
|
|
99
|
-
const value = slotValue(slot, values, data, warn)
|
|
100
|
+
const value = slotValue(slot, values, data, warn, scripts)
|
|
100
101
|
if (value !== undefined) table[slot.sid] = value
|
|
101
102
|
}
|
|
102
103
|
return table
|
|
@@ -122,12 +123,13 @@ export function propertyOf(data, path) {
|
|
|
122
123
|
* @param {Record<string, any>} values
|
|
123
124
|
* @param {Data} data
|
|
124
125
|
* @param {(message: string) => void} [warn]
|
|
126
|
+
* @param {import('../script/play/play.js').Scripts | null} [scripts]
|
|
125
127
|
*/
|
|
126
|
-
function sourceValue(source, values, data, warn) {
|
|
128
|
+
export function sourceValue(source, values, data, warn, scripts) {
|
|
127
129
|
const raw = values[source.path]
|
|
128
130
|
if (raw === undefined || !source.converter) return raw
|
|
129
131
|
const property = propertyOf(data, source.path)
|
|
130
|
-
return applyConverter(source.converter, raw, { read: (path) => values[path], names: property && property.values, warn })
|
|
132
|
+
return applyConverter(source.converter, raw, { read: (path) => values[path], names: property && property.values, warn, scripts: scripts || undefined, values, type: property ? property.type : undefined })
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
/**
|
|
@@ -135,21 +137,22 @@ function sourceValue(source, values, data, warn) {
|
|
|
135
137
|
* @param {Record<string, any>} values
|
|
136
138
|
* @param {Data} data
|
|
137
139
|
* @param {(message: string) => void} [warn]
|
|
140
|
+
* @param {import('../script/play/play.js').Scripts | null} [scripts]
|
|
138
141
|
*/
|
|
139
|
-
function slotValue(slot, values, data, warn) {
|
|
142
|
+
function slotValue(slot, values, data, warn, scripts) {
|
|
140
143
|
const sources = slot.sources || []
|
|
141
144
|
if (!sources.length) return undefined
|
|
142
145
|
|
|
143
146
|
const isPair = slot.type === 'Position' || slot.type === 'Vector'
|
|
144
147
|
if (!isPair) {
|
|
145
|
-
const raw = sourceValue(sources[0], values, data, warn)
|
|
148
|
+
const raw = sourceValue(sources[0], values, data, warn, scripts)
|
|
146
149
|
return raw === undefined ? undefined : convert(raw, slot.conversion, propertyOf(data, sources[0].path))
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
const pair = Array.isArray(slot.base) ? slot.base.slice() : [ 0, 0 ]
|
|
150
153
|
let found = false
|
|
151
154
|
for (let n = 0; n < sources.length; n++) {
|
|
152
|
-
const raw = sourceValue(sources[n], values, data, warn)
|
|
155
|
+
const raw = sourceValue(sources[n], values, data, warn, scripts)
|
|
153
156
|
if (raw === undefined) continue
|
|
154
157
|
const converted = convert(raw, slot.conversion)
|
|
155
158
|
if (converted === undefined) continue
|
|
@@ -222,8 +225,9 @@ export function publicValues(data, values) {
|
|
|
222
225
|
* @param {any} write a `data.writes` entry
|
|
223
226
|
* @param {any} drawn the Lottie property's value
|
|
224
227
|
* @param {any} current the view-model value now, whose alpha a colour keeps
|
|
228
|
+
* @param {import('../script/play/play.js').Scripts | null} [scripts] reverses a script converter
|
|
225
229
|
*/
|
|
226
|
-
export function writtenValue(write, drawn, current) {
|
|
230
|
+
export function writtenValue(write, drawn, current, scripts) {
|
|
227
231
|
const isPair = write.type === 'Position' || write.type === 'Vector'
|
|
228
232
|
let value = isPair && Array.isArray(drawn) ? drawn[write.axis || 0] : drawn
|
|
229
233
|
switch (write.conversion) {
|
|
@@ -246,7 +250,7 @@ export function writtenValue(write, drawn, current) {
|
|
|
246
250
|
break
|
|
247
251
|
}
|
|
248
252
|
if (typeof value !== 'number' || !isFinite(value)) return undefined
|
|
249
|
-
return write.converter ? reverseConverter(write.converter, value) : value
|
|
253
|
+
return write.converter ? reverseConverter(write.converter, value, scripts ? { scripts } : undefined) : value
|
|
250
254
|
}
|
|
251
255
|
|
|
252
256
|
/**
|
package/motion.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { createAudio } from './audio/audio.js'
|
|
19
|
-
import { loadBundle, loadJson, fileUrl } from './bundle/bundle.js'
|
|
19
|
+
import { loadBundle, loadBytes, loadJson, loadScripts, fileUrl } from './bundle/bundle.js'
|
|
20
20
|
import { loadFonts } from './bundle/bundle.fonts.js'
|
|
21
21
|
import { createAnimation } from './lottie/lottie.js'
|
|
22
22
|
import { createLists } from './list/list.js'
|
|
@@ -28,13 +28,19 @@ import { createFlow } from './motion/motion.flow.js'
|
|
|
28
28
|
import { exists, layersOf } from './motion/motion.machine.js'
|
|
29
29
|
import { isClick, keyFromEvent, keyNames, modifiersFromEvent, parseKey } from './motion/motion.keys.js'
|
|
30
30
|
import { createNested } from './motion/motion.nested.js'
|
|
31
|
-
import { flattenInputs, fromPairs, publicValues, slotValues, tintColors, writtenValue } from './motion/motion.values.js'
|
|
31
|
+
import { flattenInputs, fromPairs, publicValues, slotValues, sourceValue, tintColors, writtenValue } from './motion/motion.values.js'
|
|
32
32
|
import { cellOf, gridOf, hasBlend, linkBands, variantIds } from './bands/bands.js'
|
|
33
33
|
import { alignmentOf, fitName } from './player/player.fit.js'
|
|
34
34
|
import { createPlayer } from './player/player.js'
|
|
35
35
|
import { createPointer } from './pointer/pointer.js'
|
|
36
36
|
import { createRaster } from './player/player.raster.js'
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* What a bundle without skins holds: nothing, and no skin code loaded.
|
|
40
|
+
* @type {import('./skin/skin.js').Skins}
|
|
41
|
+
*/
|
|
42
|
+
const noSkins = { count: 0, update() {}, table: () => ({}), keyedBy: () => undefined, evaluate: () => ({}) }
|
|
43
|
+
|
|
38
44
|
/** @import { Bundle } from './bundle/bundle.js' */
|
|
39
45
|
/** @import { Media } from './player/player.js' */
|
|
40
46
|
|
|
@@ -47,8 +53,12 @@ import { createRaster } from './player/player.raster.js'
|
|
|
47
53
|
* @property {string | number[]} [alignment] where it sits in the canvas, over the bundle's: a Rive alignment name such as `topLeft`, or `[x, y]` from -1 to 1
|
|
48
54
|
* @property {'auto' | 'manual'} [keys] `auto` (default): the canvas can be given DOM focus (Tab, a click, `canvas.focus()`) and, while it has it, its keys go to the bundle's focus and keyboard listeners. `manual`: the canvas listens to no keys and the host routes them with `key`, `keyUp`, `focus`, `blur` and `click`
|
|
49
55
|
* @property {boolean} [manual] a test hook: no clock of its own, time moves only by `advance`. Default false
|
|
56
|
+
* @property {import('./bundle/bundle.js').ScriptProvider} [scripts] `script/script.js` (the Luau VM and Rive host), or a function that returns it or a promise of it, for a bundle that has scripts to run: `() => import('.../script/script.js')`. A bundle without scripts never asks for it; without it, a bundle's script conditions do not pass and its script converters pass values through
|
|
50
57
|
*/
|
|
51
58
|
|
|
59
|
+
/** The values of a Motion with no bundle playing */
|
|
60
|
+
const NO_VALUES = {}
|
|
61
|
+
|
|
52
62
|
/**
|
|
53
63
|
* @typedef {'loaded' | 'state' | 'playback' | 'finished' | 'values' | 'event' | 'audio' | 'focus' | 'error'} MotionEvent
|
|
54
64
|
*/
|
|
@@ -74,6 +84,10 @@ export function createMotion(canvas, options) {
|
|
|
74
84
|
let inputOf = {}
|
|
75
85
|
/** Slot id → value for the current values, which the text overlays read */
|
|
76
86
|
let slotTable = {}
|
|
87
|
+
/** The bundle's bound skins, deformed into their slots with the rest; the skin code is loaded only for a bundle that has some */
|
|
88
|
+
let skins = noSkins
|
|
89
|
+
/** @type {typeof import('./skin/skin.js').createSkins | null} */
|
|
90
|
+
let makeSkins = null
|
|
77
91
|
/** Whether a write-back is in progress, whose own redraw must not write again */
|
|
78
92
|
let writing = false
|
|
79
93
|
let loadCount = 0
|
|
@@ -99,6 +113,19 @@ export function createMotion(canvas, options) {
|
|
|
99
113
|
/** @type {Media | null} the mixed media of a bundle that plays through the mixer */
|
|
100
114
|
let mixedMedia = null
|
|
101
115
|
let isMixedShown = false
|
|
116
|
+
/** @type {import('./script/play/play.js').Scripts | null} the bundle's scripts, once loaded */
|
|
117
|
+
let scripts = null
|
|
118
|
+
/** @type {import('./script/play/play.objects.js').Objects | null} the bundle's scripted nodes, drawables and path effects, which run every frame */
|
|
119
|
+
let objects = null
|
|
120
|
+
/** Whether the scripted objects are running a frame, whose own writes redraw once, with the frame */
|
|
121
|
+
let isStepping = false
|
|
122
|
+
/** What the objects wrote since they last handed it over, and the triggers they fired */
|
|
123
|
+
const wrote = /** @type {Record<string, any>} */ ({})
|
|
124
|
+
const fired = /** @type {Record<string, boolean>} */ ({})
|
|
125
|
+
let hasWrites = false
|
|
126
|
+
let hasFires = false
|
|
127
|
+
/** @type {any[] | null} every vector animation drawing, once the bundle is loaded */
|
|
128
|
+
let vectorList = null
|
|
102
129
|
let isPointerWatched = false
|
|
103
130
|
let hasFocusNodes = false
|
|
104
131
|
/** @type {Record<string, Media>} a layout band's mixed media, by the band's width and height (`320x180`); they share the stage with `mixedMedia` */
|
|
@@ -119,16 +146,18 @@ export function createMotion(canvas, options) {
|
|
|
119
146
|
overlay: () => ({ values: slotTable, families, inputOf }),
|
|
120
147
|
onFrame: (item, frame) => writeBack(item, frame),
|
|
121
148
|
placement,
|
|
122
|
-
|
|
123
|
-
|
|
149
|
+
// an animation that holds its own artboard size (a state keying it) is not sized to the canvas, as in Rive
|
|
150
|
+
artboardWidth: (shown) => (holdsSize(shown) ? 0 : blended),
|
|
151
|
+
artboardHeight: (shown) => (holdsSize(shown) ? 0 : blendedHeight),
|
|
124
152
|
tick: (seconds) => {
|
|
153
|
+
if (objects) objects.step(seconds)
|
|
125
154
|
nested.tick(seconds)
|
|
126
155
|
lists.tick(seconds)
|
|
127
156
|
},
|
|
128
157
|
prepare: (shown) => {
|
|
129
158
|
if (nested.count && shown.vector) nested.apply(shown.vector.slots, shown.fps)
|
|
130
159
|
},
|
|
131
|
-
keepAlive: () => nested.count > 0 || lists.isAnimated,
|
|
160
|
+
keepAlive: () => nested.count > 0 || lists.isAnimated || (!!objects && (objects.clocked || objects.live)),
|
|
132
161
|
onCue: (cue) => {
|
|
133
162
|
audio.play(cue.sound)
|
|
134
163
|
// a keyed event reaches the host as a fired one does
|
|
@@ -220,23 +249,29 @@ export function createMotion(canvas, options) {
|
|
|
220
249
|
const attempt = ++loadCount
|
|
221
250
|
if (start && (start.fit !== undefined || start.alignment !== undefined)) asked = { fit: start.fit !== undefined ? start.fit : asked.fit, alignment: start.alignment !== undefined ? start.alignment : asked.alignment }
|
|
222
251
|
return loadBundle(url)
|
|
223
|
-
.then((loaded) => Promise.all([ loaded, loadFonts(loaded), prepareAll(loaded), loadRows(loaded) ]))
|
|
224
|
-
.then(([ loaded, loadedFamilies, prepared, rows ]) => {
|
|
252
|
+
.then((loaded) => Promise.all([ loaded, loadFonts(loaded), prepareAll(loaded), loadRows(loaded), loadScripts(loaded.manifest, options && options.scripts, (file) => loadBytes(fileUrl(loaded, file)), (message) => emit('error', message)), loadSkins(loaded) ]))
|
|
253
|
+
.then(([ loaded, loadedFamilies, prepared, rows, loadedScripts, skinModule ]) => {
|
|
225
254
|
// a later load won
|
|
226
255
|
if (attempt !== loadCount) return
|
|
227
256
|
bundle = loaded
|
|
257
|
+
scripts = loadedScripts
|
|
258
|
+
if (objects) objects.destroy()
|
|
259
|
+
makeSkins = skinModule ? skinModule.createSkins : null
|
|
260
|
+
skins = makeSkins && loaded.manifest.data && loaded.manifest.data.skins ? makeSkins(loaded.manifest.data.skins) : noSkins
|
|
228
261
|
families = loadedFamilies
|
|
229
262
|
media = prepared.media
|
|
230
263
|
mixedMedia = prepared.mixed
|
|
231
264
|
mixedBands = prepared.bands
|
|
265
|
+
vectorList = null
|
|
232
266
|
isMixedShown = false
|
|
233
267
|
inputOf = inputsOfSlots(loaded.manifest)
|
|
234
268
|
nested = createNested(loaded.manifest.nested, { write: nestedWrote, focus: focusAction })
|
|
235
|
-
lists = createLists(loaded.manifest.lists, rows, { base: loaded.base, families: () => families, band: bandWidth, blended: () => blended, cell: () => cell, error: (message) => emit('error', message), focus: focusAction })
|
|
269
|
+
lists = createLists(loaded.manifest.lists, rows, { skins: makeSkins, base: loaded.base, families: () => families, band: bandWidth, blended: () => blended, cell: () => cell, error: (message) => emit('error', message), focus: focusAction })
|
|
236
270
|
listVersion = lists.version
|
|
237
271
|
grid = gridOf(loaded.manifest)
|
|
238
272
|
band = chooseBand()
|
|
239
273
|
audio.load(loaded.manifest.audio, (uri) => fileUrl(loaded, uri))
|
|
274
|
+
objects = startObjects(loaded)
|
|
240
275
|
flow = createFlow(loaded.manifest, hooks())
|
|
241
276
|
const tree = focusTree()
|
|
242
277
|
focusing.setup(tree.nodes, tree.keys)
|
|
@@ -372,6 +407,25 @@ export function createMotion(canvas, options) {
|
|
|
372
407
|
}
|
|
373
408
|
}
|
|
374
409
|
|
|
410
|
+
/**
|
|
411
|
+
* The skin code, when the bundle has skins to deform, its lists' rows' too: a module of its own, fetched only then.
|
|
412
|
+
*
|
|
413
|
+
* @param {Bundle} loaded
|
|
414
|
+
* @returns {Promise<typeof import('./skin/skin.js') | null>}
|
|
415
|
+
*/
|
|
416
|
+
function loadSkins(loaded) {
|
|
417
|
+
const data = loaded.manifest.data
|
|
418
|
+
let has = !!(data && data.skins && data.skins.length)
|
|
419
|
+
const listed = loaded.manifest.lists || []
|
|
420
|
+
for (let n = 0; n < listed.length && !has; n++) {
|
|
421
|
+
for (const name in listed[n].rows) {
|
|
422
|
+
const rowData = listed[n].rows[name].data
|
|
423
|
+
if (rowData && rowData.skins && rowData.skins.length) has = true
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return has ? import('./skin/skin.js') : Promise.resolve(null)
|
|
427
|
+
}
|
|
428
|
+
|
|
375
429
|
/**
|
|
376
430
|
* The Lottie of every row artboard the bundle's lists draw.
|
|
377
431
|
*
|
|
@@ -421,12 +475,19 @@ export function createMotion(canvas, options) {
|
|
|
421
475
|
play(id, loops) {
|
|
422
476
|
player.show(media[variantOf(id)], loops, settings.renderer === 'raster')
|
|
423
477
|
},
|
|
478
|
+
get scripts() {
|
|
479
|
+
return scripts
|
|
480
|
+
},
|
|
424
481
|
valuesChanged(values) {
|
|
482
|
+
if (scripts) scripts.changed(values)
|
|
425
483
|
applySlots(values)
|
|
426
484
|
lists.update(values)
|
|
427
485
|
if (lists.version !== listVersion) refocus()
|
|
428
486
|
if (lists.isPointerStale && flow) watchPointer(flow.pointerTargets())
|
|
429
|
-
|
|
487
|
+
if (!isStepping) {
|
|
488
|
+
player.redraw()
|
|
489
|
+
if (objects && objects.live) player.nudge()
|
|
490
|
+
}
|
|
430
491
|
emit('values', publicValues(bundle ? bundle.manifest.data : null, values))
|
|
431
492
|
},
|
|
432
493
|
stateChanged: (state) => emit('state', state),
|
|
@@ -480,7 +541,7 @@ export function createMotion(canvas, options) {
|
|
|
480
541
|
const write = writes[n]
|
|
481
542
|
const drawn = item.vector.probe(write.sid, frame)
|
|
482
543
|
if (drawn === undefined) continue
|
|
483
|
-
const next = writtenValue(write, drawn, current[write.path])
|
|
544
|
+
const next = writtenValue(write, drawn, current[write.path], scripts)
|
|
484
545
|
if (next === undefined) continue
|
|
485
546
|
const previous = current[write.path]
|
|
486
547
|
const moved = typeof next === 'number' && typeof previous === 'number' ? Math.abs(next - previous) > 0.0001 * Math.max(1, Math.abs(previous)) : next !== previous
|
|
@@ -639,18 +700,128 @@ export function createMotion(canvas, options) {
|
|
|
639
700
|
*/
|
|
640
701
|
function applySlots(values) {
|
|
641
702
|
if (!bundle) return
|
|
642
|
-
const table = slotValues(bundle.manifest.data, values, (message) => emit('error', message))
|
|
703
|
+
const table = slotValues(bundle.manifest.data, values, (message) => emit('error', message), scripts)
|
|
643
704
|
slotTable = table
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
if (
|
|
647
|
-
for (const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
705
|
+
const data = bundle.manifest.data
|
|
706
|
+
const warn = (/** @type {string} */ message) => emit('error', message)
|
|
707
|
+
if (skins.count) skins.update((source) => sourceValue(source, values, data, warn, scripts))
|
|
708
|
+
for (const id in media) writeSlots(media[id].vector, table, id)
|
|
709
|
+
if (mixedMedia) writeSlots(mixedMedia.vector, table, '', mixedMedia)
|
|
710
|
+
for (const width in mixedBands) writeSlots(mixedBands[width].vector, table, '', mixedBands[width])
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* One vector animation's slots: the slot table, and the skins' paths for the animation.
|
|
715
|
+
*
|
|
716
|
+
* @param {any} vector
|
|
717
|
+
* @param {Record<string, any>} table
|
|
718
|
+
* @param {string} id the animation's id, which says what it keys in a skin
|
|
719
|
+
* @param {Media} [mixed] mixed media, whose skins take what each pose's animation keys in them
|
|
720
|
+
*/
|
|
721
|
+
function writeSlots(vector, table, id, mixed) {
|
|
722
|
+
if (!vector) return
|
|
723
|
+
for (const sid in vector.slots) delete vector.slots[sid]
|
|
724
|
+
for (const sid in table) vector.slots[sid] = table[sid]
|
|
725
|
+
if (skins.count) {
|
|
726
|
+
const deformed = skins.table(id)
|
|
727
|
+
const mixer = mixed && mixed.mixed ? mixed.mixed.mixer : null
|
|
728
|
+
for (const sid in deformed) vector.slots[sid] = mixer ? mixer.mixed((animation) => skins.keyedBy(sid, animation), deformed[sid]) : deformed[sid]
|
|
653
729
|
}
|
|
730
|
+
if (objects) putOutputs(vector.slots)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Every vector animation the bundle draws, listed once. */
|
|
734
|
+
function vectorsOf() {
|
|
735
|
+
if (vectorList) return vectorList
|
|
736
|
+
const list = []
|
|
737
|
+
for (const id in media) if (media[id].vector) list.push(media[id].vector)
|
|
738
|
+
if (mixedMedia && mixedMedia.vector) list.push(mixedMedia.vector)
|
|
739
|
+
for (const width in mixedBands) if (mixedBands[width].vector) list.push(mixedBands[width].vector)
|
|
740
|
+
vectorList = list
|
|
741
|
+
return list
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* What the scripted objects drew, into a slot table.
|
|
746
|
+
*
|
|
747
|
+
* @param {Record<string, any>} slots
|
|
748
|
+
*/
|
|
749
|
+
function putOutputs(slots) {
|
|
750
|
+
const made = /** @type {NonNullable<typeof objects>} */ (objects)
|
|
751
|
+
for (let n = 0; n < made.sids.length; n++) {
|
|
752
|
+
const value = made.outputs[made.sids[n]]
|
|
753
|
+
if (value !== undefined) slots[made.sids[n]] = value
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Starts the bundle's scripted nodes, drawables and path effects, when the
|
|
759
|
+
* script engine came with it. What they write lands in the flow's values at
|
|
760
|
+
* once, for the script after them, and reaches the machine, the nested
|
|
761
|
+
* artboards and the slots together, once a frame.
|
|
762
|
+
*
|
|
763
|
+
* @param {Bundle} loaded
|
|
764
|
+
* @returns {import('./script/play/play.objects.js').Objects | null}
|
|
765
|
+
*/
|
|
766
|
+
function startObjects(loaded) {
|
|
767
|
+
const list = loaded.manifest.scriptObjects
|
|
768
|
+
if (!scripts || !list || !list.length) return null
|
|
769
|
+
return scripts.objects(list, {
|
|
770
|
+
data: loaded.manifest.data,
|
|
771
|
+
values: () => (flow ? flow.values : NO_VALUES),
|
|
772
|
+
write: (path, value) => {
|
|
773
|
+
if (!flow) return
|
|
774
|
+
flow.values[path] = value
|
|
775
|
+
wrote[path] = value
|
|
776
|
+
hasWrites = true
|
|
777
|
+
},
|
|
778
|
+
fire: (path) => {
|
|
779
|
+
fired[path] = true
|
|
780
|
+
hasFires = true
|
|
781
|
+
},
|
|
782
|
+
commit: () => {
|
|
783
|
+
if (!flow || (!hasWrites && !hasFires)) return false
|
|
784
|
+
const isWritten = hasWrites
|
|
785
|
+
const isFired = hasFires
|
|
786
|
+
hasWrites = false
|
|
787
|
+
hasFires = false
|
|
788
|
+
isStepping = true
|
|
789
|
+
try {
|
|
790
|
+
if (isWritten) {
|
|
791
|
+
nested.takeInputs(wrote)
|
|
792
|
+
flow.changed()
|
|
793
|
+
}
|
|
794
|
+
if (isFired) {
|
|
795
|
+
nested.takeInputs(fired)
|
|
796
|
+
flow.takeInputs(fired)
|
|
797
|
+
}
|
|
798
|
+
} finally {
|
|
799
|
+
isStepping = false
|
|
800
|
+
for (const name in wrote) delete wrote[name]
|
|
801
|
+
for (const name in fired) delete fired[name]
|
|
802
|
+
}
|
|
803
|
+
return true
|
|
804
|
+
},
|
|
805
|
+
publish: () => {
|
|
806
|
+
const drawn = vectorsOf()
|
|
807
|
+
for (let n = 0; n < drawn.length; n++) putOutputs(drawn[n].slots)
|
|
808
|
+
},
|
|
809
|
+
probe: (sid) => {
|
|
810
|
+
const item = flow ? media[variantOf(flow.animation)] : null
|
|
811
|
+
return item && item.vector ? item.vector.probe(sid, player.frame) : undefined
|
|
812
|
+
},
|
|
813
|
+
families,
|
|
814
|
+
})
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Whether media drawn in a layout band is not the band's size: the
|
|
819
|
+
* converter laid its animation out at the artboard size the animation holds.
|
|
820
|
+
*
|
|
821
|
+
* @param {Media} shown
|
|
822
|
+
*/
|
|
823
|
+
function holdsSize(shown) {
|
|
824
|
+
return !!(band && shown && shown.width && shown.width !== band.width)
|
|
654
825
|
}
|
|
655
826
|
|
|
656
827
|
/** How the animation fits the canvas: what the caller asked for, else the bundle's, else contain and centred. */
|
|
@@ -868,6 +1039,11 @@ export function createMotion(canvas, options) {
|
|
|
868
1039
|
focusing.setup([], [])
|
|
869
1040
|
flow = null
|
|
870
1041
|
bundle = null
|
|
1042
|
+
skins = noSkins
|
|
1043
|
+
makeSkins = null
|
|
1044
|
+
if (objects) objects.destroy()
|
|
1045
|
+
objects = null
|
|
1046
|
+
vectorList = null
|
|
871
1047
|
media = {}
|
|
872
1048
|
nested = createNested([])
|
|
873
1049
|
lists = createLists([], {}, { base: '', band: bandWidth, error: () => {}, focus: () => {} })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@px-petals/roku.motion-web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Motion for the web: plays a lottie-roku bundle on a 2D canvas, vector from its Lottie or from its raster sheets, with its state machine, live data, keys and text, for browsers that can't run Rive's own runtime.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -12,20 +12,12 @@
|
|
|
12
12
|
"./*.js": "./*.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"
|
|
16
|
-
"audio",
|
|
17
|
-
"bands",
|
|
18
|
-
"bundle",
|
|
19
|
-
"focus",
|
|
20
|
-
"hit",
|
|
21
|
-
"list",
|
|
22
|
-
"lottie",
|
|
23
|
-
"mixer",
|
|
24
|
-
"motion",
|
|
25
|
-
"player",
|
|
26
|
-
"pointer",
|
|
15
|
+
"**/*",
|
|
27
16
|
"!**/.tests",
|
|
28
|
-
"!**/*.test.js"
|
|
17
|
+
"!**/*.test.js",
|
|
18
|
+
"!jsconfig*.json",
|
|
19
|
+
"!legacy",
|
|
20
|
+
"!.artifacts"
|
|
29
21
|
],
|
|
30
22
|
"sideEffects": false,
|
|
31
23
|
"browserslist": [
|
|
@@ -41,8 +33,10 @@
|
|
|
41
33
|
"build:legacy": "node legacy/legacy.js",
|
|
42
34
|
"legacy": "node .tests/legacy/legacy.js",
|
|
43
35
|
"parity": "node .tests/parity/parity.js",
|
|
44
|
-
"test": "node --test \".tests/*.test.js\" \"audio/.tests/*.test.js\" \"bands/.tests/*.test.js\" \"hit/.tests/*.test.js\" \"focus/.tests/*.test.js\" \"player/.tests/*.test.js\" \"pointer/.tests/*.test.js\" \"lottie/.tests/*.test.js\" \"list/.tests/*.test.js\" \"mixer/.tests/*.test.js\" \"motion/.tests/*.test.js\" \".tests/browser/*.test.js\"",
|
|
45
|
-
"typecheck": "tsc -p jsconfig.json"
|
|
36
|
+
"test": "node --test \".tests/*.test.js\" \"audio/.tests/*.test.js\" \"bands/.tests/*.test.js\" \"hit/.tests/*.test.js\" \"focus/.tests/*.test.js\" \"player/.tests/*.test.js\" \"pointer/.tests/*.test.js\" \"lottie/.tests/*.test.js\" \"list/.tests/*.test.js\" \"mixer/.tests/*.test.js\" \"skin/.tests/*.test.js\" \"motion/.tests/*.test.js\" \"script/luau/.tests/*.test.js\" \"script/luau/.tests/reference/*.test.js\" \"script/rive/.tests/*.test.js\" \"script/play/.tests/*.test.js\" \".tests/browser/*.test.js\"",
|
|
37
|
+
"typecheck": "tsc -p jsconfig.json",
|
|
38
|
+
"luau:fetch": "node script/luau/.tests/reference/reference.fetch.js",
|
|
39
|
+
"luau:fixtures": "node script/luau/.tests/reference/reference.regen.js"
|
|
46
40
|
},
|
|
47
41
|
"repository": {
|
|
48
42
|
"type": "git",
|
package/player/player.js
CHANGED
|
@@ -71,8 +71,8 @@ const SETTLE_PASSES = 8
|
|
|
71
71
|
* @param {(layer: number) => void} options.onFinished
|
|
72
72
|
* @param {() => Overlay} options.overlay
|
|
73
73
|
* @param {() => { fit: string, alignment: [number, number] }} [options.placement] how the animation fits the canvas, read on every frame
|
|
74
|
-
* @param {() => number}
|
|
75
|
-
* @param {() => number}
|
|
74
|
+
* @param {(media: Media) => number} [options.artboardWidth] the artboard's width when it is not the media's own (a layout blended between two bands), else 0; read on every frame
|
|
75
|
+
* @param {(media: Media) => number} [options.artboardHeight] the artboard's height when it is not the media's own (a layout sized to the canvas), else 0; read on every frame
|
|
76
76
|
* @param {() => void} [options.onTick] time has passed on a mixed media
|
|
77
77
|
* @param {() => boolean} [options.isPinned] the animation must stay vector: pointer listeners hit-test its drawn shapes
|
|
78
78
|
* @param {(cue: Cue) => void} options.onCue an audio cue the timeline passed
|
|
@@ -287,13 +287,16 @@ export function createPlayer(canvas, options) {
|
|
|
287
287
|
/** The animation's space onto the canvas's, as the bundle's fit and alignment say. */
|
|
288
288
|
function fitMatrix() {
|
|
289
289
|
// between two layout bands that blend the artboard is as big as the canvas, which only a vector picture can be drawn at
|
|
290
|
-
const
|
|
291
|
-
const
|
|
290
|
+
const shown = /** @type {Media} */ (media)
|
|
291
|
+
const blended = !useRaster && options.artboardWidth ? options.artboardWidth(shown) : 0
|
|
292
|
+
const blendedHeight = !useRaster && options.artboardHeight ? options.artboardHeight(shown) : 0
|
|
292
293
|
const width = blended || (/** @type {Media} */ (media)).width || canvas.width
|
|
293
294
|
const height = blendedHeight || (/** @type {Media} */ (media)).height || canvas.height
|
|
294
295
|
const placement = options.placement ? options.placement() : { fit: 'contain', alignment: /** @type {[number, number]} */ ([ 0, 0 ]) }
|
|
295
296
|
const ratio = canvas.clientWidth > 0 ? canvas.width / canvas.clientWidth : 1
|
|
296
|
-
|
|
297
|
+
// an artboard `layout` does not size to the canvas (it holds its own size) draws at the layout's scale, as Rive's does
|
|
298
|
+
const isHeld = placement.fit === 'layout' && !blended && !blendedHeight
|
|
299
|
+
return fitInto(isHeld ? 'none' : placement.fit, placement.alignment, width, height, canvas.width, canvas.height, ratio)
|
|
297
300
|
}
|
|
298
301
|
|
|
299
302
|
function render() {
|
|
@@ -371,7 +374,24 @@ export function createPlayer(canvas, options) {
|
|
|
371
374
|
wake,
|
|
372
375
|
setControl,
|
|
373
376
|
render,
|
|
377
|
+
/** Shows changed values: on the frame already coming while it plays, else at once, so values set many times a frame draw once */
|
|
378
|
+
redraw() {
|
|
379
|
+
// a finished animation keeps drawing while something beside it runs (scripts, nested machines)
|
|
380
|
+
const isDrawing = playback === 'playing' || (playback === 'finished' && !!options.keepAlive && options.keepAlive())
|
|
381
|
+
const isFrameComing = !!frameHandle && isDrawing
|
|
382
|
+
if (!isFrameComing) render()
|
|
383
|
+
},
|
|
374
384
|
resize,
|
|
385
|
+
/** Steps once more, though the animation has finished, for something beside it that has work (a script that reads a value that changed) */
|
|
386
|
+
nudge() {
|
|
387
|
+
if (!media || frameHandle || options.manual || playback !== 'finished') return
|
|
388
|
+
last = now()
|
|
389
|
+
frameHandle = raf(tick)
|
|
390
|
+
},
|
|
391
|
+
/** The frame of the vector animation showing: where its slots' properties are read */
|
|
392
|
+
get frame() {
|
|
393
|
+
return media && media.vector ? media.vector.inPoint + elapsed * media.fps : 0
|
|
394
|
+
},
|
|
375
395
|
/** With `manual`, moves time on by these seconds and draws. */
|
|
376
396
|
advance(/** @type {number} */ seconds) {
|
|
377
397
|
if (options.manual) step(seconds)
|