@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,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lays styled runs out in lines: greedy wrapping at the box width (at spaces,
|
|
3
|
+
* or anywhere with `wordBreak: 'breakAll'`, and inside a word wider than the
|
|
4
|
+
* box), alignment, line and paragraph spacing, an ellipsis for fixed text that
|
|
5
|
+
* overflows. Plain numbers and arrays in and out, so the BrightScript side can
|
|
6
|
+
* follow it. It does not shape (no ligatures, kerning or bidi): a glyph is a
|
|
7
|
+
* codepoint with the advance its font gives it plus the run's letter spacing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} Run
|
|
12
|
+
* @property {string} text
|
|
13
|
+
* @property {import('./text.font.js').FontInfo} font completed
|
|
14
|
+
* @property {number} size
|
|
15
|
+
* @property {number} lineHeight 0 for the font's own
|
|
16
|
+
* @property {number} letterSpacing
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} LayoutOptions
|
|
21
|
+
* @property {string} sizing `autoWidth`, `autoHeight` or `fixed`
|
|
22
|
+
* @property {string} overflow `visible`, `hidden`, `clipped` or `ellipsis`
|
|
23
|
+
* @property {string} align `left`, `center` or `right`
|
|
24
|
+
* @property {string} wrap `wrap` or `noWrap`
|
|
25
|
+
* @property {string} wordBreak `normal` or `breakAll`
|
|
26
|
+
* @property {string} origin `top` or `baseline`
|
|
27
|
+
* @property {string} direction `ltr` or `rtl`
|
|
28
|
+
* @property {number} maxWidth 0 for none
|
|
29
|
+
* @property {number} maxHeight 0 for none
|
|
30
|
+
* @property {number} paragraphSpacing
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {object} Glyph
|
|
35
|
+
* @property {number} id
|
|
36
|
+
* @property {Run} run
|
|
37
|
+
* @property {number} cp
|
|
38
|
+
* @property {number} advance
|
|
39
|
+
* @property {number} x pen position on the line
|
|
40
|
+
* @property {number} y baseline
|
|
41
|
+
* @property {number} textIndex 1-based codepoint index
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} Line
|
|
46
|
+
* @property {number} index
|
|
47
|
+
* @property {number} x
|
|
48
|
+
* @property {number} top
|
|
49
|
+
* @property {number} baseline
|
|
50
|
+
* @property {number} bottom
|
|
51
|
+
* @property {number} width
|
|
52
|
+
* @property {number} firstIndex
|
|
53
|
+
* @property {number} lastIndex
|
|
54
|
+
* @property {Glyph[]} glyphs
|
|
55
|
+
* @property {string} text
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {object} Layout
|
|
60
|
+
* @property {Line[]} lines
|
|
61
|
+
* @property {number} width
|
|
62
|
+
* @property {number} height
|
|
63
|
+
* @property {number} length codepoints
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} s
|
|
68
|
+
* @returns {number[]} its codepoints
|
|
69
|
+
*/
|
|
70
|
+
export function codepoints(s) {
|
|
71
|
+
/** @type {number[]} */
|
|
72
|
+
const out = []
|
|
73
|
+
for (let i = 0; i < s.length; i++) {
|
|
74
|
+
const c = s.charCodeAt(i)
|
|
75
|
+
const d = i + 1 < s.length ? s.charCodeAt(i + 1) : 0
|
|
76
|
+
if (c >= 0xd800 && c < 0xdc00 && d >= 0xdc00 && d < 0xe000) {
|
|
77
|
+
out.push((c - 0xd800) * 1024 + (d - 0xdc00) + 0x10000)
|
|
78
|
+
i++
|
|
79
|
+
} else out.push(c)
|
|
80
|
+
}
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {number} cp
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
function char(cp) {
|
|
89
|
+
if (cp < 0x10000) return String.fromCharCode(cp)
|
|
90
|
+
const v = cp - 0x10000
|
|
91
|
+
return String.fromCharCode(0xd800 + (v >> 10), 0xdc00 + (v & 1023))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {Run} run
|
|
96
|
+
* @param {number} cp
|
|
97
|
+
* @returns {number}
|
|
98
|
+
*/
|
|
99
|
+
function advanceOf(run, cp) {
|
|
100
|
+
const f = run.font
|
|
101
|
+
return (f.advance ? f.advance(cp) : 0.5) * run.size + run.letterSpacing
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {Run} run
|
|
106
|
+
* @returns {number}
|
|
107
|
+
*/
|
|
108
|
+
function heightOf(run) {
|
|
109
|
+
const f = /** @type {any} */ (run.font)
|
|
110
|
+
return run.lineHeight > 0 ? run.lineHeight : (f.descent - f.ascent) * run.size
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {Run[]} runs
|
|
115
|
+
* @param {LayoutOptions} o
|
|
116
|
+
* @returns {Layout}
|
|
117
|
+
*/
|
|
118
|
+
export function layoutText(runs, o) {
|
|
119
|
+
/** @type {Glyph[]} */
|
|
120
|
+
const all = []
|
|
121
|
+
let count = 0
|
|
122
|
+
for (let r = 0; r < runs.length; r++) {
|
|
123
|
+
const cps = codepoints(runs[r].text)
|
|
124
|
+
const f = runs[r].font
|
|
125
|
+
for (let k = 0; k < cps.length; k++) {
|
|
126
|
+
count++
|
|
127
|
+
all.push({ id: f.glyphId ? f.glyphId(cps[k]) : cps[k], run: runs[r], cp: cps[k], advance: cps[k] === 10 ? 0 : advanceOf(runs[r], cps[k]), x: 0, y: 0, textIndex: count })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const limit = o.wrap !== 'noWrap' && o.sizing !== 'autoWidth' && o.maxWidth > 0 ? o.maxWidth : Infinity
|
|
132
|
+
/** @type {{ glyphs: Glyph[], first: boolean }[]} */
|
|
133
|
+
const broken = []
|
|
134
|
+
/** @type {Glyph[]} */
|
|
135
|
+
let line = []
|
|
136
|
+
let width = 0
|
|
137
|
+
let softAt = 0
|
|
138
|
+
let first = true
|
|
139
|
+
const flush = () => {
|
|
140
|
+
broken.push({ glyphs: line, first })
|
|
141
|
+
first = false
|
|
142
|
+
}
|
|
143
|
+
for (let n = 0; n < all.length; n++) {
|
|
144
|
+
const g = all[n]
|
|
145
|
+
if (g.cp === 10) {
|
|
146
|
+
flush()
|
|
147
|
+
first = true
|
|
148
|
+
line = []
|
|
149
|
+
width = 0
|
|
150
|
+
softAt = 0
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
if (g.cp !== 32 && width + g.advance > limit && line.length > 0) {
|
|
154
|
+
if (o.wordBreak !== 'breakAll' && softAt > 0) {
|
|
155
|
+
const rest = line.splice(softAt)
|
|
156
|
+
flush()
|
|
157
|
+
line = rest
|
|
158
|
+
width = 0
|
|
159
|
+
for (let k = 0; k < rest.length; k++) width += rest[k].advance
|
|
160
|
+
} else {
|
|
161
|
+
flush()
|
|
162
|
+
line = []
|
|
163
|
+
width = 0
|
|
164
|
+
}
|
|
165
|
+
softAt = 0
|
|
166
|
+
}
|
|
167
|
+
line.push(g)
|
|
168
|
+
width += g.advance
|
|
169
|
+
if (g.cp === 32) softAt = line.length
|
|
170
|
+
}
|
|
171
|
+
flush()
|
|
172
|
+
|
|
173
|
+
const fallback = runs[0]
|
|
174
|
+
/** @type {Line[]} */
|
|
175
|
+
const lines = []
|
|
176
|
+
let top = 0
|
|
177
|
+
let widest = 0
|
|
178
|
+
for (let n = 0; n < broken.length; n++) {
|
|
179
|
+
const gs = broken[n].glyphs
|
|
180
|
+
let w = 0
|
|
181
|
+
let end = gs.length
|
|
182
|
+
while (end > 0 && gs[end - 1].cp === 32) end--
|
|
183
|
+
for (let k = 0; k < end; k++) w += gs[k].advance
|
|
184
|
+
let lineH = 0
|
|
185
|
+
let asc = 0
|
|
186
|
+
let desc = 0
|
|
187
|
+
/** @type {Run[]} */
|
|
188
|
+
const used = []
|
|
189
|
+
for (let k = 0; k < gs.length; k++) if (used.indexOf(gs[k].run) < 0) used.push(gs[k].run)
|
|
190
|
+
if (!used.length && fallback) used.push(fallback)
|
|
191
|
+
for (let k = 0; k < used.length; k++) {
|
|
192
|
+
const f = /** @type {any} */ (used[k].font)
|
|
193
|
+
lineH = Math.max(lineH, heightOf(used[k]))
|
|
194
|
+
asc = Math.max(asc, -f.ascent * used[k].size)
|
|
195
|
+
desc = Math.max(desc, f.descent * used[k].size)
|
|
196
|
+
}
|
|
197
|
+
if (n > 0 && broken[n].first) top += o.paragraphSpacing
|
|
198
|
+
const baseline = top + asc + (lineH - asc - desc) / 2
|
|
199
|
+
const firstIndex = gs.length ? gs[0].textIndex : n > 0 ? lines[n - 1].lastIndex + 2 : 1
|
|
200
|
+
lines.push({ index: n + 1, x: 0, top, baseline, bottom: top + lineH, width: w, firstIndex, lastIndex: gs.length ? gs[gs.length - 1].textIndex : firstIndex - 1, glyphs: gs, text: '' })
|
|
201
|
+
if (w > widest) widest = w
|
|
202
|
+
top += lineH
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// fixed text that overflows keeps the lines that fit; ellipsis ends the last in an ellipsis character
|
|
206
|
+
if (o.sizing === 'fixed' && o.maxHeight > 0 && (o.overflow === 'ellipsis' || o.overflow === 'hidden') && lines.length > 1) {
|
|
207
|
+
let keep = lines.length
|
|
208
|
+
while (keep > 1 && lines[keep - 1].bottom > o.maxHeight) keep--
|
|
209
|
+
if (keep < lines.length) {
|
|
210
|
+
lines.length = keep
|
|
211
|
+
if (o.overflow === 'ellipsis') ellipsize(lines[keep - 1], limit)
|
|
212
|
+
top = lines[keep - 1].bottom
|
|
213
|
+
widest = 0
|
|
214
|
+
for (let n = 0; n < lines.length; n++) widest = Math.max(widest, lines[n].width)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const boxWidth = o.sizing === 'autoWidth' || !(o.maxWidth > 0) ? widest : o.maxWidth
|
|
219
|
+
const shift = o.origin === 'baseline' && lines.length ? -lines[0].baseline : 0
|
|
220
|
+
for (let n = 0; n < lines.length; n++) {
|
|
221
|
+
const l = lines[n]
|
|
222
|
+
l.x = o.align === 'right' ? boxWidth - l.width : o.align === 'center' ? (boxWidth - l.width) / 2 : 0
|
|
223
|
+
l.top += shift
|
|
224
|
+
l.baseline += shift
|
|
225
|
+
l.bottom += shift
|
|
226
|
+
let pen = l.x
|
|
227
|
+
let s = ''
|
|
228
|
+
for (let k = 0; k < l.glyphs.length; k++) {
|
|
229
|
+
const g = l.glyphs[k]
|
|
230
|
+
g.x = pen
|
|
231
|
+
g.y = l.baseline
|
|
232
|
+
pen += g.advance
|
|
233
|
+
s += char(g.cp)
|
|
234
|
+
}
|
|
235
|
+
l.text = s
|
|
236
|
+
}
|
|
237
|
+
return { lines, width: boxWidth, height: top + shift, length: count }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {Line} line
|
|
242
|
+
* @param {number} limit
|
|
243
|
+
*/
|
|
244
|
+
function ellipsize(line, limit) {
|
|
245
|
+
const gs = line.glyphs
|
|
246
|
+
if (!gs.length) return
|
|
247
|
+
const run = gs[gs.length - 1].run
|
|
248
|
+
const dots = advanceOf(run, 0x2026)
|
|
249
|
+
let w = 0
|
|
250
|
+
for (let k = 0; k < gs.length; k++) w += gs[k].advance
|
|
251
|
+
while (gs.length > 1 && w + dots > limit) {
|
|
252
|
+
const g = /** @type {Glyph} */ (gs.pop())
|
|
253
|
+
w -= g.advance
|
|
254
|
+
}
|
|
255
|
+
const at = gs[gs.length - 1].textIndex
|
|
256
|
+
gs.push({ id: 0x2026, run, cp: 0x2026, advance: dots, x: 0, y: 0, textIndex: at + 1 })
|
|
257
|
+
line.width = w + dots
|
|
258
|
+
line.lastIndex = at + 1
|
|
259
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Context` a script's `init` receives and the `DataContext` it can ask
|
|
3
|
+
* for: where the script reaches its view model (the one bound to its node,
|
|
4
|
+
* the artboard's root one, global ones by name), asks to be updated again
|
|
5
|
+
* (`markNeedsUpdate`) and logs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EMPTY, LuaTable, NativeFunction } from '../../luau/vm/vm.value.js'
|
|
9
|
+
import { decodeUtf8, encodeUtf8 } from '../../luau/luau.convert.js'
|
|
10
|
+
import { defineType, text } from '../api/api.type.js'
|
|
11
|
+
import { viewModelAt } from './context.viewmodel.js'
|
|
12
|
+
|
|
13
|
+
/** @import { Scope, ViewModelAdapter } from './context.viewmodel.js' */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a context stands on: the host's view of the world.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {object} ContextEnv
|
|
19
|
+
* @property {(adapter: ViewModelAdapter) => Scope} scopeFor
|
|
20
|
+
* @property {ViewModelAdapter | null} immediate the view model bound to the script's node
|
|
21
|
+
* @property {ViewModelAdapter | null} root the artboard's own view model
|
|
22
|
+
* @property {ViewModelAdapter | null} parent the view model of the enclosing data context
|
|
23
|
+
* @property {Record<string, ViewModelAdapter>} globals global view models by name
|
|
24
|
+
* @property {() => void} markNeedsUpdate
|
|
25
|
+
* @property {{ image: (n: string) => any, font: (n: string) => any, audio: (n: string) => any }} [assets] what `image`, `font` and `audio` hand out
|
|
26
|
+
* @property {(message: string) => void} log
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {ContextEnv} env
|
|
31
|
+
* @param {ViewModelAdapter | null | undefined} adapter
|
|
32
|
+
* @returns {any} a ViewModel, or nil
|
|
33
|
+
*/
|
|
34
|
+
function viewModelOf(env, adapter) {
|
|
35
|
+
return adapter ? viewModelAt(env.scopeFor(adapter), '') : undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const DataContext = defineType('DataContext', {
|
|
39
|
+
methods: {
|
|
40
|
+
viewModel: (d) => viewModelOf(d.env, d.adapter),
|
|
41
|
+
parent: (d) => (d.parent ? DataContext.wrap({ env: d.env, adapter: d.parent, parent: null }) : undefined),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const Context = defineType('Context', {
|
|
46
|
+
// Rive's own docs call it `context.log('x')` and others `context:log('x')`, so it is a field holding a function that takes the message last
|
|
47
|
+
fields: {
|
|
48
|
+
log: {
|
|
49
|
+
get: (env) => new NativeFunction('Context.log', function (vm, args) {
|
|
50
|
+
const last = args[args.length - 1]
|
|
51
|
+
env.log(typeof last === 'string' ? decodeUtf8(last) : '')
|
|
52
|
+
return EMPTY
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
methods: {
|
|
57
|
+
viewModel: (env) => viewModelOf(env, env.immediate),
|
|
58
|
+
rootViewModel: (env) => viewModelOf(env, env.root),
|
|
59
|
+
globalViewModel: (env, args) => viewModelOf(env, env.globals[text(args, 1, 'globalViewModel')]),
|
|
60
|
+
globalViewModelNames: (env) => {
|
|
61
|
+
const t = new LuaTable()
|
|
62
|
+
const names = Object.keys(env.globals).map(encodeUtf8)
|
|
63
|
+
t.setRange(1, names, 0, names.length)
|
|
64
|
+
return t
|
|
65
|
+
},
|
|
66
|
+
dataContext: (env) => (env.immediate ? DataContext.wrap({ env, adapter: env.immediate, parent: env.parent }) : undefined),
|
|
67
|
+
markNeedsUpdate: (env) => {
|
|
68
|
+
env.markNeedsUpdate()
|
|
69
|
+
},
|
|
70
|
+
image: (env, args) => (env.assets ? env.assets.image(text(args, 1, 'image')) : undefined),
|
|
71
|
+
blob: () => undefined,
|
|
72
|
+
font: (env, args) => (env.assets ? env.assets.font(text(args, 1, 'font')) : undefined),
|
|
73
|
+
audio: (env, args) => (env.assets ? env.assets.audio(text(args, 1, 'audio')) : undefined),
|
|
74
|
+
shader: () => undefined,
|
|
75
|
+
},
|
|
76
|
+
})
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A view model held in memory, implementing `ViewModelAdapter`: for tests,
|
|
3
|
+
* for a player with no view model of its own, and as the reference for what
|
|
4
|
+
* an adapter does. Nested objects are nested view models; a property
|
|
5
|
+
* changes (and notifies) only when its value does.
|
|
6
|
+
*
|
|
7
|
+
* ```js
|
|
8
|
+
* const vm = createMemoryViewModel({ score: 0, arrow1: { position: 0 } }, { colors: [ 'tint' ] })
|
|
9
|
+
* vm.set('arrow1.position', 12)
|
|
10
|
+
* vm.get('arrow1.position') // 12
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @import { ViewModelAdapter } from './context.viewmodel.js' */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {ViewModelAdapter & { values: Record<string, any>, kinds: Record<string, string>, lists: Record<string, MemoryViewModel[]>, snapshot: () => Record<string, any> }} MemoryViewModel
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Dotted keys as nested objects, which is what `createMemoryViewModel` takes.
|
|
22
|
+
*
|
|
23
|
+
* @param {Record<string, any>} flat
|
|
24
|
+
* @returns {Record<string, any>}
|
|
25
|
+
*/
|
|
26
|
+
function nest(flat) {
|
|
27
|
+
/** @type {Record<string, any>} */
|
|
28
|
+
const out = {}
|
|
29
|
+
for (const key in flat) {
|
|
30
|
+
const parts = key.split('.')
|
|
31
|
+
let at = out
|
|
32
|
+
for (let n = 0; n < parts.length - 1; n++) at = at[parts[n]] || (at[parts[n]] = {})
|
|
33
|
+
at[parts[parts.length - 1]] = flat[key]
|
|
34
|
+
}
|
|
35
|
+
return out
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {Record<string, any>} [initial] property values; an object value is a nested view model
|
|
40
|
+
* @param {object} [options]
|
|
41
|
+
* @param {string[]} [options.colors] dotted paths of colour properties (ARGB numbers)
|
|
42
|
+
* @param {string[]} [options.triggers] paths of triggers
|
|
43
|
+
* @param {string[]} [options.enums] paths of enum properties (strings)
|
|
44
|
+
* @param {string[]} [options.images] paths of image properties (an asset's name)
|
|
45
|
+
* @param {string[]} [options.fonts] paths of font properties (an asset's name)
|
|
46
|
+
* @param {Record<string, Record<string, any>>} [options.instances] named instances for `instance(name)`, as initial objects
|
|
47
|
+
* @returns {MemoryViewModel}
|
|
48
|
+
*/
|
|
49
|
+
export function createMemoryViewModel(initial, options) {
|
|
50
|
+
const o = options || {}
|
|
51
|
+
/** @type {Record<string, any>} */
|
|
52
|
+
const values = {}
|
|
53
|
+
/** @type {Record<string, string>} */
|
|
54
|
+
const kinds = {}
|
|
55
|
+
/** @type {Record<string, ((value: any) => void)[]>} */
|
|
56
|
+
const subscribers = {}
|
|
57
|
+
/** @type {Record<string, MemoryViewModel[]>} list properties: their items, each a view model of its own */
|
|
58
|
+
const lists = {}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} path
|
|
62
|
+
* @param {any} value
|
|
63
|
+
*/
|
|
64
|
+
const define = (path, value) => {
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
kinds[path] = 'list'
|
|
67
|
+
lists[path] = value.map((item) => (item && typeof item.subscribe === 'function' ? item : createMemoryViewModel(item, o)))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (value !== null && typeof value === 'object') {
|
|
71
|
+
kinds[path] = 'viewModel'
|
|
72
|
+
for (const key in value) define(path === '' ? key : path + '.' + key, value[key])
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
values[path] = value
|
|
76
|
+
kinds[path] = typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string'
|
|
77
|
+
}
|
|
78
|
+
if (initial) for (const key in initial) define(key, initial[key])
|
|
79
|
+
/**
|
|
80
|
+
* @param {string[] | undefined} paths
|
|
81
|
+
* @param {string} kind
|
|
82
|
+
*/
|
|
83
|
+
const tag = (paths, kind) => {
|
|
84
|
+
if (paths) for (let n = 0; n < paths.length; n++) kinds[paths[n]] = kind
|
|
85
|
+
}
|
|
86
|
+
tag(o.colors, 'color')
|
|
87
|
+
tag(o.triggers, 'trigger')
|
|
88
|
+
tag(o.enums, 'enum')
|
|
89
|
+
tag(o.images, 'image')
|
|
90
|
+
tag(o.fonts, 'font')
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} path
|
|
94
|
+
* @param {any} value
|
|
95
|
+
*/
|
|
96
|
+
const notify = (path, value) => {
|
|
97
|
+
const list = subscribers[path]
|
|
98
|
+
if (!list) return
|
|
99
|
+
const copy = list.slice()
|
|
100
|
+
for (let n = 0; n < copy.length; n++) copy[n](value)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} path
|
|
105
|
+
*/
|
|
106
|
+
const changed = (path) => notify(path, lists[path].length)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
values,
|
|
110
|
+
kinds,
|
|
111
|
+
lists,
|
|
112
|
+
listSize: (path) => (lists[path] ? lists[path].length : 0),
|
|
113
|
+
listGet: (path, index) => (lists[path] ? lists[path][index] : undefined),
|
|
114
|
+
listInsert(path, item, index) {
|
|
115
|
+
const list = lists[path]
|
|
116
|
+
if (!list) return
|
|
117
|
+
list.splice(index, 0, /** @type {MemoryViewModel} */ (item))
|
|
118
|
+
changed(path)
|
|
119
|
+
},
|
|
120
|
+
listRemove(path, index) {
|
|
121
|
+
const list = lists[path]
|
|
122
|
+
if (!list || index < 0 || index >= list.length) return undefined
|
|
123
|
+
const out = list.splice(index, 1)[0]
|
|
124
|
+
changed(path)
|
|
125
|
+
return out
|
|
126
|
+
},
|
|
127
|
+
listClear(path) {
|
|
128
|
+
const list = lists[path]
|
|
129
|
+
if (!list || !list.length) return
|
|
130
|
+
list.length = 0
|
|
131
|
+
changed(path)
|
|
132
|
+
},
|
|
133
|
+
listSwap(path, a, b) {
|
|
134
|
+
const list = lists[path]
|
|
135
|
+
if (!list) return
|
|
136
|
+
const t = list[a]
|
|
137
|
+
list[a] = list[b]
|
|
138
|
+
list[b] = t
|
|
139
|
+
changed(path)
|
|
140
|
+
},
|
|
141
|
+
instance(path, name) {
|
|
142
|
+
if (name !== undefined && o.instances && o.instances[name]) return createMemoryViewModel(o.instances[name], o)
|
|
143
|
+
const prefix = path === '' ? '' : path + '.'
|
|
144
|
+
/** @type {Record<string, any>} */
|
|
145
|
+
const copy = {}
|
|
146
|
+
/** @type {string[]} */
|
|
147
|
+
const colors = []
|
|
148
|
+
/** @type {string[]} */
|
|
149
|
+
const enums = []
|
|
150
|
+
/** @type {string[]} */
|
|
151
|
+
const triggers = []
|
|
152
|
+
/** @type {string[]} */
|
|
153
|
+
const images = []
|
|
154
|
+
/** @type {string[]} */
|
|
155
|
+
const fonts = []
|
|
156
|
+
for (const key in kinds) {
|
|
157
|
+
if (key.indexOf(prefix) !== 0 || key === path) continue
|
|
158
|
+
const rest = key.slice(prefix.length)
|
|
159
|
+
const kind = kinds[key]
|
|
160
|
+
if (kind === 'viewModel') continue
|
|
161
|
+
copy[rest] = kind === 'list' ? [] : values[key]
|
|
162
|
+
if (kind === 'color') colors.push(rest)
|
|
163
|
+
else if (kind === 'enum') enums.push(rest)
|
|
164
|
+
else if (kind === 'trigger') triggers.push(rest)
|
|
165
|
+
else if (kind === 'image') images.push(rest)
|
|
166
|
+
else if (kind === 'font') fonts.push(rest)
|
|
167
|
+
}
|
|
168
|
+
return createMemoryViewModel(nest(copy), { colors, enums, triggers, images, fonts, instances: o.instances })
|
|
169
|
+
},
|
|
170
|
+
get: (path) => values[path],
|
|
171
|
+
kind: (path) => kinds[path],
|
|
172
|
+
set(path, value) {
|
|
173
|
+
if (kinds[path] === undefined) kinds[path] = typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string'
|
|
174
|
+
if (values[path] === value) return
|
|
175
|
+
values[path] = value
|
|
176
|
+
notify(path, value)
|
|
177
|
+
},
|
|
178
|
+
fire(path) {
|
|
179
|
+
notify(path, true)
|
|
180
|
+
},
|
|
181
|
+
subscribe(path, fn) {
|
|
182
|
+
const list = subscribers[path] || (subscribers[path] = [])
|
|
183
|
+
list.push(fn)
|
|
184
|
+
return function () {
|
|
185
|
+
const at = list.indexOf(fn)
|
|
186
|
+
if (at >= 0) list.splice(at, 1)
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
snapshot() {
|
|
190
|
+
/** @type {Record<string, any>} */
|
|
191
|
+
const out = {}
|
|
192
|
+
for (const key in values) out[key] = values[key]
|
|
193
|
+
return out
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|