@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
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Rive scripting host: runs one compiled script on the Luau VM the way
|
|
3
|
+
* Rive's runtime does, and gives the player what the script does (view-model
|
|
4
|
+
* writes through the adapter, drawings, a path effect's output).
|
|
5
|
+
*
|
|
6
|
+
* A compiled script's main chunk returns a factory function. The host calls
|
|
7
|
+
* it to make the script's table (`self`), sets the script's inputs on it, then
|
|
8
|
+
* drives it with `self` as the first argument:
|
|
9
|
+
*
|
|
10
|
+
* - `init(self, context)` once, returning false to refuse;
|
|
11
|
+
* - `advance(self, seconds)` each frame while it keeps returning true (a false
|
|
12
|
+
* stops them until a view-model change or an input wakes it);
|
|
13
|
+
* - `update(self)` when an input changed or `context:markNeedsUpdate()` asked
|
|
14
|
+
* (a request made from inside `update` is ignored, as in Rive);
|
|
15
|
+
* - `draw(self, renderer)` each frame, which the host records;
|
|
16
|
+
* - for a path effect, `update(self, path, node)` returns the new path;
|
|
17
|
+
* - for a converter, `convert(self, value)` and `reverseConvert(self, value)`;
|
|
18
|
+
* - for a listener action, `perform(self, pointerEvent)`;
|
|
19
|
+
* - for a transition condition, `evaluate(self)`;
|
|
20
|
+
* - for a layout, `measure(self)` and `resize(self, size)`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createLuau } from '../../luau/luau.js'
|
|
24
|
+
import { LuaClosure, LuaTable, NativeFunction, Vector } from '../../luau/vm/vm.value.js'
|
|
25
|
+
import { LuauError, encodeUtf8 } from '../../luau/luau.convert.js'
|
|
26
|
+
import { Context } from '../context/context.js'
|
|
27
|
+
import { viewModelAt } from '../context/context.viewmodel.js'
|
|
28
|
+
import { commandsToContours } from '../api/draw/draw.contours.js'
|
|
29
|
+
import { Path, pathFromContours } from '../api/draw/draw.path.js'
|
|
30
|
+
import { Recorder, Renderer } from '../api/draw/draw.renderer.js'
|
|
31
|
+
import { dataValue, readDataValue } from '../api/data/data.value.js'
|
|
32
|
+
import { installGlobals } from './host.globals.js'
|
|
33
|
+
import { NodeReadData } from './host.node.js'
|
|
34
|
+
import { createAssets } from '../api/assets/assets.js'
|
|
35
|
+
import { PointerEvent, pointerData } from '../api/pointer/pointer.event.js'
|
|
36
|
+
|
|
37
|
+
/** @import { Contour } from '../api/draw/draw.contours.js' */
|
|
38
|
+
/** @import { Drawing } from '../api/draw/draw.renderer.js' */
|
|
39
|
+
/** @import { Scope, ViewModelAdapter } from '../context/context.viewmodel.js' */
|
|
40
|
+
/** @import { NodeInfo } from './host.node.js' */
|
|
41
|
+
|
|
42
|
+
/** a pointer event at the listener's origin, shared since a Vector is never mutated */
|
|
43
|
+
const ORIGIN = new Vector(0, 0, 0)
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {object} HostOptions
|
|
47
|
+
* @property {import('../../luau/luau.js').Luau} [luau] a VM to run in; default a new one
|
|
48
|
+
* @property {Uint8Array} [bytes] the script's bytecode (see `load`)
|
|
49
|
+
* @property {ViewModelAdapter | null} [viewModel] the view model bound to the script's node, `context:viewModel()`; null for none
|
|
50
|
+
* @property {ViewModelAdapter | null} [rootViewModel] `context:rootViewModel()`; default the same as `viewModel`
|
|
51
|
+
* @property {ViewModelAdapter | null} [parentViewModel] the enclosing data context's, `dataContext():parent():viewModel()`
|
|
52
|
+
* @property {Record<string, ViewModelAdapter>} [globalViewModels] `context:globalViewModel(name)`
|
|
53
|
+
* @property {Record<string, any>} [inputs] the script's inputs by name: numbers, booleans, strings, ARGB colour numbers, or a view-model adapter for a view-model input
|
|
54
|
+
* @property {(message: string) => void} [log] where `print` and `context.log` write
|
|
55
|
+
* @property {(error: Error) => void} [onError] where an error a script raises goes; default logs it
|
|
56
|
+
* @property {boolean} [strict] rethrow a script's errors instead of reporting them
|
|
57
|
+
* @property {(drawings: Drawing[]) => void} [onDraw] called with each frame's drawings
|
|
58
|
+
* @property {import('../api/assets/assets.js').AssetSource} [assets] the images, fonts, audio and artboards the player has, by name; without it every name is accepted (an image of unknown size, the fallback font)
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A script's bytecode without Rive's wrapper: a script asset stores one byte
|
|
63
|
+
* (0) before the Luau version byte, and valid bytecode never starts with 0.
|
|
64
|
+
*
|
|
65
|
+
* @param {Uint8Array} bytes
|
|
66
|
+
* @returns {Uint8Array}
|
|
67
|
+
*/
|
|
68
|
+
export function stripWrapper(bytes) {
|
|
69
|
+
return bytes.length > 1 && bytes[0] === 0 ? bytes.subarray(1) : bytes
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One script, instantiated */
|
|
73
|
+
export class ScriptHost {
|
|
74
|
+
/** @param {HostOptions} [options] */
|
|
75
|
+
constructor(options) {
|
|
76
|
+
const o = options || {}
|
|
77
|
+
this.options = o
|
|
78
|
+
/** @type {string[]} */
|
|
79
|
+
this.logged = []
|
|
80
|
+
/** @type {Error[]} errors the script raised, in order */
|
|
81
|
+
this.errors = []
|
|
82
|
+
this.luau = o.luau || createLuau({ print: (text) => this.log(text) })
|
|
83
|
+
/** @type {import('../api/media/media.audio.js').AudioEvent[]} */
|
|
84
|
+
this.audioEvents = []
|
|
85
|
+
this.assets = createAssets(o.assets)
|
|
86
|
+
installGlobals(this.luau, { audio: this.audioEvents, fallback: this.assets.fallback })
|
|
87
|
+
this.recorder = new Recorder()
|
|
88
|
+
/** @type {Record<number, Vector>} where each pointer was last */
|
|
89
|
+
this.pointers = {}
|
|
90
|
+
/** @type {LuaTable | null} the script's own table */
|
|
91
|
+
this.self = null
|
|
92
|
+
/** @type {{ adapter: ViewModelAdapter, scope: Scope }[]} */
|
|
93
|
+
this.scopes = []
|
|
94
|
+
/** `update` is owed */
|
|
95
|
+
this.needsUpdate = false
|
|
96
|
+
/** `advance` is still wanted */
|
|
97
|
+
this.advancing = true
|
|
98
|
+
/** `update` is running, when `markNeedsUpdate` does nothing */
|
|
99
|
+
this.updating = false
|
|
100
|
+
this.initialized = false
|
|
101
|
+
/** false once `init` returned false or raised */
|
|
102
|
+
this.running = true
|
|
103
|
+
this.context = Context.wrap(this.contextEnv())
|
|
104
|
+
/** the DataValue and the PointerEvent reused for each protocol call, so a call allocates none of its own @type {{ in: any, event: any, eventData: any }} */
|
|
105
|
+
this.scratch = { in: null, event: null, eventData: null }
|
|
106
|
+
if (o.bytes) this.load(o.bytes)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} message
|
|
111
|
+
*/
|
|
112
|
+
log(message) {
|
|
113
|
+
this.logged.push(message)
|
|
114
|
+
if (this.options.log) this.options.log(message)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* What the Context stands on.
|
|
119
|
+
*
|
|
120
|
+
* @returns {import('../context/context.js').ContextEnv}
|
|
121
|
+
*/
|
|
122
|
+
contextEnv() {
|
|
123
|
+
const o = this.options
|
|
124
|
+
const viewModel = o.viewModel || null
|
|
125
|
+
return {
|
|
126
|
+
scopeFor: (adapter) => this.scopeFor(adapter),
|
|
127
|
+
immediate: viewModel,
|
|
128
|
+
root: o.rootViewModel === undefined ? viewModel : o.rootViewModel,
|
|
129
|
+
parent: o.parentViewModel || null,
|
|
130
|
+
globals: o.globalViewModels || {},
|
|
131
|
+
assets: this.assets,
|
|
132
|
+
markNeedsUpdate: () => {
|
|
133
|
+
if (!this.updating) this.needsUpdate = true
|
|
134
|
+
},
|
|
135
|
+
log: (message) => this.log(message),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {ViewModelAdapter} adapter
|
|
141
|
+
* @returns {Scope}
|
|
142
|
+
*/
|
|
143
|
+
scopeFor(adapter) {
|
|
144
|
+
for (let n = 0; n < this.scopes.length; n++) if (this.scopes[n].adapter === adapter) return this.scopes[n].scope
|
|
145
|
+
/** @type {Scope} */
|
|
146
|
+
const scope = {
|
|
147
|
+
adapter,
|
|
148
|
+
props: {},
|
|
149
|
+
scopeFor: (a) => this.scopeFor(a),
|
|
150
|
+
asset: (kind, name) => (kind === 'image' ? this.assets.image(name) : kind === 'font' ? this.assets.font(name) : undefined),
|
|
151
|
+
call: (fn, args) => {
|
|
152
|
+
this.guard(() => this.luau.callRaw(fn, args))
|
|
153
|
+
},
|
|
154
|
+
wake: () => {
|
|
155
|
+
this.advancing = true
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
this.scopes.push({ adapter, scope })
|
|
159
|
+
return scope
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Runs host work that calls into Lua, keeping a script's error out of the player.
|
|
164
|
+
*
|
|
165
|
+
* @template T
|
|
166
|
+
* @param {() => T} work
|
|
167
|
+
* @returns {T | undefined}
|
|
168
|
+
*/
|
|
169
|
+
guard(work) {
|
|
170
|
+
try {
|
|
171
|
+
return work()
|
|
172
|
+
} catch (e) {
|
|
173
|
+
if (!(e instanceof LuauError) || this.options.strict) throw e
|
|
174
|
+
this.errors.push(e)
|
|
175
|
+
if (this.options.onError) this.options.onError(e)
|
|
176
|
+
else this.log('error: ' + e.message)
|
|
177
|
+
return undefined
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Loading
|
|
182
|
+
// ■■■■■■■
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Loads the script and makes its table. The bytes may still carry Rive's
|
|
186
|
+
* wrapper byte (see `stripWrapper`).
|
|
187
|
+
*
|
|
188
|
+
* @param {Uint8Array} bytes
|
|
189
|
+
* @returns {this}
|
|
190
|
+
*/
|
|
191
|
+
load(bytes) {
|
|
192
|
+
const module = this.luau.load(stripWrapper(bytes))
|
|
193
|
+
const returned = this.luau.callRaw(this.luau.vm.instantiate(module), [])
|
|
194
|
+
const factory = returned[0]
|
|
195
|
+
if (!(factory instanceof NativeFunction) && !(factory instanceof LuaClosure)) throw new Error("a script's main chunk must return a factory function")
|
|
196
|
+
const made = this.luau.callRaw(factory, [])[0]
|
|
197
|
+
if (!(made instanceof LuaTable)) throw new Error("a script's factory must return a table")
|
|
198
|
+
this.self = made
|
|
199
|
+
const inputs = this.options.inputs
|
|
200
|
+
if (inputs) for (const name in inputs) this.setValue(name, inputs[name])
|
|
201
|
+
return this
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {string} name
|
|
206
|
+
* @param {any} value
|
|
207
|
+
*/
|
|
208
|
+
setValue(name, value) {
|
|
209
|
+
if (!this.self) throw new Error('no script is loaded')
|
|
210
|
+
this.self.set(encodeUtf8(name), this.inputValue(value))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {any} value
|
|
215
|
+
* @returns {any}
|
|
216
|
+
*/
|
|
217
|
+
inputValue(value) {
|
|
218
|
+
if (typeof value === 'string') return encodeUtf8(value)
|
|
219
|
+
if (value && typeof value === 'object' && typeof value.kind === 'string' && typeof value.name === 'string') return this.assets.input(value)
|
|
220
|
+
if (value && typeof value === 'object' && typeof value.subscribe === 'function') return viewModelAt(this.scopeFor(value), '')
|
|
221
|
+
return value === null ? undefined : value
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Sets a script input, as the player does when it changes (an animated or
|
|
226
|
+
* data-bound input); `update` runs next.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} name
|
|
229
|
+
* @param {any} value
|
|
230
|
+
*/
|
|
231
|
+
setInput(name, value) {
|
|
232
|
+
this.setValue(name, value)
|
|
233
|
+
this.needsUpdate = true
|
|
234
|
+
this.advancing = true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {string} name
|
|
239
|
+
* @returns {any} the method, or undefined when the script has none
|
|
240
|
+
*/
|
|
241
|
+
method(name) {
|
|
242
|
+
const fn = this.self ? this.self.getStr(name) : undefined
|
|
243
|
+
return fn === undefined ? undefined : fn
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {string} name
|
|
248
|
+
* @returns {boolean}
|
|
249
|
+
*/
|
|
250
|
+
has(name) {
|
|
251
|
+
return this.method(name) !== undefined
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Calls a method with `self` first.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} name
|
|
258
|
+
* @param {any[]} args
|
|
259
|
+
* @returns {{ ok: boolean, result: any }}
|
|
260
|
+
*/
|
|
261
|
+
invoke(name, args) {
|
|
262
|
+
const fn = this.method(name)
|
|
263
|
+
if (fn === undefined) return { ok: true, result: undefined }
|
|
264
|
+
let ok = false
|
|
265
|
+
/** @type {any} */
|
|
266
|
+
let result
|
|
267
|
+
this.guard(() => {
|
|
268
|
+
result = this.luau.callRaw(fn, [ this.self ].concat(args))[0]
|
|
269
|
+
ok = true
|
|
270
|
+
})
|
|
271
|
+
return { ok, result }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Protocols
|
|
275
|
+
// ■■■■■■■■■
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Converter scripts: `convert(self, input)` turns a view-model value into
|
|
279
|
+
* the target's. A number, string or boolean goes in by its type; a colour
|
|
280
|
+
* (an ARGB number) by `kind`.
|
|
281
|
+
*
|
|
282
|
+
* @param {any} value
|
|
283
|
+
* @param {string} [kind] `color` for a colour
|
|
284
|
+
* @returns {{ kind: string, value: any } | undefined} what the script returned (a colour is its ARGB number); undefined when the script has no such method, failed, or returned something but a DataValue
|
|
285
|
+
*/
|
|
286
|
+
convert(value, kind) {
|
|
287
|
+
return this.dataCall('convert', value, kind)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The same for `reverseConvert(self, input)`, which a two-way bind uses to
|
|
292
|
+
* turn the target's value back into the source's.
|
|
293
|
+
*
|
|
294
|
+
* @param {any} value
|
|
295
|
+
* @param {string} [kind]
|
|
296
|
+
* @returns {{ kind: string, value: any } | undefined}
|
|
297
|
+
*/
|
|
298
|
+
reverseConvert(value, kind) {
|
|
299
|
+
return this.dataCall('reverseConvert', value, kind)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {string} name
|
|
304
|
+
* @param {any} value
|
|
305
|
+
* @param {string} [kind]
|
|
306
|
+
* @returns {{ kind: string, value: any } | undefined}
|
|
307
|
+
*/
|
|
308
|
+
dataCall(name, value, kind) {
|
|
309
|
+
if (!this.initialized) this.init()
|
|
310
|
+
if (!this.running || !this.has(name)) return undefined
|
|
311
|
+
this.scratch.in = dataValue(this.scratch.in, value, kind)
|
|
312
|
+
const r = this.invoke(name, [ this.scratch.in ])
|
|
313
|
+
const out = r.ok ? readDataValue(r.result) : undefined
|
|
314
|
+
return out ? { kind: out.kind, value: out.value } : undefined
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Listener action scripts: `perform(self, pointerEvent)`.
|
|
319
|
+
*
|
|
320
|
+
* @param {{ id?: number, x?: number, y?: number, previousX?: number, previousY?: number, type?: string, timeStamp?: number }} [event] the pointer event, in the listener's local coordinates; none is a pointer at the origin
|
|
321
|
+
* @returns {{ ok: boolean, hit: string }} `hit` is what the script did with the event: "" nothing, "opaque" or "translucent"
|
|
322
|
+
*/
|
|
323
|
+
perform(event) {
|
|
324
|
+
if (!this.initialized) this.init()
|
|
325
|
+
if (!this.running || !this.has('perform')) return { ok: false, hit: '' }
|
|
326
|
+
const e = event || {}
|
|
327
|
+
const s = this.scratch
|
|
328
|
+
if (!s.event) {
|
|
329
|
+
s.eventData = pointerData(0, ORIGIN, ORIGIN, 'move', 0)
|
|
330
|
+
s.event = PointerEvent.wrap(s.eventData)
|
|
331
|
+
}
|
|
332
|
+
const d = s.eventData
|
|
333
|
+
d.id = e.id || 0
|
|
334
|
+
d.position = e.x || e.y ? new Vector(e.x || 0, e.y || 0, 0) : ORIGIN
|
|
335
|
+
d.previous = e.previousX === undefined ? d.position : new Vector(e.previousX, e.previousY || 0, 0)
|
|
336
|
+
d.type = e.type || 'move'
|
|
337
|
+
d.time = e.timeStamp || 0
|
|
338
|
+
d.hit = false
|
|
339
|
+
d.translucent = false
|
|
340
|
+
const r = this.invoke('perform', [ s.event ])
|
|
341
|
+
return { ok: r.ok, hit: d.hit ? (d.translucent ? 'translucent' : 'opaque') : '' }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Transition condition scripts: `evaluate(self)`; true lets the transition pass.
|
|
346
|
+
*
|
|
347
|
+
* @returns {boolean} false when the script has no `evaluate` or fails
|
|
348
|
+
*/
|
|
349
|
+
evaluate() {
|
|
350
|
+
if (!this.initialized) this.init()
|
|
351
|
+
if (!this.running || !this.has('evaluate')) return false
|
|
352
|
+
const r = this.invoke('evaluate', [])
|
|
353
|
+
return r.ok && r.result === true
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Layout scripts: `measure(self)` proposes an ideal size.
|
|
358
|
+
*
|
|
359
|
+
* @returns {{ width: number, height: number } | null} null when the script has no `measure` or returns no vector
|
|
360
|
+
*/
|
|
361
|
+
measure() {
|
|
362
|
+
if (!this.initialized) this.init()
|
|
363
|
+
if (!this.running || !this.has('measure')) return null
|
|
364
|
+
const r = this.invoke('measure', [])
|
|
365
|
+
const v = r.result
|
|
366
|
+
return r.ok && v instanceof Vector ? { width: v.x, height: v.y } : null
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Layout scripts: `resize(self, size)`, called when the layout gets its size.
|
|
371
|
+
*
|
|
372
|
+
* @param {number} width
|
|
373
|
+
* @param {number} height
|
|
374
|
+
* @returns {boolean} whether the script ran
|
|
375
|
+
*/
|
|
376
|
+
resize(width, height) {
|
|
377
|
+
if (!this.initialized) this.init()
|
|
378
|
+
if (!this.running || !this.has('resize')) return false
|
|
379
|
+
return this.invoke('resize', [ new Vector(width, height, 0) ]).ok
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Lifecycle
|
|
383
|
+
// ■■■■■■■■■
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Calls `init`. Whatever it returns but false counts as success; an error
|
|
387
|
+
* or false stops the script.
|
|
388
|
+
*
|
|
389
|
+
* @returns {boolean}
|
|
390
|
+
*/
|
|
391
|
+
init() {
|
|
392
|
+
if (this.initialized) return this.running
|
|
393
|
+
this.initialized = true
|
|
394
|
+
const r = this.invoke('init', [ this.context ])
|
|
395
|
+
this.running = r.ok && r.result !== false
|
|
396
|
+
this.needsUpdate = true
|
|
397
|
+
return this.running
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Calls `advance` while the script wants it.
|
|
402
|
+
*
|
|
403
|
+
* @param {number} seconds
|
|
404
|
+
* @returns {boolean} whether the script wants more advances
|
|
405
|
+
*/
|
|
406
|
+
advance(seconds) {
|
|
407
|
+
if (!this.running || !this.advancing || !this.has('advance')) return false
|
|
408
|
+
const r = this.invoke('advance', [ seconds ])
|
|
409
|
+
if (!r.ok) {
|
|
410
|
+
this.running = false
|
|
411
|
+
return false
|
|
412
|
+
}
|
|
413
|
+
this.advancing = r.result !== false
|
|
414
|
+
return this.advancing
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Calls `update` if an input changed or an update was asked for.
|
|
419
|
+
*
|
|
420
|
+
* @returns {boolean} whether it ran
|
|
421
|
+
*/
|
|
422
|
+
update() {
|
|
423
|
+
if (!this.running || !this.needsUpdate) return false
|
|
424
|
+
this.needsUpdate = false
|
|
425
|
+
if (!this.has('update')) return false
|
|
426
|
+
this.updating = true
|
|
427
|
+
try {
|
|
428
|
+
this.invoke('update', [])
|
|
429
|
+
} finally {
|
|
430
|
+
this.updating = false
|
|
431
|
+
}
|
|
432
|
+
return true
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* One frame: `advance`, then `update` when owed.
|
|
437
|
+
*
|
|
438
|
+
* @param {number} seconds
|
|
439
|
+
* @returns {{ advanced: boolean, updated: boolean }}
|
|
440
|
+
*/
|
|
441
|
+
frame(seconds) {
|
|
442
|
+
const advanced = this.advance(seconds)
|
|
443
|
+
return { advanced, updated: this.update() }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Calls `draw` and returns what it drew.
|
|
448
|
+
*
|
|
449
|
+
* @param {Drawing[]} [into] a list to fill (emptied first) instead of a new one, so a player that draws every frame allocates no list
|
|
450
|
+
* @returns {Drawing[]}
|
|
451
|
+
*/
|
|
452
|
+
draw(into) {
|
|
453
|
+
const rec = this.recorder
|
|
454
|
+
rec.reset(into)
|
|
455
|
+
if (this.running && this.has('draw')) this.invoke('draw', [ Renderer.wrap(rec) ])
|
|
456
|
+
if (this.options.onDraw) this.options.onDraw(rec.drawings)
|
|
457
|
+
return rec.drawings
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* The audio the script started, stopped or seeked since the last call.
|
|
462
|
+
*
|
|
463
|
+
* @returns {import('../api/media/media.audio.js').AudioEvent[]}
|
|
464
|
+
*/
|
|
465
|
+
audio() {
|
|
466
|
+
return this.audioEvents.splice(0, this.audioEvents.length)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* A pointer event for the scripted node, at a point in the node's own space:
|
|
471
|
+
* calls `pointerDown`, `pointerMove`, `pointerUp` or `pointerExit` if the
|
|
472
|
+
* script has it.
|
|
473
|
+
*
|
|
474
|
+
* @param {'down' | 'move' | 'up' | 'exit'} type
|
|
475
|
+
* @param {number} x
|
|
476
|
+
* @param {number} y
|
|
477
|
+
* @param {number} [id]
|
|
478
|
+
* @param {number} [seconds] the event's time stamp
|
|
479
|
+
* @returns {{ hit: boolean, translucent: boolean }} whether the script claimed it (`event:hit()`), and let it through
|
|
480
|
+
*/
|
|
481
|
+
pointer(type, x, y, id, seconds) {
|
|
482
|
+
const key = id || 0
|
|
483
|
+
const at = new Vector(x, y, 0)
|
|
484
|
+
const name = 'pointer' + type.charAt(0).toUpperCase() + type.slice(1)
|
|
485
|
+
const data = pointerData(key, at, this.pointers[key] || at, name, seconds || 0)
|
|
486
|
+
if (type === 'exit') delete this.pointers[key]
|
|
487
|
+
else this.pointers[key] = at
|
|
488
|
+
if (this.running && this.has(name)) this.invoke(name, [ PointerEvent.wrap(data) ])
|
|
489
|
+
return { hit: data.hit, translucent: data.translucent }
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Runs a path effect: `update(self, path, node)` on a path given as Lottie
|
|
494
|
+
* vertex arrays. A script without `update`, or one that fails, leaves the
|
|
495
|
+
* path as it was.
|
|
496
|
+
*
|
|
497
|
+
* @param {Contour[]} contours
|
|
498
|
+
* @param {NodeInfo} [node]
|
|
499
|
+
* @returns {Contour[]}
|
|
500
|
+
*/
|
|
501
|
+
effect(contours, node) {
|
|
502
|
+
if (!this.running || !this.has('update')) return contours
|
|
503
|
+
const path = pathFromContours(contours)
|
|
504
|
+
const data = NodeReadData.wrap({ info: node || {}, path })
|
|
505
|
+
this.updating = true
|
|
506
|
+
this.needsUpdate = false
|
|
507
|
+
let out
|
|
508
|
+
try {
|
|
509
|
+
out = this.invoke('update', [ path, data ])
|
|
510
|
+
} finally {
|
|
511
|
+
this.updating = false
|
|
512
|
+
}
|
|
513
|
+
const made = out.ok ? Path.unwrap(out.result) : undefined
|
|
514
|
+
return made ? commandsToContours(made.cmds) : contours
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Makes a host for a script.
|
|
520
|
+
*
|
|
521
|
+
* ```js
|
|
522
|
+
* const host = createScriptHost({ bytes, viewModel: createMemoryViewModel({ score: 0 }), inputs: { speed: 2 } })
|
|
523
|
+
* host.init()
|
|
524
|
+
* host.frame(1 / 60)
|
|
525
|
+
* const drawings = host.draw()
|
|
526
|
+
* ```
|
|
527
|
+
*
|
|
528
|
+
* @param {HostOptions} [options]
|
|
529
|
+
* @returns {ScriptHost}
|
|
530
|
+
*/
|
|
531
|
+
export function createScriptHost(options) {
|
|
532
|
+
return new ScriptHost(options)
|
|
533
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NodeReadData`: the node a path effect belongs to, as the effect's `update`
|
|
3
|
+
* sees it (its transform, and the paint of the stroke or fill the effect is
|
|
4
|
+
* on). The player describes the node in plain numbers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Vector } from '../../luau/vm/vm.value.js'
|
|
8
|
+
import { defineType } from '../api/api.type.js'
|
|
9
|
+
import { Mat2D } from '../api/math/math.mat2d.js'
|
|
10
|
+
import { Paint, defaultPaint } from '../api/draw/draw.paint.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} NodeInfo
|
|
14
|
+
* @property {number} [x]
|
|
15
|
+
* @property {number} [y]
|
|
16
|
+
* @property {number} [rotation] radians
|
|
17
|
+
* @property {number} [scaleX]
|
|
18
|
+
* @property {number} [scaleY]
|
|
19
|
+
* @property {number[]} [worldTransform] [xx, xy, yx, yy, tx, ty]
|
|
20
|
+
* @property {import('../api/draw/draw.paint.js').PaintData} [paint] the stroke or fill the effect is on
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} NodeData
|
|
25
|
+
* @property {NodeInfo} info
|
|
26
|
+
* @property {any} path the PathData wrapper the effect is given
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const NodeReadData = defineType('NodeReadData', {
|
|
30
|
+
fields: {
|
|
31
|
+
x: { get: (n) => n.info.x || 0 },
|
|
32
|
+
y: { get: (n) => n.info.y || 0 },
|
|
33
|
+
rotation: { get: (n) => n.info.rotation || 0 },
|
|
34
|
+
scaleX: { get: (n) => (n.info.scaleX === undefined ? 1 : n.info.scaleX) },
|
|
35
|
+
scaleY: { get: (n) => (n.info.scaleY === undefined ? 1 : n.info.scaleY) },
|
|
36
|
+
position: { get: (n) => new Vector(n.info.x || 0, n.info.y || 0, 0) },
|
|
37
|
+
scale: { get: (n) => new Vector(n.info.scaleX === undefined ? 1 : n.info.scaleX, n.info.scaleY === undefined ? 1 : n.info.scaleY, 0) },
|
|
38
|
+
worldTransform: { get: (n) => Mat2D.wrap(n.info.worldTransform ? n.info.worldTransform.slice() : [ 1, 0, 0, 1, 0, 0 ]) },
|
|
39
|
+
paint: { get: (n) => Paint.wrap(n.info.paint || defaultPaint()) },
|
|
40
|
+
},
|
|
41
|
+
methods: {
|
|
42
|
+
asPath: (n) => n.path,
|
|
43
|
+
asPaint: (n) => Paint.wrap(n.info.paint || defaultPaint()),
|
|
44
|
+
},
|
|
45
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Rive scripting host: runs the Luau scripts a Rive file carries (node,
|
|
3
|
+
* drawable and path-effect scripts) on the Luau VM beside it, behind an API
|
|
4
|
+
* a player implements for its view model. See `rive.md`.
|
|
5
|
+
*
|
|
6
|
+
* ```js
|
|
7
|
+
* import { createScriptHost, createMemoryViewModel } from '@px-petals/roku.motion-web/script/rive/rive.js'
|
|
8
|
+
*
|
|
9
|
+
* const host = createScriptHost({ bytes, viewModel: createMemoryViewModel({ score: 0 }), inputs: { speed: 2 } })
|
|
10
|
+
* host.init()
|
|
11
|
+
* host.frame(1 / 60)
|
|
12
|
+
* const drawings = host.draw()
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export { ScriptHost, createScriptHost, stripWrapper } from './host/host.js'
|
|
17
|
+
export { createMemoryViewModel } from './context/context.memory.js'
|
|
18
|
+
export { commandsToContours, contoursToCommands, transformContours } from './api/draw/draw.contours.js'
|
|
19
|
+
|
|
20
|
+
/** @typedef {import('./host/host.js').HostOptions} HostOptions */
|
|
21
|
+
/** @typedef {import('./context/context.viewmodel.js').ViewModelAdapter} ViewModelAdapter */
|
|
22
|
+
/** @typedef {import('./api/draw/draw.renderer.js').Drawing} Drawing */
|
|
23
|
+
/** @typedef {import('./api/draw/draw.contours.js').Contour} Contour */
|