@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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scripted objects of a bundle (`scriptObjects` in motion.json): nodes,
|
|
3
|
+
* drawables and path effects that run every frame, on the player's clock.
|
|
4
|
+
*
|
|
5
|
+
* One host per entry, run in the entry's `order`. A host steps (`advance`,
|
|
6
|
+
* then `update`) when it has never run, when its entry has a `clock`, or when
|
|
7
|
+
* a view-model path in its `reads` changed; any other frame it is skipped. A
|
|
8
|
+
* drawable's drawing fills the entry's `draws` slots and a path effect's
|
|
9
|
+
* result its `effect` slot, both through `outputs`, which the player merges
|
|
10
|
+
* into the slot table of every animation it draws.
|
|
11
|
+
*
|
|
12
|
+
* Rive's first advance after a view model is bound only binds: it runs no
|
|
13
|
+
* script. The first `step` here does the same; the next runs `init`,
|
|
14
|
+
* `advance` and `update` together (see `script/rive/rive.md`).
|
|
15
|
+
*
|
|
16
|
+
* Nothing is allocated in a step beyond what the hosts allocate: the draw
|
|
17
|
+
* lists, colours and the contour list of an effect are kept and reused.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createValuesViewModel } from '../rive/context/lists/lists.values.js'
|
|
21
|
+
import { assetSource, scoped } from './play.scope.js'
|
|
22
|
+
|
|
23
|
+
/** @import { ScriptHost } from '../rive/host/host.js' */
|
|
24
|
+
|
|
25
|
+
/** A path with no vertices, which draws nothing: what a slot holds when its drawable drew fewer paths */
|
|
26
|
+
const EMPTY = { v: [], i: [], o: [], c: false }
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One entry of `scriptObjects`.
|
|
30
|
+
*
|
|
31
|
+
* @typedef {object} ObjectEntry
|
|
32
|
+
* @property {string} id the bytecode's name in `manifest.scripts`
|
|
33
|
+
* @property {string} [name]
|
|
34
|
+
* @property {string} kind node, drawable or pathEffect
|
|
35
|
+
* @property {number} [order] the run order across the bundle
|
|
36
|
+
* @property {string} [scope] the view-model path its context is bound at
|
|
37
|
+
* @property {Record<string, any>} [inputs]
|
|
38
|
+
* @property {string[]} [reads] view-model paths it reads, under `scope`
|
|
39
|
+
* @property {string[]} [writes]
|
|
40
|
+
* @property {boolean} [clock] runs every frame, not only when its reads change
|
|
41
|
+
* @property {{ sid: string, fill?: string, stroke?: string, width?: string }[]} [draws] a drawable's path slots, and the slots its paint goes to
|
|
42
|
+
* @property {{ sid: string, source?: string }} [effect] a path effect's output slot, and the slot holding the path it applies to
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What the player gives the objects.
|
|
47
|
+
*
|
|
48
|
+
* @typedef {object} ObjectsEnv
|
|
49
|
+
* @property {any} data the bundle's view model (`manifest.data`)
|
|
50
|
+
* @property {() => Record<string, any>} values the live values
|
|
51
|
+
* @property {(path: string, value: any) => void} write sets a value now, for the scripts after it to read, and holds it for `commit`
|
|
52
|
+
* @property {(path: string) => void} fire fires a trigger, held for `commit`
|
|
53
|
+
* @property {() => boolean} commit hands the player what was written and fired since the last call; whether that redrew the slots
|
|
54
|
+
* @property {() => void} publish puts `outputs` into the slots now
|
|
55
|
+
* @property {(sid: string) => any} probe the value a slot's property has now: a path's geometry, for an effect
|
|
56
|
+
* @property {Record<string, string>} [families] the bundle's fonts, by name, as registered
|
|
57
|
+
* @property {Record<string, { width: number, height: number }>} [images] the images the bundle lists, by name
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {object} Objects
|
|
62
|
+
* @property {Record<string, any>} outputs slot id → value, for the slots the objects fill
|
|
63
|
+
* @property {string[]} sids every slot id in `outputs`
|
|
64
|
+
* @property {boolean} live whether the next step would run a host: one has not run, has a clock, or has a read that changed
|
|
65
|
+
* @property {boolean} clocked whether some host runs every frame, which keeps a finished animation stepping
|
|
66
|
+
* @property {number} count hosts made
|
|
67
|
+
* @property {(seconds: number) => void} step one frame
|
|
68
|
+
* @property {() => void} destroy releases the hosts
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {object} Item
|
|
73
|
+
* @property {ScriptHost} host
|
|
74
|
+
* @property {ObjectEntry} entry
|
|
75
|
+
* @property {string[]} reads the paths it reads, with its scope
|
|
76
|
+
* @property {any[]} seen what they held when it last ran
|
|
77
|
+
* @property {boolean} isStarted
|
|
78
|
+
* @property {boolean} isClocked
|
|
79
|
+
* @property {any[]} into the list its drawing is recorded into
|
|
80
|
+
* @property {number[][]} colors the colour slot values of its draws
|
|
81
|
+
* @property {any[]} contours the one-contour list an effect is given
|
|
82
|
+
* @property {any} input the geometry an effect applies to when its entry names no source
|
|
83
|
+
* @property {{}} node
|
|
84
|
+
* @property {boolean} isOver whether it was told it drew too much
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {ObjectEntry[]} entries
|
|
89
|
+
* @param {Record<string, Uint8Array>} bytes each script's bytecode, by `id`
|
|
90
|
+
* @param {ObjectsEnv} env
|
|
91
|
+
* @param {(message: string) => void} report
|
|
92
|
+
* @param {Function} make makes a script host
|
|
93
|
+
* @returns {Objects}
|
|
94
|
+
*/
|
|
95
|
+
export function createObjects(entries, bytes, env, report, make) {
|
|
96
|
+
/** @type {Record<string, any>} */
|
|
97
|
+
const outputs = {}
|
|
98
|
+
/** @type {string[]} */
|
|
99
|
+
const sids = []
|
|
100
|
+
const values = env.values
|
|
101
|
+
const base = createValuesViewModel({ data: env.data, values, write: env.write })
|
|
102
|
+
const hasViewModel = !!env.data
|
|
103
|
+
const fire = base.fire
|
|
104
|
+
base.fire = (path) => {
|
|
105
|
+
env.fire(path)
|
|
106
|
+
if (fire) fire(path)
|
|
107
|
+
}
|
|
108
|
+
const assets = assetSource(env.families || {}, env.images || {})
|
|
109
|
+
/** @type {Record<string, import('../rive/context/context.viewmodel.js').ViewModelAdapter>} */
|
|
110
|
+
const scopes = {}
|
|
111
|
+
|
|
112
|
+
/** @type {Item[]} */
|
|
113
|
+
const items = []
|
|
114
|
+
const ordered = entries.map((entry, n) => ({ entry, n })).filter((one) => bytes[one.entry.id]).sort((a, b) => (a.entry.order || 0) - (b.entry.order || 0) || a.n - b.n)
|
|
115
|
+
|
|
116
|
+
for (let n = 0; n < ordered.length; n++) {
|
|
117
|
+
const entry = ordered[n].entry
|
|
118
|
+
const scope = entry.scope || ''
|
|
119
|
+
const adapter = scopes[scope] || (scopes[scope] = scoped(base, scope))
|
|
120
|
+
const host = make({
|
|
121
|
+
bytes: bytes[entry.id],
|
|
122
|
+
inputs: entry.inputs,
|
|
123
|
+
// a bundle without a view model gives its scripts none, as Rive's context:viewModel() is nil there
|
|
124
|
+
viewModel: hasViewModel ? adapter : null,
|
|
125
|
+
rootViewModel: hasViewModel ? adapter : null,
|
|
126
|
+
assets,
|
|
127
|
+
log: () => {},
|
|
128
|
+
onError: (/** @type {Error} */ error) => report("script '" + entry.id + "': " + error.message),
|
|
129
|
+
})
|
|
130
|
+
const listed = entry.reads || []
|
|
131
|
+
const reads = []
|
|
132
|
+
for (let k = 0; k < listed.length; k++) reads.push(scope ? scope + '.' + listed[k] : listed[k])
|
|
133
|
+
const draws = entry.draws || []
|
|
134
|
+
const colors = []
|
|
135
|
+
for (let k = 0; k < draws.length; k++) {
|
|
136
|
+
sids.push(draws[k].sid)
|
|
137
|
+
outputs[draws[k].sid] = EMPTY
|
|
138
|
+
if (draws[k].fill) sids.push(/** @type {string} */ (draws[k].fill))
|
|
139
|
+
if (draws[k].stroke) sids.push(/** @type {string} */ (draws[k].stroke))
|
|
140
|
+
if (draws[k].width) sids.push(/** @type {string} */ (draws[k].width))
|
|
141
|
+
colors.push([ 0, 0, 0, 1 ])
|
|
142
|
+
}
|
|
143
|
+
if (entry.effect) sids.push(entry.effect.sid)
|
|
144
|
+
items.push({ host, entry, reads, seen: new Array(reads.length), isStarted: false, isClocked: !!entry.clock, into: [], colors, contours: [ null ], input: null, node: {}, isOver: false })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let isBound = false
|
|
148
|
+
let isDestroyed = false
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether a host's reads differ from when it last ran.
|
|
152
|
+
*
|
|
153
|
+
* @param {Item} item
|
|
154
|
+
* @param {boolean} isTaking whether to remember what they hold now
|
|
155
|
+
* @returns {boolean}
|
|
156
|
+
*/
|
|
157
|
+
function changed(item, isTaking) {
|
|
158
|
+
const current = values()
|
|
159
|
+
let result = false
|
|
160
|
+
for (let n = 0; n < item.reads.length; n++) {
|
|
161
|
+
const value = current[item.reads[n]]
|
|
162
|
+
if (item.seen[n] === value) continue
|
|
163
|
+
if (!isTaking) return true
|
|
164
|
+
result = true
|
|
165
|
+
item.seen[n] = value
|
|
166
|
+
}
|
|
167
|
+
return result
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** @param {Item} item */
|
|
171
|
+
function isDue(item) {
|
|
172
|
+
return !item.isStarted || item.isClocked || changed(item, false)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A drawable's drawing, into its slots.
|
|
177
|
+
*
|
|
178
|
+
* @param {Item} item
|
|
179
|
+
*/
|
|
180
|
+
function drawn(item) {
|
|
181
|
+
const draws = item.entry.draws || []
|
|
182
|
+
const list = item.host.draw(item.into)
|
|
183
|
+
let k = 0
|
|
184
|
+
let over = 0
|
|
185
|
+
for (let n = 0; n < list.length; n++) {
|
|
186
|
+
const record = list[n]
|
|
187
|
+
if (record.kind !== 'path') continue
|
|
188
|
+
const rgba = record.paint.rgba
|
|
189
|
+
const isStroke = record.paint.style === 'stroke'
|
|
190
|
+
for (let c = 0; c < record.path.length; c++) {
|
|
191
|
+
if (k >= draws.length) {
|
|
192
|
+
over++
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
const draw = draws[k]
|
|
196
|
+
outputs[draw.sid] = record.path[c]
|
|
197
|
+
const paint = isStroke ? draw.stroke : draw.fill
|
|
198
|
+
if (paint) {
|
|
199
|
+
const color = item.colors[k]
|
|
200
|
+
color[0] = rgba[0]
|
|
201
|
+
color[1] = rgba[1]
|
|
202
|
+
color[2] = rgba[2]
|
|
203
|
+
color[3] = rgba[3] * record.opacity
|
|
204
|
+
outputs[paint] = color
|
|
205
|
+
}
|
|
206
|
+
if (isStroke && draw.width) outputs[draw.width] = record.paint.thickness
|
|
207
|
+
k++
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (; k < draws.length; k++) outputs[draws[k].sid] = EMPTY
|
|
211
|
+
if (over && !item.isOver) {
|
|
212
|
+
item.isOver = true
|
|
213
|
+
report("script '" + item.entry.id + "' drew " + (draws.length + over) + ' paths, more than its ' + draws.length + ' slots; the rest are dropped')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* A path effect on the geometry of the path it applies to.
|
|
219
|
+
*
|
|
220
|
+
* @param {Item} item
|
|
221
|
+
*/
|
|
222
|
+
function effected(item) {
|
|
223
|
+
const effect = /** @type {NonNullable<ObjectEntry['effect']>} */ (item.entry.effect)
|
|
224
|
+
// without a `source` the input is the slot's own value as the bundle baked it, taken before the first write
|
|
225
|
+
const live = effect.source ? env.probe(effect.source) : undefined
|
|
226
|
+
if (live) item.input = live
|
|
227
|
+
else if (!item.input && !effect.source) item.input = env.probe(effect.sid)
|
|
228
|
+
if (!item.input) return
|
|
229
|
+
item.contours[0] = item.input
|
|
230
|
+
const out = item.host.effect(item.contours, item.node)
|
|
231
|
+
if (out !== item.contours && out.length) outputs[effect.sid] = out[0]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** @param {number} seconds */
|
|
235
|
+
function step(seconds) {
|
|
236
|
+
if (isDestroyed) return
|
|
237
|
+
if (!isBound) {
|
|
238
|
+
// Rive's first advance after binding runs no script
|
|
239
|
+
isBound = true
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// listeners hear what the player changed since the hosts last ran
|
|
244
|
+
let isOutside = false
|
|
245
|
+
for (let n = 0; n < items.length && !isOutside; n++) isOutside = items[n].isStarted && changed(items[n], false)
|
|
246
|
+
if (isOutside) base.refresh()
|
|
247
|
+
|
|
248
|
+
let isProduced = false
|
|
249
|
+
for (let n = 0; n < items.length; n++) {
|
|
250
|
+
const item = items[n]
|
|
251
|
+
if (!isDue(item)) continue
|
|
252
|
+
changed(item, true)
|
|
253
|
+
if (!item.isStarted) {
|
|
254
|
+
item.isStarted = true
|
|
255
|
+
item.host.init()
|
|
256
|
+
}
|
|
257
|
+
const kind = item.entry.kind
|
|
258
|
+
if (kind === 'pathEffect') {
|
|
259
|
+
item.host.advance(seconds)
|
|
260
|
+
effected(item)
|
|
261
|
+
isProduced = true
|
|
262
|
+
} else {
|
|
263
|
+
item.host.frame(seconds)
|
|
264
|
+
if (kind === 'drawable') {
|
|
265
|
+
drawn(item)
|
|
266
|
+
isProduced = true
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (!env.commit() && isProduced) env.publish()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
outputs,
|
|
275
|
+
sids,
|
|
276
|
+
get live() {
|
|
277
|
+
for (let n = 0; n < items.length; n++) if (isDue(items[n])) return true
|
|
278
|
+
return false
|
|
279
|
+
},
|
|
280
|
+
get clocked() {
|
|
281
|
+
for (let n = 0; n < items.length; n++) if (items[n].isClocked) return true
|
|
282
|
+
return false
|
|
283
|
+
},
|
|
284
|
+
get count() {
|
|
285
|
+
return items.length
|
|
286
|
+
},
|
|
287
|
+
step,
|
|
288
|
+
destroy() {
|
|
289
|
+
isDestroyed = true
|
|
290
|
+
items.length = 0
|
|
291
|
+
sids.length = 0
|
|
292
|
+
for (const sid in outputs) delete outputs[sid]
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a scripted object reaches: the player's values as a view model rooted
|
|
3
|
+
* at the object's `scope`, and the fonts and images the bundle carries.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { measuredFont } from '../rive/api/text/text.font.js'
|
|
7
|
+
|
|
8
|
+
/** @import { ViewModelAdapter } from '../rive/context/context.viewmodel.js' */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The adapter with every path under `scope`: a nested artboard bound at
|
|
12
|
+
* `gauge` reads `gauge.value` when its script asks for `value`.
|
|
13
|
+
*
|
|
14
|
+
* @param {ViewModelAdapter} base
|
|
15
|
+
* @param {string} scope the path the object's view model sits at; "" for the host's own
|
|
16
|
+
* @returns {ViewModelAdapter}
|
|
17
|
+
*/
|
|
18
|
+
export function scoped(base, scope) {
|
|
19
|
+
if (!scope) return base
|
|
20
|
+
const at = (/** @type {string} */ path) => (path ? scope + '.' + path : scope)
|
|
21
|
+
/** @type {ViewModelAdapter} */
|
|
22
|
+
const adapter = {
|
|
23
|
+
get: (path) => base.get(at(path)),
|
|
24
|
+
set: (path, value) => base.set(at(path), value),
|
|
25
|
+
subscribe: (path, fn) => base.subscribe(at(path), fn),
|
|
26
|
+
kind: (path) => (base.kind ? base.kind(at(path)) : undefined),
|
|
27
|
+
fire: (path) => (base.fire ? base.fire(at(path)) : base.set(at(path), true)),
|
|
28
|
+
}
|
|
29
|
+
if (base.listSize) {
|
|
30
|
+
const size = base.listSize
|
|
31
|
+
adapter.listSize = (path) => size(at(path))
|
|
32
|
+
}
|
|
33
|
+
if (base.listGet) {
|
|
34
|
+
const get = base.listGet
|
|
35
|
+
adapter.listGet = (path, index) => get(at(path), index)
|
|
36
|
+
}
|
|
37
|
+
if (base.listInsert) {
|
|
38
|
+
const insert = base.listInsert
|
|
39
|
+
adapter.listInsert = (path, item, index) => insert(at(path), item, index)
|
|
40
|
+
}
|
|
41
|
+
if (base.listRemove) {
|
|
42
|
+
const remove = base.listRemove
|
|
43
|
+
adapter.listRemove = (path, index) => remove(at(path), index)
|
|
44
|
+
}
|
|
45
|
+
if (base.listSwap) {
|
|
46
|
+
const swap = base.listSwap
|
|
47
|
+
adapter.listSwap = (path, a, b) => swap(at(path), a, b)
|
|
48
|
+
}
|
|
49
|
+
if (base.listClear) {
|
|
50
|
+
const clear = base.listClear
|
|
51
|
+
adapter.listClear = (path) => clear(at(path))
|
|
52
|
+
}
|
|
53
|
+
if (base.instance) {
|
|
54
|
+
const instance = base.instance
|
|
55
|
+
adapter.instance = (path, name) => instance(at(path), name)
|
|
56
|
+
}
|
|
57
|
+
return adapter
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The assets a script may ask for by name: the bundle's fonts, measured on a
|
|
62
|
+
* canvas in the family the player registered them under, and its images.
|
|
63
|
+
*
|
|
64
|
+
* @param {Record<string, string>} families font name → registered family
|
|
65
|
+
* @param {Record<string, { width: number, height: number }>} images image name → size, for the images the bundle lists
|
|
66
|
+
* @returns {import('../rive/api/assets/assets.js').AssetSource}
|
|
67
|
+
*/
|
|
68
|
+
export function assetSource(families, images) {
|
|
69
|
+
/** @type {CanvasRenderingContext2D | null | undefined} */
|
|
70
|
+
let context
|
|
71
|
+
const measurer = () => {
|
|
72
|
+
if (context === undefined) {
|
|
73
|
+
const canvas = typeof document === 'undefined' ? null : document.createElement('canvas')
|
|
74
|
+
context = canvas ? canvas.getContext('2d') : null
|
|
75
|
+
}
|
|
76
|
+
return context
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
image: (name) => images[name] || { width: 0, height: 0 },
|
|
80
|
+
font: (name) => {
|
|
81
|
+
if (!(name in families)) return undefined
|
|
82
|
+
return measuredFont(name, (character) => {
|
|
83
|
+
const ctx = measurer()
|
|
84
|
+
if (!ctx) return 0.5
|
|
85
|
+
// measured at 100 px so the advance is in em
|
|
86
|
+
ctx.font = '100px "' + families[name] + '"'
|
|
87
|
+
return ctx.measureText(character).width / 100
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The static side of a host type as a Lua global: a table of functions
|
|
3
|
+
* (`Text.new`), which can also be called itself (`ImageSampler('clamp', ...)`)
|
|
4
|
+
* as Rive's constructors can.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LuaTable, NativeFunction } from '../../luau/vm/vm.value.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} name
|
|
11
|
+
* @param {Record<string, (args: any[]) => any>} fns each takes the call's arguments
|
|
12
|
+
* @param {string} [call] the function a call of the table itself runs
|
|
13
|
+
* @returns {LuaTable}
|
|
14
|
+
*/
|
|
15
|
+
export function statics(name, fns, call) {
|
|
16
|
+
const t = new LuaTable()
|
|
17
|
+
for (const key in fns) {
|
|
18
|
+
const fn = fns[key]
|
|
19
|
+
t.set(key, new NativeFunction(name + '.' + key, function (vm, args) {
|
|
20
|
+
return fn(args)
|
|
21
|
+
}))
|
|
22
|
+
}
|
|
23
|
+
if (call) {
|
|
24
|
+
const meta = new LuaTable()
|
|
25
|
+
meta.set('__call', new NativeFunction(name + '.__call', function (vm, args) {
|
|
26
|
+
return fns[call](args.slice(1))
|
|
27
|
+
}))
|
|
28
|
+
t.meta = meta
|
|
29
|
+
}
|
|
30
|
+
return t
|
|
31
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host types for Lua: a Lua table that carries a JS object and a metatable
|
|
3
|
+
* whose `__index` and `__newindex` read and write that object's fields and
|
|
4
|
+
* find its methods. Every Rive type a script can hold (Path, Paint, Mat2D,
|
|
5
|
+
* the view model and its properties, the context) is made this way, so a
|
|
6
|
+
* script indexes them as it would Rive's own userdata.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EMPTY, LuaTable, NativeFunction, fail, typeName } from '../../luau/vm/vm.value.js'
|
|
10
|
+
import { toNumber } from '../../luau/vm/vm.meta.js'
|
|
11
|
+
import { decodeUtf8, encodeUtf8 } from '../../luau/luau.convert.js'
|
|
12
|
+
|
|
13
|
+
/** A Lua table that stands for a host object */
|
|
14
|
+
export class Wrapped extends LuaTable {
|
|
15
|
+
/** @param {any} host */
|
|
16
|
+
constructor(host) {
|
|
17
|
+
super()
|
|
18
|
+
this.host = host
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} TypeSpec
|
|
24
|
+
* @property {Record<string, { get: (h: any, vm: any) => any, set?: (h: any, value: any, vm: any) => void }>} [fields]
|
|
25
|
+
* @property {Record<string, (h: any, args: any[], vm: any) => any>} [methods] `args[0]` is the wrapper itself (the `self` of a method call)
|
|
26
|
+
* @property {(h: any, key: any, vm: any) => any} [index] reads a key no field or method names
|
|
27
|
+
* @property {(h: any, key: any, value: any, vm: any) => void} [newindex] writes a key no field names
|
|
28
|
+
* @property {Record<string, (vm: any, args: any[]) => any>} [meta] metamethods such as `__mul`, `__eq`, `__tostring`
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} HostType
|
|
33
|
+
* @property {string} name
|
|
34
|
+
* @property {LuaTable} meta
|
|
35
|
+
* @property {(host: any) => Wrapped} wrap
|
|
36
|
+
* @property {(v: any) => boolean} is
|
|
37
|
+
* @property {(v: any) => any} unwrap the host object, or undefined when v is another kind of value
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} name
|
|
42
|
+
* @param {TypeSpec} spec
|
|
43
|
+
* @returns {HostType}
|
|
44
|
+
*/
|
|
45
|
+
export function defineType(name, spec) {
|
|
46
|
+
const meta = new LuaTable()
|
|
47
|
+
/** @type {Record<string, NativeFunction>} */
|
|
48
|
+
const methods = Object.create(null)
|
|
49
|
+
const fields = spec.fields || Object.create(null)
|
|
50
|
+
const defs = spec.methods || Object.create(null)
|
|
51
|
+
|
|
52
|
+
for (const key in defs) methods[key] = method(name, key, defs[key])
|
|
53
|
+
|
|
54
|
+
meta.set('__index', new NativeFunction(name + '.__index', function (vm, args) {
|
|
55
|
+
const self = args[0]
|
|
56
|
+
const key = args[1]
|
|
57
|
+
const host = self.host
|
|
58
|
+
if (typeof key === 'string') {
|
|
59
|
+
const field = fields[key]
|
|
60
|
+
if (field !== undefined) return field.get(host, vm)
|
|
61
|
+
const m = methods[key]
|
|
62
|
+
if (m !== undefined) return m
|
|
63
|
+
}
|
|
64
|
+
return spec.index ? spec.index(host, key, vm) : undefined
|
|
65
|
+
}))
|
|
66
|
+
|
|
67
|
+
meta.set('__newindex', new NativeFunction(name + '.__newindex', function (vm, args) {
|
|
68
|
+
const host = args[0].host
|
|
69
|
+
const key = args[1]
|
|
70
|
+
const field = typeof key === 'string' ? fields[key] : undefined
|
|
71
|
+
if (field !== undefined && field.set) field.set(host, args[2], vm)
|
|
72
|
+
else if (field === undefined && spec.newindex) spec.newindex(host, key, args[2], vm)
|
|
73
|
+
else throw fail('cannot set ' + name + " field '" + String(key) + "'")
|
|
74
|
+
return EMPTY
|
|
75
|
+
}))
|
|
76
|
+
|
|
77
|
+
if (spec.meta) for (const event in spec.meta) meta.set(event, new NativeFunction(name + '.' + event, spec.meta[event]))
|
|
78
|
+
if (!(spec.meta && spec.meta.__tostring)) meta.set('__tostring', new NativeFunction(name + '.__tostring', function () { return name }))
|
|
79
|
+
|
|
80
|
+
/** @type {HostType} */
|
|
81
|
+
const type = {
|
|
82
|
+
name,
|
|
83
|
+
meta,
|
|
84
|
+
wrap(host) {
|
|
85
|
+
const t = new Wrapped(host)
|
|
86
|
+
t.meta = meta
|
|
87
|
+
return t
|
|
88
|
+
},
|
|
89
|
+
is(v) {
|
|
90
|
+
return v instanceof Wrapped && v.meta === meta
|
|
91
|
+
},
|
|
92
|
+
unwrap(v) {
|
|
93
|
+
return v instanceof Wrapped && v.meta === meta ? v.host : undefined
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
return type
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} typeNameText
|
|
101
|
+
* @param {string} key
|
|
102
|
+
* @param {(h: any, args: any[], vm: any) => any} fn
|
|
103
|
+
* @returns {NativeFunction}
|
|
104
|
+
*/
|
|
105
|
+
function method(typeNameText, key, fn) {
|
|
106
|
+
return new NativeFunction(typeNameText + '.' + key, function (vm, args) {
|
|
107
|
+
const self = args[0]
|
|
108
|
+
if (!(self instanceof Wrapped)) throw fail('missing self: call ' + typeNameText + '.' + key + ' with a colon')
|
|
109
|
+
return fn(self.host, args, vm)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Arguments
|
|
114
|
+
// ■■■■■■■■■
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {any[]} args
|
|
118
|
+
* @param {number} i 0-based
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
function got(args, i) {
|
|
122
|
+
return i >= args.length ? 'no value' : typeName(args[i])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {number} i
|
|
127
|
+
* @param {string} fname
|
|
128
|
+
* @param {string} expected
|
|
129
|
+
* @param {any[]} args
|
|
130
|
+
*/
|
|
131
|
+
export function badArg(i, fname, expected, args) {
|
|
132
|
+
return fail('invalid argument #' + (i + 1) + " to '" + fname + "' (" + expected + ' expected, got ' + got(args, i) + ')')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {any[]} args
|
|
137
|
+
* @param {number} i
|
|
138
|
+
* @param {string} fname
|
|
139
|
+
* @returns {number}
|
|
140
|
+
*/
|
|
141
|
+
export function num(args, i, fname) {
|
|
142
|
+
const n = typeof args[i] === 'number' ? args[i] : toNumber(args[i])
|
|
143
|
+
if (n === undefined) throw badArg(i, fname, 'number', args)
|
|
144
|
+
return n
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {any[]} args
|
|
149
|
+
* @param {number} i
|
|
150
|
+
* @param {string} fname
|
|
151
|
+
* @param {number} def
|
|
152
|
+
* @returns {number}
|
|
153
|
+
*/
|
|
154
|
+
export function optNum(args, i, fname, def) {
|
|
155
|
+
return args[i] === undefined ? def : num(args, i, fname)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {any[]} args
|
|
160
|
+
* @param {number} i
|
|
161
|
+
* @param {string} fname
|
|
162
|
+
* @returns {string} a string argument as text
|
|
163
|
+
*/
|
|
164
|
+
export function text(args, i, fname) {
|
|
165
|
+
if (typeof args[i] !== 'string') throw badArg(i, fname, 'string', args)
|
|
166
|
+
return decodeUtf8(args[i])
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {any[]} args
|
|
171
|
+
* @param {number} i
|
|
172
|
+
* @param {HostType} type
|
|
173
|
+
* @param {string} fname
|
|
174
|
+
* @returns {any} the host object of a wrapped value of this type
|
|
175
|
+
*/
|
|
176
|
+
export function wrapped(args, i, type, fname) {
|
|
177
|
+
const h = type.unwrap(args[i])
|
|
178
|
+
if (h === undefined) throw badArg(i, fname, type.name, args)
|
|
179
|
+
return h
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {string} s JS text
|
|
184
|
+
* @returns {string} as a Lua string
|
|
185
|
+
*/
|
|
186
|
+
export function lua(s) {
|
|
187
|
+
return encodeUtf8(s)
|
|
188
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Artboard`: an artboard a script holds as an input (or makes with
|
|
3
|
+
* `instance()`) and draws. Motion plays the artboard itself, as the nested
|
|
4
|
+
* artboard it is placed as; the script only places it. `draw` records
|
|
5
|
+
*
|
|
6
|
+
* `{ kind: 'artboard', name, instance, time, transform, clips, opacity, color, viewModel }`
|
|
7
|
+
*
|
|
8
|
+
* where `instance` numbers the artboard's instances (an `instance()` is a new
|
|
9
|
+
* one), `time` is the seconds the script advanced it by, and `viewModel` the
|
|
10
|
+
* adapter an instance was bound to (or null). A visitor passed to `draw` is not
|
|
11
|
+
* called: there are no drawables to visit here.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Vector } from '../../../luau/vm/vm.value.js'
|
|
15
|
+
import { Wrapped, defineType, num, wrapped } from '../api.type.js'
|
|
16
|
+
import { Renderer } from '../draw/draw.renderer.js'
|
|
17
|
+
import { stamp } from '../media/media.draw.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} ArtboardData
|
|
21
|
+
* @property {string} name
|
|
22
|
+
* @property {number} width
|
|
23
|
+
* @property {number} height
|
|
24
|
+
* @property {number} instance
|
|
25
|
+
* @property {number} time
|
|
26
|
+
* @property {boolean} frameOrigin
|
|
27
|
+
* @property {any} viewModel a ViewModelAdapter, or null
|
|
28
|
+
* @property {{ next: number }} counter shared by the instances of one artboard
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {any} r a Recorder
|
|
33
|
+
* @param {ArtboardData} a
|
|
34
|
+
*/
|
|
35
|
+
function record(r, a) {
|
|
36
|
+
const out = /** @type {any} */ (stamp(r, 'artboard'))
|
|
37
|
+
out.name = a.name
|
|
38
|
+
out.instance = a.instance
|
|
39
|
+
out.time = a.time
|
|
40
|
+
out.viewModel = a.viewModel
|
|
41
|
+
r.drawings.push(out)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const none = () => 0
|
|
45
|
+
|
|
46
|
+
export const Artboard = defineType('Artboard', {
|
|
47
|
+
fields: {
|
|
48
|
+
frameOrigin: { get: (a) => a.frameOrigin, set: (a, v) => { a.frameOrigin = v === true } },
|
|
49
|
+
width: { get: (a) => a.width },
|
|
50
|
+
height: { get: (a) => a.height },
|
|
51
|
+
data: { get: (a) => a.data },
|
|
52
|
+
},
|
|
53
|
+
methods: {
|
|
54
|
+
draw: (a, args) => record(wrapped(args, 1, Renderer, 'draw'), a),
|
|
55
|
+
drawModulated: (a, args) => record(wrapped(args, 1, Renderer, 'drawModulated'), a),
|
|
56
|
+
propertyKey: () => undefined,
|
|
57
|
+
advance: (a, args) => {
|
|
58
|
+
a.time += num(args, 1, 'advance')
|
|
59
|
+
return true
|
|
60
|
+
},
|
|
61
|
+
instance: (a, args) => {
|
|
62
|
+
const vm = args[1] instanceof Wrapped && args[1].host && args[1].host.scope ? args[1].host.scope.adapter : null
|
|
63
|
+
a.counter.next++
|
|
64
|
+
return Artboard.wrap({ name: a.name, width: a.width, height: a.height, instance: a.counter.next, time: 0, frameOrigin: a.frameOrigin, viewModel: vm, counter: a.counter, data: undefined })
|
|
65
|
+
},
|
|
66
|
+
animation: () => undefined,
|
|
67
|
+
node: () => undefined,
|
|
68
|
+
bounds: (a) => [ new Vector(0, 0, 0), new Vector(a.width, a.height, 0) ],
|
|
69
|
+
pointerDown: none,
|
|
70
|
+
pointerUp: none,
|
|
71
|
+
pointerMove: none,
|
|
72
|
+
pointerExit: none,
|
|
73
|
+
pointerScroll: none,
|
|
74
|
+
gamepadConnected: none,
|
|
75
|
+
gamepadEvent: none,
|
|
76
|
+
gamepadDisconnected: none,
|
|
77
|
+
addToPath: () => {},
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} name
|
|
83
|
+
* @param {number} width
|
|
84
|
+
* @param {number} height
|
|
85
|
+
* @returns {any} an Artboard for a script
|
|
86
|
+
*/
|
|
87
|
+
export function artboardHandle(name, width, height) {
|
|
88
|
+
return Artboard.wrap({ name, width, height, instance: 0, time: 0, frameOrigin: false, viewModel: null, counter: { next: 0 }, data: undefined })
|
|
89
|
+
}
|