@px-petals/roku.motion-web 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/audio/audio.cues.js +27 -0
- package/audio/audio.js +259 -0
- package/bands/bands.js +390 -0
- package/bundle/bundle.fonts.js +55 -0
- package/bundle/bundle.js +75 -0
- package/focus/focus.direction.js +46 -0
- package/focus/focus.dom.js +158 -0
- package/focus/focus.js +277 -0
- package/focus/focus.tree.js +146 -0
- package/hit/hit.geometry.js +113 -0
- package/hit/hit.js +129 -0
- package/list/list.flex.js +181 -0
- package/list/list.items.js +80 -0
- package/list/list.js +507 -0
- package/lottie/lottie.js +124 -0
- package/lottie/lottie.layers.js +363 -0
- package/lottie/lottie.paint.js +208 -0
- package/lottie/lottie.paths.js +459 -0
- package/lottie/lottie.props.js +255 -0
- package/lottie/lottie.shapes.js +258 -0
- package/mixer/mixer.js +284 -0
- package/mixer/mixer.mix.js +90 -0
- package/mixer/mixer.stage.js +431 -0
- package/motion/motion.convert.js +373 -0
- package/motion/motion.flow.js +403 -0
- package/motion/motion.keys.js +150 -0
- package/motion/motion.machine.js +367 -0
- package/motion/motion.nested.js +345 -0
- package/motion/motion.values.js +364 -0
- package/motion.js +979 -0
- package/package.json +53 -4
- package/player/overlay/overlay.js +325 -0
- package/player/overlay/overlay.layout.js +208 -0
- package/player/player.fit.js +101 -0
- package/player/player.js +401 -0
- package/player/player.raster.js +127 -0
- package/pointer/pointer.js +292 -0
- package/readme.md +270 -0
- package/README.md +0 -3
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live data: view-model values to slot values in Lottie units (as Roku's
|
|
3
|
+
* Motion.Task patches them, bundle.bs), to tint colours for raster sheets
|
|
4
|
+
* (motion.tint.bs), and from the string pairs a host such as maestro-web-sdk
|
|
5
|
+
* holds, typed by the bundle's own property declarations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { applyConverter, reverseConverter } from './motion.convert.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A bundle's `data` block: the view model's properties (nested ones by dotted
|
|
12
|
+
* path, enums with their `values`, images with their `images`), its slots,
|
|
13
|
+
* the write-backs (`writes`) and instances.
|
|
14
|
+
* @typedef {{ properties?: any[], slots?: any[], writes?: any[], instances?: Record<string, any>, defaultInstance?: string }} Data
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A colour as a caller gives it: an ARGB number (as Rive and JSON hold it),
|
|
19
|
+
* `#RRGGBB`, `#AARRGGBB`, `0x`-prefixed, or `[r, g, b, a]` in 0-1.
|
|
20
|
+
*
|
|
21
|
+
* @param {any} value
|
|
22
|
+
* @returns {number[] | null} `[r, g, b, a]` in 0-1
|
|
23
|
+
*/
|
|
24
|
+
export function rgba(value) {
|
|
25
|
+
if (Array.isArray(value)) return value
|
|
26
|
+
if (typeof value === 'number' && isFinite(value)) {
|
|
27
|
+
// above 2^31 a colour isn't safe for bit operations, so it's split by division
|
|
28
|
+
let whole = Math.floor(value)
|
|
29
|
+
const channels = []
|
|
30
|
+
for (let n = 0; n < 4; n++) {
|
|
31
|
+
channels.unshift((whole % 256) / 255)
|
|
32
|
+
whole = Math.floor(whole / 256)
|
|
33
|
+
}
|
|
34
|
+
return [ channels[1], channels[2], channels[3], channels[0] ]
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === 'string') {
|
|
37
|
+
const hex = value.replace(/^#|^0x/i, '')
|
|
38
|
+
const isHex = /^[0-9a-f]{6}([0-9a-f]{2})?$/i.test(hex)
|
|
39
|
+
if (!isHex) return null
|
|
40
|
+
const byte = (/** @type {number} */ at) => parseInt(hex.slice(at, at + 2), 16) / 255
|
|
41
|
+
return hex.length === 6 ? [ byte(0), byte(2), byte(4), 1 ] : [ byte(2), byte(4), byte(6), byte(0) ]
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A view-model value in a slot's Lottie units: colours as `[r, g, b, 1]`,
|
|
48
|
+
* alphas and percents ×100, radians to degrees.
|
|
49
|
+
*
|
|
50
|
+
* @param {any} value
|
|
51
|
+
* @param {string} conversion color, alpha, percent, degrees, number, text or image
|
|
52
|
+
* @param {any} [property] the view-model property the value is of: an enum names its values, an image its assets
|
|
53
|
+
*/
|
|
54
|
+
export function convert(value, conversion, property) {
|
|
55
|
+
if (conversion === 'color' || conversion === 'alpha') {
|
|
56
|
+
const colour = rgba(value)
|
|
57
|
+
if (!colour) return undefined
|
|
58
|
+
return conversion === 'alpha' ? colour[3] * 100 : [ colour[0], colour[1], colour[2], 1 ]
|
|
59
|
+
}
|
|
60
|
+
if (conversion === 'text') return property && property.values && typeof value === 'number' && property.values[value] !== undefined ? property.values[value] : value
|
|
61
|
+
if (conversion === 'image') return assetId(value, property)
|
|
62
|
+
if (typeof value !== 'number') return undefined
|
|
63
|
+
if (conversion === 'percent') return value * 100
|
|
64
|
+
if (conversion === 'degrees') return (value * 180) / Math.PI
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The Lottie asset an image value picks: by the asset's name, by its Lottie
|
|
70
|
+
* id, or by its index in the file.
|
|
71
|
+
*
|
|
72
|
+
* @param {any} value
|
|
73
|
+
* @param {any} [property]
|
|
74
|
+
*/
|
|
75
|
+
function assetId(value, property) {
|
|
76
|
+
const images = (property && property.images) || {}
|
|
77
|
+
if (typeof value === 'string') {
|
|
78
|
+
if (images[value]) return images[value]
|
|
79
|
+
return /^image_\d+$/.test(value) ? value : undefined
|
|
80
|
+
}
|
|
81
|
+
return typeof value === 'number' ? 'image_' + value : undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The slot table for a set of values: slot id → Lottie value, for every slot
|
|
86
|
+
* some input fills. A 2-D slot keeps its base value on an axis no source fills.
|
|
87
|
+
*
|
|
88
|
+
* @param {Data} data
|
|
89
|
+
* @param {Record<string, any>} values
|
|
90
|
+
* @param {(message: string) => void} [warn] a converter this player does not know
|
|
91
|
+
* @returns {Record<string, any>}
|
|
92
|
+
*/
|
|
93
|
+
export function slotValues(data, values, warn) {
|
|
94
|
+
/** @type {Record<string, any>} */
|
|
95
|
+
const table = {}
|
|
96
|
+
const slots = (data && data.slots) || []
|
|
97
|
+
for (let n = 0; n < slots.length; n++) {
|
|
98
|
+
const slot = slots[n]
|
|
99
|
+
const value = slotValue(slot, values, data, warn)
|
|
100
|
+
if (value !== undefined) table[slot.sid] = value
|
|
101
|
+
}
|
|
102
|
+
return table
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A view-model property's declaration, by its dotted path.
|
|
107
|
+
*
|
|
108
|
+
* @param {Data} data
|
|
109
|
+
* @param {string} path
|
|
110
|
+
* @returns {any}
|
|
111
|
+
*/
|
|
112
|
+
export function propertyOf(data, path) {
|
|
113
|
+
const properties = (data && data.properties) || []
|
|
114
|
+
for (let n = 0; n < properties.length; n++) if (properties[n].name === path) return properties[n]
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* What a source reads: its view-model value, through its converter.
|
|
120
|
+
*
|
|
121
|
+
* @param {any} source
|
|
122
|
+
* @param {Record<string, any>} values
|
|
123
|
+
* @param {Data} data
|
|
124
|
+
* @param {(message: string) => void} [warn]
|
|
125
|
+
*/
|
|
126
|
+
function sourceValue(source, values, data, warn) {
|
|
127
|
+
const raw = values[source.path]
|
|
128
|
+
if (raw === undefined || !source.converter) return raw
|
|
129
|
+
const property = propertyOf(data, source.path)
|
|
130
|
+
return applyConverter(source.converter, raw, { read: (path) => values[path], names: property && property.values, warn })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {any} slot
|
|
135
|
+
* @param {Record<string, any>} values
|
|
136
|
+
* @param {Data} data
|
|
137
|
+
* @param {(message: string) => void} [warn]
|
|
138
|
+
*/
|
|
139
|
+
function slotValue(slot, values, data, warn) {
|
|
140
|
+
const sources = slot.sources || []
|
|
141
|
+
if (!sources.length) return undefined
|
|
142
|
+
|
|
143
|
+
const isPair = slot.type === 'Position' || slot.type === 'Vector'
|
|
144
|
+
if (!isPair) {
|
|
145
|
+
const raw = sourceValue(sources[0], values, data, warn)
|
|
146
|
+
return raw === undefined ? undefined : convert(raw, slot.conversion, propertyOf(data, sources[0].path))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const pair = Array.isArray(slot.base) ? slot.base.slice() : [ 0, 0 ]
|
|
150
|
+
let found = false
|
|
151
|
+
for (let n = 0; n < sources.length; n++) {
|
|
152
|
+
const raw = sourceValue(sources[n], values, data, warn)
|
|
153
|
+
if (raw === undefined) continue
|
|
154
|
+
const converted = convert(raw, slot.conversion)
|
|
155
|
+
if (converted === undefined) continue
|
|
156
|
+
pair[sources[n].axis || 0] = converted
|
|
157
|
+
found = true
|
|
158
|
+
}
|
|
159
|
+
return found ? pair : undefined
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Inputs with nested objects flattened to dotted paths: `{ player: { score: 3 } }`
|
|
164
|
+
* is `{ 'player.score': 3 }`. Arrays (a list's items) are values, not paths.
|
|
165
|
+
*
|
|
166
|
+
* @param {Record<string, any>} inputs
|
|
167
|
+
* @returns {Record<string, any>}
|
|
168
|
+
*/
|
|
169
|
+
export function flattenInputs(inputs) {
|
|
170
|
+
/** @type {Record<string, any>} */
|
|
171
|
+
const result = {}
|
|
172
|
+
const visit = (/** @type {Record<string, any>} */ object, /** @type {string} */ prefix) => {
|
|
173
|
+
for (const key in object) {
|
|
174
|
+
const value = object[key]
|
|
175
|
+
const isGroup = typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
176
|
+
if (isGroup) visit(value, prefix + key + '.')
|
|
177
|
+
else result[prefix + key] = value
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
visit(inputs || {}, '')
|
|
181
|
+
return result
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* An input as the flow holds it: an enum given by name becomes its index, so
|
|
186
|
+
* the machine's numeric guards compare it.
|
|
187
|
+
*
|
|
188
|
+
* @param {Data} data
|
|
189
|
+
* @param {string} name the input's dotted path
|
|
190
|
+
* @param {any} value
|
|
191
|
+
* @returns {any}
|
|
192
|
+
*/
|
|
193
|
+
export function inputValue(data, name, value) {
|
|
194
|
+
const property = propertyOf(data, name)
|
|
195
|
+
if (!property || !property.values || typeof value !== 'string') return value
|
|
196
|
+
const index = property.values.indexOf(value)
|
|
197
|
+
if (index >= 0) return index
|
|
198
|
+
return isFinite(Number(value)) && value !== '' ? Number(value) : value
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The values a host sees: an enum as the name of its value.
|
|
203
|
+
*
|
|
204
|
+
* @param {Data} data
|
|
205
|
+
* @param {Record<string, any>} values
|
|
206
|
+
* @returns {Record<string, any>}
|
|
207
|
+
*/
|
|
208
|
+
export function publicValues(data, values) {
|
|
209
|
+
/** @type {Record<string, any>} */
|
|
210
|
+
const result = {}
|
|
211
|
+
for (const name in values) {
|
|
212
|
+
const property = propertyOf(data, name)
|
|
213
|
+
const named = property && property.values && typeof values[name] === 'number' ? property.values[values[name]] : undefined
|
|
214
|
+
result[name] = named === undefined ? values[name] : named
|
|
215
|
+
}
|
|
216
|
+
return result
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A property the animation draws, back in the view model's units.
|
|
221
|
+
*
|
|
222
|
+
* @param {any} write a `data.writes` entry
|
|
223
|
+
* @param {any} drawn the Lottie property's value
|
|
224
|
+
* @param {any} current the view-model value now, whose alpha a colour keeps
|
|
225
|
+
*/
|
|
226
|
+
export function writtenValue(write, drawn, current) {
|
|
227
|
+
const isPair = write.type === 'Position' || write.type === 'Vector'
|
|
228
|
+
let value = isPair && Array.isArray(drawn) ? drawn[write.axis || 0] : drawn
|
|
229
|
+
switch (write.conversion) {
|
|
230
|
+
case 'color': {
|
|
231
|
+
if (!Array.isArray(value)) return undefined
|
|
232
|
+
const colour = rgba(current)
|
|
233
|
+
const alpha = colour ? Math.round(colour[3] * 255) : 255
|
|
234
|
+
const channel = (/** @type {number} */ at) => Math.round(value[at] * 255)
|
|
235
|
+
return alpha * 16777216 + channel(0) * 65536 + channel(1) * 256 + channel(2)
|
|
236
|
+
}
|
|
237
|
+
case 'percent':
|
|
238
|
+
value = value / 100
|
|
239
|
+
break
|
|
240
|
+
case 'degrees':
|
|
241
|
+
value = (value * Math.PI) / 180
|
|
242
|
+
break
|
|
243
|
+
case 'alpha':
|
|
244
|
+
return undefined
|
|
245
|
+
default:
|
|
246
|
+
break
|
|
247
|
+
}
|
|
248
|
+
if (typeof value !== 'number' || !isFinite(value)) return undefined
|
|
249
|
+
return write.converter ? reverseConverter(write.converter, value) : value
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Each tint's live colour and opacity, for raster sheets: the slot's input
|
|
254
|
+
* when it has one, else the tint's own.
|
|
255
|
+
*
|
|
256
|
+
* @param {any[]} tints a raster entry's `tints`
|
|
257
|
+
* @param {Data} data
|
|
258
|
+
* @param {Record<string, any>} values
|
|
259
|
+
* @returns {{ sid: string, rgb: number[], alpha: number }[]}
|
|
260
|
+
*/
|
|
261
|
+
export function tintColors(tints, data, values) {
|
|
262
|
+
const result = []
|
|
263
|
+
for (let n = 0; n < (tints || []).length; n++) {
|
|
264
|
+
const tint = tints[n]
|
|
265
|
+
const live = rgba(slotInput(data, tint.sid, values))
|
|
266
|
+
const liveAlpha = tint.alphaSid ? alphaOf(data, tint.alphaSid, values) : undefined
|
|
267
|
+
result.push({ sid: tint.sid, rgb: live ? live.slice(0, 3) : tint.color, alpha: typeof liveAlpha === 'number' ? liveAlpha : tint.alpha })
|
|
268
|
+
}
|
|
269
|
+
return result
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {Data} data
|
|
274
|
+
* @param {string} sid
|
|
275
|
+
* @param {Record<string, any>} values
|
|
276
|
+
*/
|
|
277
|
+
function slotInput(data, sid, values) {
|
|
278
|
+
const slot = findSlot(data, sid)
|
|
279
|
+
return slot ? values[slot.sources[0].path] : undefined
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* A Rive colour carries its alpha; a Rive opacity is 0-1; a Lottie one 0-100.
|
|
284
|
+
*
|
|
285
|
+
* @param {Data} data
|
|
286
|
+
* @param {string} sid
|
|
287
|
+
* @param {Record<string, any>} values
|
|
288
|
+
*/
|
|
289
|
+
function alphaOf(data, sid, values) {
|
|
290
|
+
const slot = findSlot(data, sid)
|
|
291
|
+
if (!slot) return undefined
|
|
292
|
+
const raw = values[slot.sources[0].path]
|
|
293
|
+
if (raw === undefined) return undefined
|
|
294
|
+
if (slot.conversion === 'alpha') {
|
|
295
|
+
const colour = rgba(raw)
|
|
296
|
+
return colour && colour.length > 3 ? colour[3] : undefined
|
|
297
|
+
}
|
|
298
|
+
if (typeof raw !== 'number') return undefined
|
|
299
|
+
return slot.conversion === 'percent' ? raw : raw / 100
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {Data} data
|
|
304
|
+
* @param {string} sid
|
|
305
|
+
*/
|
|
306
|
+
export function findSlot(data, sid) {
|
|
307
|
+
const slots = (data && data.slots) || []
|
|
308
|
+
for (let n = 0; n < slots.length; n++) {
|
|
309
|
+
const slot = slots[n]
|
|
310
|
+
if (slot.sid === sid && slot.sources && slot.sources.length) return slot
|
|
311
|
+
}
|
|
312
|
+
return null
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Values from `{ key, value }` string pairs, typed by the view model's own
|
|
317
|
+
* property declarations, so a colour named anything reads as a colour. A key
|
|
318
|
+
* the bundle doesn't declare is kept as given.
|
|
319
|
+
*
|
|
320
|
+
* @param {Data} data
|
|
321
|
+
* @param {{ key: string, value: string }[]} pairs
|
|
322
|
+
* @returns {Record<string, any>}
|
|
323
|
+
*/
|
|
324
|
+
export function fromPairs(data, pairs) {
|
|
325
|
+
/** @type {Record<string, string>} */
|
|
326
|
+
const types = {}
|
|
327
|
+
const properties = (data && data.properties) || []
|
|
328
|
+
for (let n = 0; n < properties.length; n++) types[properties[n].name] = properties[n].type
|
|
329
|
+
|
|
330
|
+
/** @type {Record<string, any>} */
|
|
331
|
+
const values = {}
|
|
332
|
+
for (let n = 0; n < (pairs || []).length; n++) {
|
|
333
|
+
const { key, value } = pairs[n]
|
|
334
|
+
if (!key) continue
|
|
335
|
+
values[key] = typed(value, types[key])
|
|
336
|
+
}
|
|
337
|
+
return values
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A list as the JSON text of its items; text that is not a list is an empty one.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} value
|
|
344
|
+
*/
|
|
345
|
+
function jsonList(value) {
|
|
346
|
+
try {
|
|
347
|
+
const items = JSON.parse(value)
|
|
348
|
+
return Array.isArray(items) ? items : []
|
|
349
|
+
} catch {
|
|
350
|
+
return []
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @param {string} value
|
|
356
|
+
* @param {string} [type]
|
|
357
|
+
*/
|
|
358
|
+
function typed(value, type) {
|
|
359
|
+
if (type === 'number') return Number(value)
|
|
360
|
+
if (type === 'boolean') return value === 'true'
|
|
361
|
+
if (type === 'list') return jsonList(value)
|
|
362
|
+
if (type === 'color') return isFinite(Number(value)) && value !== '' ? Number(value) : value
|
|
363
|
+
return value
|
|
364
|
+
}
|