@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,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
|
+
}
|