@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.
Files changed (77) hide show
  1. package/bands/bands.js +3 -1
  2. package/bundle/bundle.js +56 -0
  3. package/list/list.js +12 -1
  4. package/lottie/lottie.layers.js +64 -11
  5. package/lottie/lottie.paint.js +30 -0
  6. package/lottie/lottie.paths.js +19 -4
  7. package/lottie/lottie.props.js +18 -8
  8. package/lottie/lottie.shapes.js +2 -1
  9. package/mixer/mixer.js +31 -8
  10. package/mixer/mixer.mix.js +17 -0
  11. package/motion/motion.convert.js +32 -1
  12. package/motion/motion.flow.js +20 -2
  13. package/motion/motion.machine.js +28 -13
  14. package/motion/motion.values.js +13 -9
  15. package/motion.js +196 -20
  16. package/package.json +10 -16
  17. package/player/player.js +25 -5
  18. package/readme.md +78 -3
  19. package/script/luau/lib/base/base.js +202 -0
  20. package/script/luau/lib/base/base.table.js +189 -0
  21. package/script/luau/lib/lib.args.js +152 -0
  22. package/script/luau/lib/lib.js +23 -0
  23. package/script/luau/lib/math/math.bit32.js +144 -0
  24. package/script/luau/lib/math/math.js +288 -0
  25. package/script/luau/lib/math/math.vector.js +125 -0
  26. package/script/luau/lib/string/string.format.js +143 -0
  27. package/script/luau/lib/string/string.js +160 -0
  28. package/script/luau/lib/string/string.pattern.js +505 -0
  29. package/script/luau/load/load.js +362 -0
  30. package/script/luau/load/load.ops.js +79 -0
  31. package/script/luau/luau.convert.js +259 -0
  32. package/script/luau/luau.js +132 -0
  33. package/script/luau/luau.md +111 -0
  34. package/script/luau/vm/vm.js +167 -0
  35. package/script/luau/vm/vm.meta.js +401 -0
  36. package/script/luau/vm/vm.num.js +236 -0
  37. package/script/luau/vm/vm.run.js +842 -0
  38. package/script/luau/vm/vm.value.js +406 -0
  39. package/script/play/play.js +199 -0
  40. package/script/play/play.objects.js +295 -0
  41. package/script/play/play.scope.js +91 -0
  42. package/script/rive/api/api.statics.js +31 -0
  43. package/script/rive/api/api.type.js +188 -0
  44. package/script/rive/api/artboard/artboard.js +89 -0
  45. package/script/rive/api/assets/assets.js +72 -0
  46. package/script/rive/api/data/data.value.js +99 -0
  47. package/script/rive/api/draw/draw.contours.js +163 -0
  48. package/script/rive/api/draw/draw.measure.js +196 -0
  49. package/script/rive/api/draw/draw.paint.js +235 -0
  50. package/script/rive/api/draw/draw.path.js +153 -0
  51. package/script/rive/api/draw/draw.renderer.js +117 -0
  52. package/script/rive/api/math/math.color.js +96 -0
  53. package/script/rive/api/math/math.mat2d.js +180 -0
  54. package/script/rive/api/math/math.vector.js +121 -0
  55. package/script/rive/api/media/media.audio.js +113 -0
  56. package/script/rive/api/media/media.draw.js +73 -0
  57. package/script/rive/api/media/media.image.js +61 -0
  58. package/script/rive/api/media/media.mesh.js +50 -0
  59. package/script/rive/api/pointer/pointer.event.js +64 -0
  60. package/script/rive/api/pointer/pointer.route.js +69 -0
  61. package/script/rive/api/text/text.font.js +129 -0
  62. package/script/rive/api/text/text.js +278 -0
  63. package/script/rive/api/text/text.layout.js +259 -0
  64. package/script/rive/context/context.js +76 -0
  65. package/script/rive/context/context.memory.js +196 -0
  66. package/script/rive/context/context.viewmodel.js +302 -0
  67. package/script/rive/context/lists/lists.js +160 -0
  68. package/script/rive/context/lists/lists.values.js +281 -0
  69. package/script/rive/host/host.globals.js +44 -0
  70. package/script/rive/host/host.js +533 -0
  71. package/script/rive/host/host.node.js +45 -0
  72. package/script/rive/rive.js +23 -0
  73. package/script/rive/rive.md +180 -0
  74. package/script/script.js +11 -0
  75. package/skin/skin.deform.js +514 -0
  76. package/skin/skin.ik.js +260 -0
  77. 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
+ }