@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.
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Data converters: the declarative descriptors a bundle's `data.slots` carry
3
+ * on a source (rive2lottie writes them, dotlottie.converters.js lists them),
4
+ * applied to a view-model value on its way to a slot, and reversed for a bind
5
+ * that writes back. A descriptor type this code does not know warns once and
6
+ * passes the value through.
7
+ *
8
+ * The same arithmetic runs at build time for the default instance
9
+ * (dotlottie.convert.js in the converter) and on Roku (bundle.convert.bs).
10
+ */
11
+
12
+ /**
13
+ * @typedef {object} ConvertContext
14
+ * @property {(path: string) => any} [read] a view-model property's value, by dotted path
15
+ * @property {string[]} [names] the names of an enum the value indexes
16
+ * @property {(message: string) => void} [warn]
17
+ */
18
+
19
+ /**
20
+ * Operations of an `operation` descriptor and a formula, by name.
21
+ * @type {Record<string, (a: number, b: number) => number>}
22
+ */
23
+ const OPERATIONS = {
24
+ add: (a, b) => a + b,
25
+ sub: (a, b) => a - b,
26
+ mul: (a, b) => a * b,
27
+ div: (a, b) => (b === 0 ? 0 : a / b),
28
+ mod: (a, b) => (b === 0 ? 0 : a % b),
29
+ sqrt: (a) => Math.sqrt(a),
30
+ pow: (a, b) => Math.pow(a, b),
31
+ exp: (a) => Math.exp(a),
32
+ log: (a) => Math.log(a),
33
+ cos: (a) => Math.cos(a),
34
+ sin: (a) => Math.sin(a),
35
+ tan: (a) => Math.tan(a),
36
+ acos: (a) => Math.acos(a),
37
+ asin: (a) => Math.asin(a),
38
+ atan: (a) => Math.atan(a),
39
+ atan2: (a, b) => Math.atan2(a, b),
40
+ round: (a) => Math.round(a),
41
+ floor: (a) => Math.floor(a),
42
+ ceil: (a) => Math.ceil(a),
43
+ }
44
+
45
+ /**
46
+ * Formula functions: the arguments are a list.
47
+ * @type {Record<string, (args: number[]) => number>}
48
+ */
49
+ const FUNCTIONS = {
50
+ min: (args) => Math.min.apply(Math, args),
51
+ max: (args) => Math.max.apply(Math, args),
52
+ round: (args) => Math.round(args[0]),
53
+ ceil: (args) => Math.ceil(args[0]),
54
+ floor: (args) => Math.floor(args[0]),
55
+ sqrt: (args) => Math.sqrt(args[0]),
56
+ pow: (args) => Math.pow(args[0], args[1]),
57
+ exp: (args) => Math.exp(args[0]),
58
+ log: (args) => Math.log(args[0]),
59
+ cos: (args) => Math.cos(args[0]),
60
+ sin: (args) => Math.sin(args[0]),
61
+ tan: (args) => Math.tan(args[0]),
62
+ acos: (args) => Math.acos(args[0]),
63
+ asin: (args) => Math.asin(args[0]),
64
+ atan: (args) => Math.atan(args[0]),
65
+ atan2: (args) => Math.atan2(args[0], args[1]),
66
+ random: () => Math.random(),
67
+ }
68
+
69
+ /**
70
+ * Binding strength of a formula's operation tokens: the rest bind tightest.
71
+ * @type {Record<string, number>}
72
+ */
73
+ const PRECEDENCE = { add: 1, sub: 1, mul: 2, div: 2, mod: 2 }
74
+
75
+ /**
76
+ * Unknown descriptor types already warned about
77
+ * @type {Record<string, boolean>}
78
+ */
79
+ const warned = {}
80
+
81
+ /**
82
+ * Whether a value is a number the converters can work on.
83
+ * @param {any} value
84
+ */
85
+ function numeric(value) {
86
+ const n = typeof value === 'number' ? value : parseFloat(value)
87
+ return isFinite(n) ? n : 0
88
+ }
89
+
90
+ /**
91
+ * Applies a descriptor to a view-model value.
92
+ *
93
+ * @param {any} descriptor
94
+ * @param {any} value
95
+ * @param {ConvertContext} [context]
96
+ * @returns {any}
97
+ */
98
+ export function applyConverter(descriptor, value, context) {
99
+ const ctx = context || {}
100
+ switch (descriptor && descriptor.type) {
101
+ case 'negate':
102
+ return !value
103
+ case 'range':
104
+ return range(descriptor, numeric(value))
105
+ case 'round':
106
+ return roundTo(numeric(value), descriptor.decimals)
107
+ case 'operation':
108
+ return operate(descriptor.op, numeric(value), operand(descriptor, ctx))
109
+ case 'formula':
110
+ return formula(descriptor.tokens || [], numeric(value), ctx)
111
+ case 'toString':
112
+ return toText(descriptor, value, ctx)
113
+ case 'toNumber':
114
+ return numeric(value)
115
+ case 'pad':
116
+ return pad(descriptor, String(value === undefined || value === null ? '' : value))
117
+ case 'trim': {
118
+ let text = String(value === undefined || value === null ? '' : value)
119
+ if (descriptor.trimStart) text = text.replace(/^\s+/, '')
120
+ if (descriptor.trimEnd) text = text.replace(/\s+$/, '')
121
+ return text
122
+ }
123
+ case 'removeZeros':
124
+ return trimZeros(String(value === undefined || value === null ? '' : value))
125
+ case 'enumToUint':
126
+ return enumIndex(value, ctx.names)
127
+ case 'listLength':
128
+ return Array.isArray(value) ? value.length : 0
129
+ case 'group': {
130
+ let result = value
131
+ const items = descriptor.items || []
132
+ // only the first link sees the enum names
133
+ for (let n = 0; n < items.length; n++) result = applyConverter(items[n], result, n === 0 ? ctx : { read: ctx.read, warn: ctx.warn })
134
+ return result
135
+ }
136
+ default:
137
+ warnUnknown(descriptor, ctx)
138
+ return value
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Undoes a descriptor for a bind that writes a property back into the view
144
+ * model. What cannot be undone (a formula, a rounding) passes the value on.
145
+ *
146
+ * @param {any} descriptor
147
+ * @param {any} value the target property's value
148
+ * @param {ConvertContext} [context]
149
+ * @returns {any}
150
+ */
151
+ export function reverseConverter(descriptor, value, context) {
152
+ const ctx = context || {}
153
+ switch (descriptor && descriptor.type) {
154
+ case 'negate':
155
+ return !value
156
+ case 'range':
157
+ return unrange(descriptor, numeric(value))
158
+ case 'operation': {
159
+ /** @type {Record<string, string>} */
160
+ const inverses = { add: 'sub', sub: 'add', mul: 'div', div: 'mul' }
161
+ const inverse = inverses[descriptor.op]
162
+ return inverse ? operate(inverse, numeric(value), operand(descriptor, ctx)) : value
163
+ }
164
+ case 'toString':
165
+ return numeric(value)
166
+ case 'toNumber':
167
+ return String(value)
168
+ case 'group': {
169
+ let result = value
170
+ const items = descriptor.items || []
171
+ for (let n = items.length - 1; n >= 0; n--) result = reverseConverter(items[n], result, ctx)
172
+ return result
173
+ }
174
+ default:
175
+ return value
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Applies descriptors in order.
181
+ *
182
+ * @param {any} descriptor a descriptor, or null for none
183
+ * @param {any} value
184
+ * @param {ConvertContext} [context]
185
+ */
186
+ export function convertValue(descriptor, value, context) {
187
+ return descriptor ? applyConverter(descriptor, value, context) : value
188
+ }
189
+
190
+ /**
191
+ * @param {any} descriptor
192
+ * @param {ConvertContext} ctx
193
+ */
194
+ function warnUnknown(descriptor, ctx) {
195
+ const type = descriptor && descriptor.type
196
+ if (warned[type] || !ctx.warn) return
197
+ warned[type] = true
198
+ ctx.warn("data converter '" + type + "' is not known to this player; its value passes through")
199
+ }
200
+
201
+ /**
202
+ * The right-hand side of an operation: a literal, or another property's value.
203
+ *
204
+ * @param {any} descriptor
205
+ * @param {ConvertContext} ctx
206
+ */
207
+ function operand(descriptor, ctx) {
208
+ if (typeof descriptor.path === 'string') return numeric(ctx.read ? ctx.read(descriptor.path) : 0)
209
+ return numeric(descriptor.value)
210
+ }
211
+
212
+ /**
213
+ * @param {string} op
214
+ * @param {number} a
215
+ * @param {number} b
216
+ */
217
+ function operate(op, a, b) {
218
+ const fn = OPERATIONS[op]
219
+ return fn ? fn(a, b) : a
220
+ }
221
+
222
+ /**
223
+ * @param {number} value
224
+ * @param {number} decimals
225
+ */
226
+ function roundTo(value, decimals) {
227
+ const scale = Math.pow(10, decimals || 0)
228
+ return Math.round(value * scale) / scale
229
+ }
230
+
231
+ /**
232
+ * A range mapper: the input's position in its range, placed in the output's.
233
+ *
234
+ * @param {any} d
235
+ * @param {number} input
236
+ */
237
+ function range(d, input) {
238
+ const low = Math.min(d.minInput, d.maxInput)
239
+ const high = Math.max(d.minInput, d.maxInput)
240
+ let x = input
241
+ if (d.modulo && high > low) x = low + ((((x - low) % (high - low)) + (high - low)) % (high - low))
242
+ if (d.clampLower && x < low) x = low
243
+ if (d.clampUpper && x > high) x = high
244
+
245
+ const span = d.maxInput - d.minInput
246
+ let t = span === 0 ? 0 : (x - d.minInput) / span
247
+ if (d.hold) t = t >= 1 ? 1 : 0
248
+ if (d.reverse) t = 1 - t
249
+ return d.minOutput + t * (d.maxOutput - d.minOutput)
250
+ }
251
+
252
+ /**
253
+ * @param {any} d
254
+ * @param {number} output
255
+ */
256
+ function unrange(d, output) {
257
+ const span = d.maxOutput - d.minOutput
258
+ let t = span === 0 ? 0 : (output - d.minOutput) / span
259
+ if (d.reverse) t = 1 - t
260
+ let x = d.minInput + t * (d.maxInput - d.minInput)
261
+ const low = Math.min(d.minInput, d.maxInput)
262
+ const high = Math.max(d.minInput, d.maxInput)
263
+ if (d.clampLower && x < low) x = low
264
+ if (d.clampUpper && x > high) x = high
265
+ return x
266
+ }
267
+
268
+ /**
269
+ * A formula's tokens read as an infix expression.
270
+ *
271
+ * @param {any[]} tokens
272
+ * @param {number} input
273
+ * @param {ConvertContext} ctx
274
+ */
275
+ function formula(tokens, input, ctx) {
276
+ let at = 0
277
+
278
+ /** @param {number} minimum */
279
+ function expression(minimum) {
280
+ /** @type {number} */
281
+ let left = operandOf()
282
+ while (at < tokens.length && tokens[at].token === 'op') {
283
+ const op = tokens[at].op
284
+ const strength = PRECEDENCE[op] || 3
285
+ if (strength < minimum) break
286
+ at++
287
+ left = operate(op, left, expression(strength + 1))
288
+ }
289
+ return left
290
+ }
291
+
292
+ /** @returns {number} */
293
+ function operandOf() {
294
+ const token = tokens[at++]
295
+ if (!token) return 0
296
+ if (token.token === 'input') return input
297
+ if (token.token === 'value') return typeof token.path === 'string' ? numeric(ctx.read ? ctx.read(token.path) : 0) : numeric(token.value)
298
+ if (token.token === 'open') {
299
+ /** @type {number} */
300
+ const inner = expression(0)
301
+ if (at < tokens.length && tokens[at].token === 'close') at++
302
+ return inner
303
+ }
304
+ if (token.token === 'op' && token.op === 'sub') return -operandOf()
305
+ if (token.token === 'fn') {
306
+ const args = []
307
+ while (at < tokens.length && tokens[at].token !== 'close') {
308
+ args.push(expression(0))
309
+ if (at < tokens.length && tokens[at].token === 'sep') at++
310
+ }
311
+ at++
312
+ const fn = FUNCTIONS[token.fn]
313
+ return fn ? fn(args) : 0
314
+ }
315
+ return 0
316
+ }
317
+
318
+ const result = expression(0)
319
+ return isFinite(result) ? result : 0
320
+ }
321
+
322
+ /**
323
+ * ToString: a number to text at the converter's precision, a colour to hex,
324
+ * an enum index to its name.
325
+ *
326
+ * @param {any} d
327
+ * @param {any} value
328
+ * @param {ConvertContext} ctx
329
+ */
330
+ function toText(d, value, ctx) {
331
+ if (typeof value === 'boolean') return value ? 'true' : 'false'
332
+ if (typeof value === 'string') return value
333
+ if (ctx.names && typeof value === 'number' && ctx.names[value] !== undefined) return ctx.names[value]
334
+ if (typeof value !== 'number') return value === undefined || value === null ? '' : String(value)
335
+
336
+ // Rive holds numbers as 32-bit floats, which rounds 25.55 to 25.5499992
337
+ const single = Math.fround ? Math.fround(value) : value
338
+ let text = d.round ? single.toFixed(d.decimals || 0) : single.toFixed(6)
339
+ if (d.trimZeros) text = trimZeros(text)
340
+ return text
341
+ }
342
+
343
+ /** @param {string} text */
344
+ function trimZeros(text) {
345
+ return text.indexOf('.') < 0 ? text : text.replace(/0+$/, '').replace(/\.$/, '')
346
+ }
347
+
348
+ /**
349
+ * @param {any} d
350
+ * @param {string} text
351
+ */
352
+ function pad(d, text) {
353
+ const filler = String(d.text || '')
354
+ if (!filler) return text
355
+ const wanted = Math.max(d.length, text.length)
356
+ let result = text
357
+ while (result.length < wanted) result = d.atEnd ? result + filler : filler + result
358
+ return d.atEnd ? result.slice(0, wanted) : result.slice(result.length - wanted)
359
+ }
360
+
361
+ /**
362
+ * An enum value as its index, from an index or a name.
363
+ *
364
+ * @param {any} value
365
+ * @param {string[]} [names]
366
+ */
367
+ function enumIndex(value, names) {
368
+ if (typeof value === 'string' && names) {
369
+ const at = names.indexOf(value)
370
+ if (at >= 0) return at
371
+ }
372
+ return numeric(value)
373
+ }