@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,72 @@
1
+ /**
2
+ * The assets a script reaches by name (`context:image`, `font`, `audio`, an
3
+ * image or font view-model property, an artboard input). The player says what
4
+ * it has through `HostOptions.assets`; without that option every name is
5
+ * accepted, as an asset of unknown size and the fallback font, so a script
6
+ * still runs and the drawings still name what it used. With it, a name the
7
+ * player does not know is nil, as in Rive.
8
+ */
9
+
10
+ import { artboardHandle } from '../artboard/artboard.js'
11
+ import { AudioSource } from '../media/media.audio.js'
12
+ import { Image } from '../media/media.image.js'
13
+ import { Font, completeFont } from '../text/text.font.js'
14
+
15
+ /**
16
+ * @typedef {object} AssetSource
17
+ * @property {(name: string) => ({ width: number, height: number } | undefined)} [image]
18
+ * @property {(name: string) => (import('../text/text.font.js').FontInfo | undefined)} [font]
19
+ * @property {(name: string) => ({ duration: number } | undefined)} [audio]
20
+ * @property {(name: string) => ({ width: number, height: number } | undefined)} [artboard]
21
+ */
22
+
23
+ /**
24
+ * @param {AssetSource | undefined} source
25
+ * @returns {{ image: (name: string) => any, font: (name: string) => any, audio: (name: string) => any, artboard: (name: string) => any, fallback: () => any, input: (value: any) => any }}
26
+ */
27
+ export function createAssets(source) {
28
+ /** @type {Record<string, any>} */
29
+ const made = {}
30
+ /**
31
+ * @param {string} kind
32
+ * @param {string} name
33
+ * @param {() => any} make
34
+ */
35
+ const once = (kind, name, make) => {
36
+ const key = kind + ':' + name
37
+ if (!(key in made)) made[key] = make()
38
+ return made[key]
39
+ }
40
+ const fallbackFont = completeFont({ name: '' })
41
+ /** @type {any} */
42
+ const api = {
43
+ image: (/** @type {string} */ name) => once('image', name, () => {
44
+ const info = source ? (source.image ? source.image(name) : undefined) : { width: 0, height: 0 }
45
+ return info ? Image.wrap({ name, width: info.width, height: info.height }) : undefined
46
+ }),
47
+ font: (/** @type {string} */ name) => once('font', name, () => {
48
+ const info = source ? (source.font ? source.font(name) : undefined) : completeFont({ name })
49
+ return info ? Font.wrap(completeFont(info)) : undefined
50
+ }),
51
+ audio: (/** @type {string} */ name) => once('audio', name, () => {
52
+ const info = source ? (source.audio ? source.audio(name) : undefined) : { duration: 0 }
53
+ return info ? AudioSource.wrap({ name, duration: info.duration }) : undefined
54
+ }),
55
+ artboard: (/** @type {string} */ name) => once('artboard', name, () => {
56
+ const info = source ? (source.artboard ? source.artboard(name) : undefined) : { width: 0, height: 0 }
57
+ return info ? artboardHandle(name, info.width, info.height) : undefined
58
+ }),
59
+ fallback: () => fallbackFont,
60
+ /**
61
+ * An input value that names an asset, `{ kind: 'image' | 'font' | 'audio' | 'artboard', name }`, as the Lua value the script gets.
62
+ *
63
+ * @param {any} v
64
+ */
65
+ input(v) {
66
+ if (!v || typeof v !== 'object' || typeof v.kind !== 'string' || typeof v.name !== 'string') return v
67
+ return v.kind === 'image' ? api.image(v.name) : v.kind === 'font' ? api.font(v.name) : v.kind === 'audio' ? api.audio(v.name) : v.kind === 'artboard' ? api.artboard(v.name) : v
68
+ },
69
+ }
70
+ return api
71
+ }
72
+
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The values the protocols beyond nodes pass to a script: `DataValue` (what a
3
+ * converter script converts), `PointerEvent` (what a listener action script
4
+ * performs on) and `Vec2D` (what a layout script measures and is resized to,
5
+ * the same value as `Vector`).
6
+ */
7
+
8
+ import { LuaTable, NativeFunction } from '../../../luau/vm/vm.value.js'
9
+ import { encodeUtf8 } from '../../../luau/luau.convert.js'
10
+ import { badArg, defineType, num, text } from '../api.type.js'
11
+
12
+ /** The kinds of DataValue, by the constructor that makes them */
13
+ export const KINDS = [ 'number', 'string', 'boolean', 'color' ]
14
+
15
+ /** What each kind holds before a script sets it */
16
+ const EMPTY_VALUES = /** @type {Record<string, any>} */ ({ number: 0, string: '', boolean: false, color: 0 })
17
+
18
+ /**
19
+ * @typedef {object} DataValueData
20
+ * @property {'number' | 'string' | 'boolean' | 'color'} kind
21
+ * @property {any} value JS text for a string; a colour is its ARGB number
22
+ */
23
+
24
+ export const DataValue = defineType('DataValue', {
25
+ fields: {
26
+ value: {
27
+ get: (d) => (d.kind === 'string' ? encodeUtf8(d.value) : d.value),
28
+ set: (d, v) => {
29
+ if (d.kind === 'number' || d.kind === 'color') d.value = num([ v ], 0, 'value')
30
+ else if (d.kind === 'string') d.value = text([ v ], 0, 'value')
31
+ else if (typeof v === 'boolean') d.value = v
32
+ else throw badArg(0, 'value', 'boolean', [ v ])
33
+ },
34
+ },
35
+ },
36
+ methods: {
37
+ isNumber: (d) => d.kind === 'number',
38
+ isString: (d) => d.kind === 'string',
39
+ isBoolean: (d) => d.kind === 'boolean',
40
+ isColor: (d) => d.kind === 'color',
41
+ },
42
+ })
43
+
44
+ /**
45
+ * The DataValue kind a JS value is, when the player does not say.
46
+ *
47
+ * @param {any} value
48
+ * @returns {DataValueData['kind']}
49
+ */
50
+ export function kindOf(value) {
51
+ if (typeof value === 'string') return 'string'
52
+ if (typeof value === 'boolean') return 'boolean'
53
+ return 'number'
54
+ }
55
+
56
+ /**
57
+ * Fills a DataValue wrapper (or makes one) with a JS value.
58
+ *
59
+ * @param {any} wrapper an earlier wrapper to reuse, or undefined
60
+ * @param {any} value
61
+ * @param {string} [kind] `color` for an ARGB number; default by the value's type
62
+ * @returns {any}
63
+ */
64
+ export function dataValue(wrapper, value, kind) {
65
+ const k = kind || kindOf(value)
66
+ if (wrapper) {
67
+ wrapper.host.kind = k
68
+ wrapper.host.value = value
69
+ return wrapper
70
+ }
71
+ return DataValue.wrap({ kind: k, value })
72
+ }
73
+
74
+ /**
75
+ * @param {any} lua a value a script returned
76
+ * @returns {DataValueData | undefined} undefined when it is not a DataValue
77
+ */
78
+ export function readDataValue(lua) {
79
+ return DataValue.unwrap(lua)
80
+ }
81
+
82
+ /**
83
+ * Installs `DataValue` and `Vec2D`; `PointerEvent` is the pointer cluster's.
84
+ *
85
+ * @param {import('../../../luau/vm/vm.js').VM} vm
86
+ */
87
+ export function installData(vm) {
88
+ const values = new LuaTable()
89
+ for (const kind of KINDS) {
90
+ values.set(kind, new NativeFunction('DataValue.' + kind, function () {
91
+ return DataValue.wrap({ kind, value: EMPTY_VALUES[kind] })
92
+ }))
93
+ }
94
+ vm.globals.set('DataValue', values)
95
+
96
+
97
+ // a Vec2D is the Vector a script already has
98
+ vm.globals.set('Vec2D', vm.globals.get('Vector'))
99
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Paths as drawing commands and as Lottie vertex arrays. A command is
3
+ * `{ t, p }`: `t` one of `moveTo`, `lineTo`, `quadTo`, `cubicTo`, `close` and
4
+ * `p` its points flattened (`[x, y]`, `[cx, cy, x, y]`, `[c1x, c1y, c2x, c2y, x, y]`).
5
+ * A contour is Lottie's shape value: `v` vertices, `i` and `o` the in and out
6
+ * handles *relative to their vertex*, and `c` whether it closes.
7
+ */
8
+
9
+ /**
10
+ * @typedef {object} Command
11
+ * @property {'moveTo' | 'lineTo' | 'quadTo' | 'cubicTo' | 'close'} t
12
+ * @property {number[]} p
13
+ */
14
+
15
+ /**
16
+ * @typedef {object} Contour
17
+ * @property {number[][]} v
18
+ * @property {number[][]} i
19
+ * @property {number[][]} o
20
+ * @property {boolean} c
21
+ */
22
+
23
+ /** How many numbers each command carries */
24
+ export const POINTS = { moveTo: 2, lineTo: 2, quadTo: 4, cubicTo: 6, close: 0 }
25
+
26
+ /** A closing vertex this close to the first is the same vertex */
27
+ const SAME = 1e-9
28
+
29
+ /**
30
+ * @param {Command[]} cmds
31
+ * @param {number[]} [m] a matrix to apply, [xx, xy, yx, yy, tx, ty]
32
+ * @returns {Contour[]}
33
+ */
34
+ export function commandsToContours(cmds, m) {
35
+ /** @type {Contour[]} */
36
+ const out = []
37
+ /** @type {{ cur: Contour | null }} the contour being built */
38
+ const st = { cur: null }
39
+ let lx = 0
40
+ let ly = 0
41
+ let sx = 0
42
+ let sy = 0
43
+
44
+ /** @returns {Contour} */
45
+ const begin = () => {
46
+ const c = { v: [ [ lx, ly ] ], i: [ [ 0, 0 ] ], o: [ [ 0, 0 ] ], c: false }
47
+ st.cur = c
48
+ return c
49
+ }
50
+ const finish = () => {
51
+ if (st.cur && st.cur.v.length > 1) out.push(st.cur)
52
+ st.cur = null
53
+ }
54
+ /**
55
+ * @param {number} c1x
56
+ * @param {number} c1y
57
+ * @param {number} c2x
58
+ * @param {number} c2y
59
+ * @param {number} x
60
+ * @param {number} y
61
+ */
62
+ const curve = (c1x, c1y, c2x, c2y, x, y) => {
63
+ const c = st.cur || begin()
64
+ const last = c.v.length - 1
65
+ c.o[last] = [ c1x - c.v[last][0], c1y - c.v[last][1] ]
66
+ c.v.push([ x, y ])
67
+ c.i.push([ c2x - x, c2y - y ])
68
+ c.o.push([ 0, 0 ])
69
+ lx = x
70
+ ly = y
71
+ }
72
+
73
+ for (let n = 0; n < cmds.length; n++) {
74
+ const p = cmds[n].p
75
+ switch (cmds[n].t) {
76
+ case 'moveTo':
77
+ finish()
78
+ lx = sx = p[0]
79
+ ly = sy = p[1]
80
+ begin()
81
+ break
82
+ case 'lineTo': {
83
+ const c = st.cur || begin()
84
+ c.v.push([ p[0], p[1] ])
85
+ c.i.push([ 0, 0 ])
86
+ c.o.push([ 0, 0 ])
87
+ lx = p[0]
88
+ ly = p[1]
89
+ break
90
+ }
91
+ case 'quadTo':
92
+ curve(lx + (2 / 3) * (p[0] - lx), ly + (2 / 3) * (p[1] - ly), p[2] + (2 / 3) * (p[0] - p[2]), p[3] + (2 / 3) * (p[1] - p[3]), p[2], p[3])
93
+ break
94
+ case 'cubicTo':
95
+ curve(p[0], p[1], p[2], p[3], p[4], p[5])
96
+ break
97
+ default: {
98
+ // close
99
+ const c = st.cur
100
+ if (c) {
101
+ c.c = true
102
+ const last = c.v.length - 1
103
+ if (last > 0 && Math.abs(c.v[last][0] - c.v[0][0]) < SAME && Math.abs(c.v[last][1] - c.v[0][1]) < SAME) {
104
+ c.i[0] = c.i[last]
105
+ c.v.pop()
106
+ c.i.pop()
107
+ c.o.pop()
108
+ }
109
+ finish()
110
+ }
111
+ lx = sx
112
+ ly = sy
113
+ break
114
+ }
115
+ }
116
+ }
117
+ finish()
118
+ return m ? transformContours(out, m) : out
119
+ }
120
+
121
+ /**
122
+ * @param {Contour[]} contours
123
+ * @param {number[]} m
124
+ * @returns {Contour[]} the contours under the matrix; handles are vectors, so the translation does not touch them
125
+ */
126
+ export function transformContours(contours, m) {
127
+ return contours.map(function (c) {
128
+ return {
129
+ v: c.v.map((pt) => [ m[0] * pt[0] + m[2] * pt[1] + m[4], m[1] * pt[0] + m[3] * pt[1] + m[5] ]),
130
+ i: c.i.map((pt) => [ m[0] * pt[0] + m[2] * pt[1], m[1] * pt[0] + m[3] * pt[1] ]),
131
+ o: c.o.map((pt) => [ m[0] * pt[0] + m[2] * pt[1], m[1] * pt[0] + m[3] * pt[1] ]),
132
+ c: c.c,
133
+ }
134
+ })
135
+ }
136
+
137
+ /**
138
+ * @param {Contour[]} contours
139
+ * @returns {Command[]}
140
+ */
141
+ export function contoursToCommands(contours) {
142
+ /** @type {Command[]} */
143
+ const cmds = []
144
+ for (let n = 0; n < contours.length; n++) {
145
+ const c = contours[n]
146
+ if (c.v.length === 0) continue
147
+ cmds.push({ t: 'moveTo', p: [ c.v[0][0], c.v[0][1] ] })
148
+ const count = c.c ? c.v.length : c.v.length - 1
149
+ for (let k = 0; k < count; k++) {
150
+ const a = k
151
+ const b = (k + 1) % c.v.length
152
+ const o = c.o[a] || [ 0, 0 ]
153
+ const i = c.i[b] || [ 0, 0 ]
154
+ if (o[0] === 0 && o[1] === 0 && i[0] === 0 && i[1] === 0) {
155
+ if (b !== 0) cmds.push({ t: 'lineTo', p: [ c.v[b][0], c.v[b][1] ] })
156
+ } else {
157
+ cmds.push({ t: 'cubicTo', p: [ c.v[a][0] + o[0], c.v[a][1] + o[1], c.v[b][0] + i[0], c.v[b][1] + i[1], c.v[b][0], c.v[b][1] ] })
158
+ }
159
+ }
160
+ if (c.c) cmds.push({ t: 'close', p: [] })
161
+ }
162
+ return cmds
163
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Measuring paths: `path:measure()` (all contours as one length) and
3
+ * `path:contours()` (one `ContourMeasure` per contour, chained by `next`).
4
+ * Curves are measured as polylines of a fixed number of pieces per curve,
5
+ * which is exact for lines and within a hair for the curves a script draws.
6
+ */
7
+
8
+ import { Vector, fail } from '../../../luau/vm/vm.value.js'
9
+ import { defineType, num } from '../api.type.js'
10
+
11
+ /** @import { Command, Contour } from './draw.contours.js' */
12
+
13
+ /** Pieces a cubic segment is measured in */
14
+ const PIECES = 24
15
+
16
+ /**
17
+ * @typedef {object} Poly
18
+ * @property {number[][]} pts the polyline's points
19
+ * @property {number[]} cum the distance along it to each point
20
+ * @property {number} length
21
+ * @property {boolean} closed
22
+ */
23
+
24
+ /**
25
+ * @param {Contour} c
26
+ * @returns {Poly}
27
+ */
28
+ export function flatten(c) {
29
+ /** @type {number[][]} */
30
+ const pts = [ c.v[0] ]
31
+ const count = c.c ? c.v.length : c.v.length - 1
32
+ for (let k = 0; k < count; k++) {
33
+ const b = (k + 1) % c.v.length
34
+ const p0 = c.v[k]
35
+ const p3 = c.v[b]
36
+ const p1 = [ p0[0] + c.o[k][0], p0[1] + c.o[k][1] ]
37
+ const p2 = [ p3[0] + c.i[b][0], p3[1] + c.i[b][1] ]
38
+ const straight = c.o[k][0] === 0 && c.o[k][1] === 0 && c.i[b][0] === 0 && c.i[b][1] === 0
39
+ if (straight) {
40
+ pts.push(p3)
41
+ continue
42
+ }
43
+ for (let s = 1; s <= PIECES; s++) {
44
+ const t = s / PIECES
45
+ const u = 1 - t
46
+ pts.push([
47
+ u * u * u * p0[0] + 3 * u * u * t * p1[0] + 3 * u * t * t * p2[0] + t * t * t * p3[0],
48
+ u * u * u * p0[1] + 3 * u * u * t * p1[1] + 3 * u * t * t * p2[1] + t * t * t * p3[1],
49
+ ])
50
+ }
51
+ }
52
+ const cum = [ 0 ]
53
+ for (let n = 1; n < pts.length; n++) cum.push(cum[n - 1] + Math.sqrt((pts[n][0] - pts[n - 1][0]) * (pts[n][0] - pts[n - 1][0]) + (pts[n][1] - pts[n - 1][1]) * (pts[n][1] - pts[n - 1][1])))
54
+ return { pts, cum, length: cum[cum.length - 1], closed: c.c }
55
+ }
56
+
57
+ /**
58
+ * Where a polyline is at a distance, and which way it heads.
59
+ *
60
+ * @param {Poly} poly
61
+ * @param {number} d clamped to the polyline
62
+ * @returns {{ x: number, y: number, tx: number, ty: number }}
63
+ */
64
+ function sample(poly, d) {
65
+ const dist = Math.max(0, Math.min(poly.length, d))
66
+ let n = 1
67
+ while (n < poly.pts.length - 1 && poly.cum[n] < dist) n++
68
+ if (poly.pts.length < 2) return { x: poly.pts[0][0], y: poly.pts[0][1], tx: 1, ty: 0 }
69
+ const a = poly.pts[n - 1]
70
+ const b = poly.pts[n]
71
+ const span = poly.cum[n] - poly.cum[n - 1]
72
+ const t = span === 0 ? 0 : (dist - poly.cum[n - 1]) / span
73
+ const dx = b[0] - a[0]
74
+ const dy = b[1] - a[1]
75
+ const len = Math.sqrt(dx * dx + dy * dy) || 1
76
+ return { x: a[0] + dx * t, y: a[1] + dy * t, tx: dx / len, ty: dy / len }
77
+ }
78
+
79
+ /**
80
+ * @typedef {object} Measure
81
+ * @property {Poly[]} polys the contours measured, in order
82
+ * @property {number} first index of the first contour this measure covers
83
+ * @property {boolean} single whether it covers that one contour only (a ContourMeasure)
84
+ * @property {(dest: any, cmds: Command[]) => void} sink appends commands to the path an `extract` was given
85
+ */
86
+
87
+ /**
88
+ * @param {Measure} m
89
+ * @returns {number}
90
+ */
91
+ function totalLength(m) {
92
+ let sum = 0
93
+ const end = m.single ? m.first + 1 : m.polys.length
94
+ for (let n = m.first; n < end; n++) sum += m.polys[n].length
95
+ return sum
96
+ }
97
+
98
+ /**
99
+ * @param {Measure} m
100
+ * @param {number} d
101
+ * @returns {{ x: number, y: number, tx: number, ty: number }}
102
+ */
103
+ function sampleMeasure(m, d) {
104
+ const end = m.single ? m.first + 1 : m.polys.length
105
+ let rest = Math.max(0, Math.min(totalLength(m), d))
106
+ for (let n = m.first; n < end; n++) {
107
+ const poly = m.polys[n]
108
+ if (rest <= poly.length || n === end - 1) return sample(poly, rest)
109
+ rest -= poly.length
110
+ }
111
+ return { x: 0, y: 0, tx: 1, ty: 0 }
112
+ }
113
+
114
+ /**
115
+ * The commands for the part of a measure between two distances, as lines.
116
+ *
117
+ * @param {Measure} m
118
+ * @param {number} from
119
+ * @param {number} to
120
+ * @param {boolean} startWithMove
121
+ * @returns {Command[]}
122
+ */
123
+ function extract(m, from, to, startWithMove) {
124
+ /** @type {Command[]} */
125
+ const cmds = []
126
+ const end = m.single ? m.first + 1 : m.polys.length
127
+ const total = totalLength(m)
128
+ const a = Math.max(0, Math.min(total, Math.min(from, to)))
129
+ const b = Math.max(0, Math.min(total, Math.max(from, to)))
130
+ let offset = 0
131
+ let started = !startWithMove
132
+ for (let n = m.first; n < end; n++) {
133
+ const poly = m.polys[n]
134
+ const lo = Math.max(a, offset) - offset
135
+ const hi = Math.min(b, offset + poly.length) - offset
136
+ offset += poly.length
137
+ if (hi < lo || (hi === lo && b !== a)) continue
138
+ const p = sample(poly, lo)
139
+ cmds.push({ t: started ? 'lineTo' : 'moveTo', p: [ p.x, p.y ] })
140
+ started = true
141
+ for (let k = 1; k < poly.pts.length; k++) {
142
+ if (poly.cum[k] > lo && poly.cum[k] < hi) cmds.push({ t: 'lineTo', p: [ poly.pts[k][0], poly.pts[k][1] ] })
143
+ }
144
+ const q = sample(poly, hi)
145
+ if (hi > lo) cmds.push({ t: 'lineTo', p: [ q.x, q.y ] })
146
+ started = false
147
+ }
148
+ return cmds
149
+ }
150
+
151
+ /** @type {Record<string, (h: Measure, args: any[], vm: any) => any>} */
152
+ const SHARED = {
153
+ positionAndTangent: (h, args) => {
154
+ const s = sampleMeasure(h, num(args, 1, 'positionAndTangent'))
155
+ return [ new Vector(s.x, s.y, 0), new Vector(s.tx, s.ty, 0) ]
156
+ },
157
+ warp: (h, args) => {
158
+ const v = args[1]
159
+ if (!(v instanceof Vector)) throw fail("invalid argument #1 to 'warp' (Vector expected)")
160
+ const s = sampleMeasure(h, v.x)
161
+ return new Vector(s.x - s.ty * v.y, s.y + s.tx * v.y, 0)
162
+ },
163
+ extract: (h, args) => {
164
+ const startWithMove = args[4] === undefined || args[4] !== false
165
+ h.sink(args[3], extract(h, num(args, 1, 'extract'), num(args, 2, 'extract'), startWithMove))
166
+ },
167
+ }
168
+
169
+ const PathMeasure = defineType('PathMeasure', {
170
+ fields: {
171
+ length: { get: (h) => totalLength(h) },
172
+ isClosed: { get: (h) => h.polys.length === 1 && h.polys[0].closed },
173
+ },
174
+ methods: SHARED,
175
+ })
176
+
177
+ const ContourMeasure = defineType('ContourMeasure', {
178
+ fields: {
179
+ length: { get: (h) => totalLength(h) },
180
+ isClosed: { get: (h) => h.polys[h.first].closed },
181
+ next: { get: (h) => (h.first + 1 < h.polys.length ? ContourMeasure.wrap({ polys: h.polys, first: h.first + 1, single: true, sink: h.sink }) : undefined) },
182
+ },
183
+ methods: SHARED,
184
+ })
185
+
186
+ /**
187
+ * @param {Contour[]} contours
188
+ * @param {boolean} perContour a chain of ContourMeasures instead of one PathMeasure
189
+ * @param {(dest: any, cmds: Command[]) => void} sink appends commands to a script's Path
190
+ * @returns {any} the Lua value `contours()` or `measure()` returns
191
+ */
192
+ export function measureOf(contours, perContour, sink) {
193
+ const polys = contours.filter((c) => c.v.length > 0).map(flatten)
194
+ if (perContour) return polys.length === 0 ? undefined : ContourMeasure.wrap({ polys, first: 0, single: true, sink })
195
+ return PathMeasure.wrap({ polys, first: 0, single: false, sink })
196
+ }