lecodes-cli 0.1.6 → 0.2.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/README.md +7 -1
- package/dist/index.js +202 -84
- package/package.json +4 -4
- package/runtime/sdk/animate/animate.ts +238 -0
- package/runtime/sdk/animate/bezier.ts +138 -0
- package/runtime/sdk/animate/easings.ts +126 -0
- package/runtime/sdk/core/Aspect.ts +151 -0
- package/runtime/sdk/core/color.ts +66 -0
- package/runtime/sdk/core/events.ts +38 -0
- package/runtime/sdk/core/registry.ts +23 -0
- package/runtime/sdk/g2/Camera2D.ts +33 -0
- package/runtime/sdk/g2/CharacterController2D.ts +75 -0
- package/runtime/sdk/g2/Node2D.ts +194 -0
- package/runtime/sdk/g2/Physics2D.ts +126 -0
- package/runtime/sdk/g2/Scene2D.ts +101 -0
- package/runtime/sdk/g2/Shape.ts +63 -0
- package/runtime/sdk/g2/Sprite.ts +79 -0
- package/runtime/sdk/g2/SpriteAnimation.ts +90 -0
- package/runtime/sdk/g2/Texture2D.ts +37 -0
- package/runtime/sdk/g2/Tilemap.ts +41 -0
- package/runtime/sdk/g2/Trigger.ts +47 -0
- package/runtime/sdk/g2/loop.ts +39 -0
- package/runtime/sdk/g2/touch.ts +83 -0
- package/runtime/sdk/gl/Camera.ts +40 -0
- package/runtime/sdk/gl/Collider.ts +29 -0
- package/runtime/sdk/gl/Geometry.ts +218 -0
- package/runtime/sdk/gl/Light.ts +37 -0
- package/runtime/sdk/gl/Material.ts +124 -0
- package/runtime/sdk/gl/Mesh.ts +100 -0
- package/runtime/sdk/gl/Model.ts +40 -0
- package/runtime/sdk/gl/ModelAnimation.ts +64 -0
- package/runtime/sdk/gl/Node.ts +234 -0
- package/runtime/sdk/gl/Noise.ts +30 -0
- package/runtime/sdk/gl/Particles.ts +185 -0
- package/runtime/sdk/gl/Physics.ts +95 -0
- package/runtime/sdk/gl/Plane.ts +53 -0
- package/runtime/sdk/gl/Ray.ts +16 -0
- package/runtime/sdk/gl/Scene.ts +179 -0
- package/runtime/sdk/gl/Texture.ts +35 -0
- package/runtime/sdk/gl/controls.ts +95 -0
- package/runtime/sdk/gl/state.ts +6 -0
- package/runtime/sdk/gl/touch.ts +56 -0
- package/runtime/sdk/inject.ts +85 -0
- package/runtime/sdk/math/Mathf.ts +118 -0
- package/runtime/sdk/math/mat4.ts +278 -0
- package/runtime/sdk/math/quat.ts +232 -0
- package/runtime/sdk/math/vec.ts +255 -0
- package/runtime/sdk/plugins/qr.ts +36 -0
- package/runtime/sdk/runtime/device.ts +33 -0
- package/runtime/sdk/runtime/fetch.ts +71 -0
- package/runtime/sdk/runtime/files.ts +23 -0
- package/runtime/sdk/runtime/input.ts +12 -0
- package/runtime/sdk/runtime/media.ts +111 -0
- package/runtime/sdk/runtime/misc.ts +12 -0
- package/runtime/sdk/runtime/net.ts +36 -0
- package/runtime/sdk/runtime/storage.ts +13 -0
- package/runtime/sdk/runtime/touch.ts +50 -0
- package/runtime/sdk/ui/UI.ts +19 -0
- package/runtime/sdk/ui/UIButton.ts +84 -0
- package/runtime/sdk/ui/UIContainer.ts +100 -0
- package/runtime/sdk/ui/UIImage.ts +68 -0
- package/runtime/sdk/ui/UIInput.ts +78 -0
- package/runtime/sdk/ui/UINode.ts +342 -0
- package/runtime/sdk/ui/UIScreen.ts +122 -0
- package/runtime/sdk/ui/UIScrollable.ts +70 -0
- package/runtime/sdk/ui/UISpacer.ts +14 -0
- package/runtime/sdk/ui/UIText.ts +40 -0
- package/runtime/sdk/ui/UIVideo.ts +38 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +222 -0
- package/runtime/sdk/ui/UIWidget.ts +97 -0
- package/runtime/sdk/ui/fonts.ts +10 -0
- package/runtime/sdk/ui/router.ts +73 -0
- package/runtime/inject.js +0 -4629
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// animate self-implements its matrix decompose/recompose; these are just the numeric-array shapes.
|
|
2
|
+
type mat4 = Float32Array | number[]
|
|
3
|
+
type quat = Float32Array | number[]
|
|
4
|
+
type vec2 = Float32Array | number[]
|
|
5
|
+
type vec3 = Float32Array | number[]
|
|
6
|
+
import type { Easing } from "./easings"
|
|
7
|
+
|
|
8
|
+
type AnimateProps = number | string | vec2 | vec3 | quat
|
|
9
|
+
|
|
10
|
+
export type AnimateOptions<T> = {
|
|
11
|
+
from: T,
|
|
12
|
+
to: T,
|
|
13
|
+
duration: number,
|
|
14
|
+
onUpdate(val: T): void,
|
|
15
|
+
onComplete?(): void,
|
|
16
|
+
easing?: Easing,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const animate = <T extends AnimateProps>(options: AnimateOptions<T>): number => {
|
|
20
|
+
|
|
21
|
+
let onRawUpdate
|
|
22
|
+
if (typeof options.to === "number") {
|
|
23
|
+
const { onUpdate, from, to } = options as AnimateOptions<number>
|
|
24
|
+
onRawUpdate = (t: number) => onUpdate(from + (to - from) * t)
|
|
25
|
+
} else if (options.to === "string") {
|
|
26
|
+
// TODO: Make color animate here
|
|
27
|
+
const { onUpdate, from, to } = options as AnimateOptions<string>
|
|
28
|
+
onRawUpdate = (t: number) => onUpdate(from)
|
|
29
|
+
|
|
30
|
+
// onRawUpdate = (val: number) => onUpdate(from + (to - from) * val)
|
|
31
|
+
} else if (options.to.length === 2) {
|
|
32
|
+
const { onUpdate, from, to } = options as AnimateOptions<vec2>
|
|
33
|
+
const tempVec = new Float32Array(3)
|
|
34
|
+
onRawUpdate = (t: number) => onUpdate(lerp2(tempVec, from, to, t))
|
|
35
|
+
|
|
36
|
+
} else if (options.to.length === 3) {
|
|
37
|
+
const { onUpdate, from, to } = options as AnimateOptions<vec3>
|
|
38
|
+
const tempVec = new Float32Array(3)
|
|
39
|
+
|
|
40
|
+
onRawUpdate = (t: number) => onUpdate(lerp(tempVec, from, to, t))
|
|
41
|
+
} else if (options.to.length === 4) {
|
|
42
|
+
const { onUpdate, from, to } = options as AnimateOptions<quat>
|
|
43
|
+
|
|
44
|
+
const tempQuat = new Float32Array(4)
|
|
45
|
+
onRawUpdate = (t: number) => onUpdate(slerp(tempQuat, from, to, t))
|
|
46
|
+
} else {
|
|
47
|
+
onRawUpdate = () => {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const animationId = _creatorUtils.animate(val => {
|
|
51
|
+
const _val = options.easing? options.easing(val): val
|
|
52
|
+
onRawUpdate(_val)
|
|
53
|
+
if (val === 1 && options.onComplete) {
|
|
54
|
+
options.onComplete()
|
|
55
|
+
}
|
|
56
|
+
}, options.duration)
|
|
57
|
+
|
|
58
|
+
return animationId
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const stopAnimation = (id: number): void => _creatorUtils.stopAnimation(id)
|
|
62
|
+
export const pauseAnimation = (id: number): void => _creatorUtils.pauseAnimation(id)
|
|
63
|
+
export const resumeAnimation = (id: number): void => _creatorUtils.resumeAnimation(id)
|
|
64
|
+
|
|
65
|
+
export const animateMat4 = (options: AnimateOptions<mat4>): number => {
|
|
66
|
+
const fromPosition = new Float32Array(3)
|
|
67
|
+
const fromQuaternion = new Float32Array(4)
|
|
68
|
+
const fromScale = new Float32Array(3)
|
|
69
|
+
const toPosition = new Float32Array(3)
|
|
70
|
+
const toQuaternion = new Float32Array(4)
|
|
71
|
+
const toScale = new Float32Array(3)
|
|
72
|
+
decompose(fromQuaternion, fromPosition, fromScale, (options as AnimateOptions<mat4>).from)
|
|
73
|
+
decompose(toQuaternion, toPosition, toScale, (options as AnimateOptions<mat4>).to)
|
|
74
|
+
|
|
75
|
+
const tempPosition = new Float32Array(3)
|
|
76
|
+
const tempQuat = new Float32Array(4)
|
|
77
|
+
const tempScale = new Float32Array(3)
|
|
78
|
+
const tempMat = new Float32Array(16)
|
|
79
|
+
|
|
80
|
+
const onUpdate = (options as AnimateOptions<mat4>).onUpdate
|
|
81
|
+
const onRawUpdate = (t: number) => {
|
|
82
|
+
lerp(tempPosition, fromPosition, toPosition, t)
|
|
83
|
+
slerp(tempQuat, fromQuaternion, toQuaternion, t)
|
|
84
|
+
lerp(tempScale, fromScale, toScale, t)
|
|
85
|
+
|
|
86
|
+
onUpdate(fromRotationTranslationScale(tempMat, tempQuat, tempPosition, tempScale))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const animationId = _creatorUtils.animate(val => {
|
|
90
|
+
const _val = options.easing? options.easing(val): val
|
|
91
|
+
onRawUpdate(_val)
|
|
92
|
+
if (val === 1 && options.onComplete) {
|
|
93
|
+
options.onComplete()
|
|
94
|
+
}
|
|
95
|
+
}, options.duration)
|
|
96
|
+
|
|
97
|
+
return animationId
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const EPSILON = 1e-6;
|
|
101
|
+
function slerp(out: quat, a: quat, b: quat, t: number) {
|
|
102
|
+
const ax = a[0], ay = a[1], az = a[2], aw = a[3];
|
|
103
|
+
let bx = b[0], by = b[1], bz = b[2], bw = b[3];
|
|
104
|
+
let omega, cosom, sinom, scale0, scale1;
|
|
105
|
+
cosom = ax * bx + ay * by + az * bz + aw * bw;
|
|
106
|
+
if (cosom < 0) {
|
|
107
|
+
cosom = -cosom;
|
|
108
|
+
bx = -bx;
|
|
109
|
+
by = -by;
|
|
110
|
+
bz = -bz;
|
|
111
|
+
bw = -bw;
|
|
112
|
+
}
|
|
113
|
+
if (1 - cosom > EPSILON) {
|
|
114
|
+
omega = Math.acos(cosom);
|
|
115
|
+
sinom = Math.sin(omega);
|
|
116
|
+
scale0 = Math.sin((1 - t) * omega) / sinom;
|
|
117
|
+
scale1 = Math.sin(t * omega) / sinom;
|
|
118
|
+
} else {
|
|
119
|
+
scale0 = 1 - t;
|
|
120
|
+
scale1 = t;
|
|
121
|
+
}
|
|
122
|
+
out[0] = scale0 * ax + scale1 * bx;
|
|
123
|
+
out[1] = scale0 * ay + scale1 * by;
|
|
124
|
+
out[2] = scale0 * az + scale1 * bz;
|
|
125
|
+
out[3] = scale0 * aw + scale1 * bw;
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
function lerp(out: vec3, a: vec3, b: vec3, t: number) {
|
|
129
|
+
const ax = a[0];
|
|
130
|
+
const ay = a[1];
|
|
131
|
+
const az = a[2];
|
|
132
|
+
out[0] = ax + t * (b[0] - ax);
|
|
133
|
+
out[1] = ay + t * (b[1] - ay);
|
|
134
|
+
out[2] = az + t * (b[2] - az);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function lerp2(out: vec2, a: vec2, b: vec2, t: number) {
|
|
139
|
+
const ax = a[0];
|
|
140
|
+
const ay = a[1];
|
|
141
|
+
const az = a[2];
|
|
142
|
+
out[0] = ax + t * (b[0] - ax);
|
|
143
|
+
out[1] = ay + t * (b[1] - ay);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
function fromRotationTranslationScale(out: mat4, q: quat, v: vec3, s: vec3) {
|
|
147
|
+
const x = q[0], y = q[1], z = q[2], w = q[3];
|
|
148
|
+
const x2 = x + x;
|
|
149
|
+
const y2 = y + y;
|
|
150
|
+
const z2 = z + z;
|
|
151
|
+
const xx = x * x2;
|
|
152
|
+
const xy = x * y2;
|
|
153
|
+
const xz = x * z2;
|
|
154
|
+
const yy = y * y2;
|
|
155
|
+
const yz = y * z2;
|
|
156
|
+
const zz = z * z2;
|
|
157
|
+
const wx = w * x2;
|
|
158
|
+
const wy = w * y2;
|
|
159
|
+
const wz = w * z2;
|
|
160
|
+
const sx = s[0];
|
|
161
|
+
const sy = s[1];
|
|
162
|
+
const sz = s[2];
|
|
163
|
+
out[0] = (1 - (yy + zz)) * sx;
|
|
164
|
+
out[1] = (xy + wz) * sx;
|
|
165
|
+
out[2] = (xz - wy) * sx;
|
|
166
|
+
out[3] = 0;
|
|
167
|
+
out[4] = (xy - wz) * sy;
|
|
168
|
+
out[5] = (1 - (xx + zz)) * sy;
|
|
169
|
+
out[6] = (yz + wx) * sy;
|
|
170
|
+
out[7] = 0;
|
|
171
|
+
out[8] = (xz + wy) * sz;
|
|
172
|
+
out[9] = (yz - wx) * sz;
|
|
173
|
+
out[10] = (1 - (xx + yy)) * sz;
|
|
174
|
+
out[11] = 0;
|
|
175
|
+
out[12] = v[0];
|
|
176
|
+
out[13] = v[1];
|
|
177
|
+
out[14] = v[2];
|
|
178
|
+
out[15] = 1;
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function decompose(out_r: mat4, out_t: vec3, out_s: vec3, mat: mat4) {
|
|
183
|
+
out_t[0] = mat[12];
|
|
184
|
+
out_t[1] = mat[13];
|
|
185
|
+
out_t[2] = mat[14];
|
|
186
|
+
const m11 = mat[0];
|
|
187
|
+
const m12 = mat[1];
|
|
188
|
+
const m13 = mat[2];
|
|
189
|
+
const m21 = mat[4];
|
|
190
|
+
const m22 = mat[5];
|
|
191
|
+
const m23 = mat[6];
|
|
192
|
+
const m31 = mat[8];
|
|
193
|
+
const m32 = mat[9];
|
|
194
|
+
const m33 = mat[10];
|
|
195
|
+
out_s[0] = Math.hypot(m11, m12, m13);
|
|
196
|
+
out_s[1] = Math.hypot(m21, m22, m23);
|
|
197
|
+
out_s[2] = Math.hypot(m31, m32, m33);
|
|
198
|
+
const is1 = 1 / out_s[0];
|
|
199
|
+
const is2 = 1 / out_s[1];
|
|
200
|
+
const is3 = 1 / out_s[2];
|
|
201
|
+
const sm11 = m11 * is1;
|
|
202
|
+
const sm12 = m12 * is2;
|
|
203
|
+
const sm13 = m13 * is3;
|
|
204
|
+
const sm21 = m21 * is1;
|
|
205
|
+
const sm22 = m22 * is2;
|
|
206
|
+
const sm23 = m23 * is3;
|
|
207
|
+
const sm31 = m31 * is1;
|
|
208
|
+
const sm32 = m32 * is2;
|
|
209
|
+
const sm33 = m33 * is3;
|
|
210
|
+
const trace = sm11 + sm22 + sm33;
|
|
211
|
+
let S = 0;
|
|
212
|
+
if (trace > 0) {
|
|
213
|
+
S = Math.sqrt(trace + 1) * 2;
|
|
214
|
+
out_r[3] = 0.25 * S;
|
|
215
|
+
out_r[0] = (sm23 - sm32) / S;
|
|
216
|
+
out_r[1] = (sm31 - sm13) / S;
|
|
217
|
+
out_r[2] = (sm12 - sm21) / S;
|
|
218
|
+
} else if (sm11 > sm22 && sm11 > sm33) {
|
|
219
|
+
S = Math.sqrt(1 + sm11 - sm22 - sm33) * 2;
|
|
220
|
+
out_r[3] = (sm23 - sm32) / S;
|
|
221
|
+
out_r[0] = 0.25 * S;
|
|
222
|
+
out_r[1] = (sm12 + sm21) / S;
|
|
223
|
+
out_r[2] = (sm31 + sm13) / S;
|
|
224
|
+
} else if (sm22 > sm33) {
|
|
225
|
+
S = Math.sqrt(1 + sm22 - sm11 - sm33) * 2;
|
|
226
|
+
out_r[3] = (sm31 - sm13) / S;
|
|
227
|
+
out_r[0] = (sm12 + sm21) / S;
|
|
228
|
+
out_r[1] = 0.25 * S;
|
|
229
|
+
out_r[2] = (sm23 + sm32) / S;
|
|
230
|
+
} else {
|
|
231
|
+
S = Math.sqrt(1 + sm33 - sm11 - sm22) * 2;
|
|
232
|
+
out_r[3] = (sm12 - sm21) / S;
|
|
233
|
+
out_r[0] = (sm31 + sm13) / S;
|
|
234
|
+
out_r[1] = (sm23 + sm32) / S;
|
|
235
|
+
out_r[2] = 0.25 * S;
|
|
236
|
+
}
|
|
237
|
+
return out_r;
|
|
238
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Bezier function generator
|
|
3
|
+
|
|
4
|
+
Gaëtan Renaudeau's BezierEasing
|
|
5
|
+
https://github.com/gre/bezier-easing/blob/master/src/index.js
|
|
6
|
+
https://github.com/gre/bezier-easing/blob/master/LICENSE
|
|
7
|
+
You're a hero
|
|
8
|
+
|
|
9
|
+
Use
|
|
10
|
+
|
|
11
|
+
const easeOut = cubicBezier(.17,.67,.83,.67);
|
|
12
|
+
const x = easeOut(0.5); // returns 0.627...
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Easing } from "./easings"
|
|
16
|
+
|
|
17
|
+
const a = (a1: number, a2: number) => 1.0 - 3.0 * a2 + 3.0 * a1
|
|
18
|
+
const b = (a1: number, a2: number) => 3.0 * a2 - 6.0 * a1
|
|
19
|
+
const c = (a1: number) => 3.0 * a1
|
|
20
|
+
|
|
21
|
+
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
|
|
22
|
+
const calcBezier = (t: number, a1: number, a2: number) =>
|
|
23
|
+
((a(a1, a2) * t + b(a1, a2)) * t + c(a1)) * t
|
|
24
|
+
|
|
25
|
+
// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
|
|
26
|
+
const getSlope = (t: number, a1: number, a2: number) =>
|
|
27
|
+
3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1)
|
|
28
|
+
|
|
29
|
+
const subdivisionPrecision = 0.0000001
|
|
30
|
+
const subdivisionMaxIterations = 10
|
|
31
|
+
|
|
32
|
+
function binarySubdivide(
|
|
33
|
+
aX: number,
|
|
34
|
+
aA: number,
|
|
35
|
+
aB: number,
|
|
36
|
+
mX1: number,
|
|
37
|
+
mX2: number
|
|
38
|
+
) {
|
|
39
|
+
let currentX: number
|
|
40
|
+
let currentT: number
|
|
41
|
+
let i: number = 0
|
|
42
|
+
|
|
43
|
+
do {
|
|
44
|
+
currentT = aA + (aB - aA) / 2.0
|
|
45
|
+
currentX = calcBezier(currentT, mX1, mX2) - aX
|
|
46
|
+
if (currentX > 0.0) {
|
|
47
|
+
aB = currentT
|
|
48
|
+
} else {
|
|
49
|
+
aA = currentT
|
|
50
|
+
}
|
|
51
|
+
} while (
|
|
52
|
+
Math.abs(currentX) > subdivisionPrecision &&
|
|
53
|
+
++i < subdivisionMaxIterations
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return currentT
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const newtonIterations = 8
|
|
60
|
+
const newtonMinSlope = 0.001
|
|
61
|
+
function newtonRaphsonIterate(
|
|
62
|
+
aX: number,
|
|
63
|
+
aGuessT: number,
|
|
64
|
+
mX1: number,
|
|
65
|
+
mX2: number
|
|
66
|
+
) {
|
|
67
|
+
for (let i = 0; i < newtonIterations; ++i) {
|
|
68
|
+
const currentSlope = getSlope(aGuessT, mX1, mX2)
|
|
69
|
+
if (currentSlope === 0.0) {
|
|
70
|
+
return aGuessT
|
|
71
|
+
}
|
|
72
|
+
const currentX = calcBezier(aGuessT, mX1, mX2) - aX
|
|
73
|
+
aGuessT -= currentX / currentSlope
|
|
74
|
+
}
|
|
75
|
+
return aGuessT
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const kSplineTableSize = 11
|
|
79
|
+
const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0)
|
|
80
|
+
|
|
81
|
+
const linear: Easing = p => p
|
|
82
|
+
|
|
83
|
+
export function cubicBezier(
|
|
84
|
+
mX1: number,
|
|
85
|
+
mY1: number,
|
|
86
|
+
mX2: number,
|
|
87
|
+
mY2: number
|
|
88
|
+
): Easing {
|
|
89
|
+
// If this is a linear gradient, return linear easing
|
|
90
|
+
if (mX1 === mY1 && mX2 === mY2) return linear
|
|
91
|
+
|
|
92
|
+
// Precompute samples table
|
|
93
|
+
const sampleValues = new Float32Array(kSplineTableSize)
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < kSplineTableSize; ++i) {
|
|
96
|
+
sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getTForX(aX: number) {
|
|
100
|
+
let intervalStart = 0.0
|
|
101
|
+
let currentSample = 1
|
|
102
|
+
const lastSample = kSplineTableSize - 1
|
|
103
|
+
|
|
104
|
+
for (
|
|
105
|
+
;
|
|
106
|
+
currentSample !== lastSample && sampleValues[currentSample] <= aX;
|
|
107
|
+
++currentSample
|
|
108
|
+
) {
|
|
109
|
+
intervalStart += kSampleStepSize
|
|
110
|
+
}
|
|
111
|
+
--currentSample
|
|
112
|
+
|
|
113
|
+
// Interpolate to provide an initial guess for t
|
|
114
|
+
const dist =
|
|
115
|
+
(aX - sampleValues[currentSample]) /
|
|
116
|
+
(sampleValues[currentSample + 1] - sampleValues[currentSample])
|
|
117
|
+
const guessForT = intervalStart + dist * kSampleStepSize
|
|
118
|
+
|
|
119
|
+
const initialSlope = getSlope(guessForT, mX1, mX2)
|
|
120
|
+
if (initialSlope >= newtonMinSlope) {
|
|
121
|
+
return newtonRaphsonIterate(aX, guessForT, mX1, mX2)
|
|
122
|
+
} else if (initialSlope === 0.0) {
|
|
123
|
+
return guessForT
|
|
124
|
+
} else {
|
|
125
|
+
return binarySubdivide(
|
|
126
|
+
aX,
|
|
127
|
+
intervalStart,
|
|
128
|
+
intervalStart + kSampleStepSize,
|
|
129
|
+
mX1,
|
|
130
|
+
mX2
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If animation is at start/end, return t without easing
|
|
136
|
+
return (t: number) =>
|
|
137
|
+
t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2)
|
|
138
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export type Easing = (v: number) => number
|
|
2
|
+
export type EasingModifier = (easing: Easing) => Easing
|
|
3
|
+
|
|
4
|
+
const DEFAULT_OVERSHOOT_STRENGTH = 1.525
|
|
5
|
+
const BOUNCE_FIRST_THRESHOLD = 4.0 / 11.0
|
|
6
|
+
const BOUNCE_SECOND_THRESHOLD = 8.0 / 11.0
|
|
7
|
+
const BOUNCE_THIRD_THRESHOLD = 9.0 / 10.0
|
|
8
|
+
|
|
9
|
+
// helper constants
|
|
10
|
+
const ca = 4356.0 / 361.0
|
|
11
|
+
const cb = 35442.0 / 1805.0
|
|
12
|
+
const cc = 16061.0 / 1805.0
|
|
13
|
+
|
|
14
|
+
export function linear(p: number): number {
|
|
15
|
+
return p
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function bounceOut(p: number): number {
|
|
19
|
+
if (p === 1 || p === 0) return p
|
|
20
|
+
const p2 = p * p
|
|
21
|
+
|
|
22
|
+
return p < BOUNCE_FIRST_THRESHOLD
|
|
23
|
+
? 7.5625 * p2
|
|
24
|
+
: p < BOUNCE_SECOND_THRESHOLD
|
|
25
|
+
? 9.075 * p2 - 9.9 * p + 3.4
|
|
26
|
+
: p < BOUNCE_THIRD_THRESHOLD
|
|
27
|
+
? ca * p2 - cb * p + cc
|
|
28
|
+
: 10.8 * p * p - 20.52 * p + 10.72
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function bounceInOut(p: number): number {
|
|
32
|
+
return p < 0.5
|
|
33
|
+
? 0.5 * (1.0 - bounceOut(1.0 - p * 2.0))
|
|
34
|
+
: 0.5 * bounceOut(p * 2.0 - 1.0) + 0.5
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Accepts an easing function and returns a new one that outputs reversed values.
|
|
38
|
+
// Turns easeIn into easeOut.
|
|
39
|
+
export function reverseEasing(easing: Easing): Easing {
|
|
40
|
+
return p => 1 - easing(1 - p)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Accepts an easing function and returns a new one that outputs mirrored values for
|
|
44
|
+
// the second half of the animation. Turns easeIn into easeInOut.
|
|
45
|
+
export function mirrorEasing(easing: Easing): Easing {
|
|
46
|
+
return p => (p <= 0.5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Creates an easing function that is based on the exponent of the provided `power`.
|
|
50
|
+
// The higher the `power`, the stronger the easing.
|
|
51
|
+
export function createExpoIn(power: number): Easing {
|
|
52
|
+
return p => p ** power
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Creates an easing function that has a stronger overshoot the higher the provided `power`.
|
|
56
|
+
export function createBackIn(power: number): Easing {
|
|
57
|
+
return p => p * p * ((power + 1) * p - power)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Creates an easing function that pulls back a little before moving, and then
|
|
61
|
+
// has a `createBackIn`-based overshoot
|
|
62
|
+
export function createAnticipate(power: number): Easing {
|
|
63
|
+
const backEasing = createBackIn(power)
|
|
64
|
+
|
|
65
|
+
return p =>
|
|
66
|
+
(p *= 2) < 1
|
|
67
|
+
? 0.5 * backEasing(p)
|
|
68
|
+
: 0.5 * (2 - Math.pow(2, -10 * (p - 1)))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function bounceIn(p: number): number {
|
|
72
|
+
return 1 - bounceOut(1 - p)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function easeIn(p: number): number {
|
|
76
|
+
return p * p
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function easeOut(p: number): number {
|
|
80
|
+
return 1 - easeIn(1 - p)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function easeInOut(p: number): number {
|
|
84
|
+
return p <= 0.5
|
|
85
|
+
? easeIn(2 * p) / 2
|
|
86
|
+
: (2 - easeIn(2 * (1 - p))) / 2
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function circIn(p: number): number {
|
|
90
|
+
return 1 - Math.sin(Math.acos(p))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function circOut(p: number): number {
|
|
94
|
+
return 1 - circIn(1 - p)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function circInOut(p: number): number {
|
|
98
|
+
return p <= 0.5
|
|
99
|
+
? circOut(2 * p) / 2
|
|
100
|
+
: (2 - circOut(2 * (1 - p))) / 2
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function backIn(p: number): number {
|
|
104
|
+
const power = DEFAULT_OVERSHOOT_STRENGTH
|
|
105
|
+
return p * p * ((power + 1) * p - power)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function backOut(p: number): number {
|
|
109
|
+
return 1 - backIn(1 - p)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function backInOut(p: number): number {
|
|
113
|
+
return p <= 0.5
|
|
114
|
+
? backIn(2 * p) / 2
|
|
115
|
+
: (2 - backIn(2 * (1 - p))) / 2
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function anticipate(p: number): number {
|
|
119
|
+
const doubled = p * 2
|
|
120
|
+
|
|
121
|
+
if (doubled < 1) {
|
|
122
|
+
return 0.5 * backIn(doubled)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return 0.5 * (2 - Math.pow(2, -10 * (doubled - 1)))
|
|
126
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// The Aspect system: attachable capabilities (animation, collider, physics, and user behaviors)
|
|
2
|
+
// that hang off a node WITHOUT bloating the node class. Composition with the terseness of methods.
|
|
3
|
+
//
|
|
4
|
+
// const hero = new Sprite({ texture })
|
|
5
|
+
// .aspect(SpriteAnimation, { fps: 8, clips: { walk: [1, 2, 3] } }) // attach + configure
|
|
6
|
+
// .aspect(Collider, { box: [16, 8], trigger: true }) // chains: returns the node
|
|
7
|
+
// hero.anim.play('walk') // access by name (the chain result is typed as having it)
|
|
8
|
+
// if (hero.has(Physics)) hero.physics.velocity = [0, 0] // has() is a type guard
|
|
9
|
+
//
|
|
10
|
+
// A user aspect declares its name + target node in the generic and reads `this.node`:
|
|
11
|
+
// class Health extends Aspect<'health', Sprite> { hp = 100; hurt(n: number) { this.hp -= n } }
|
|
12
|
+
// sprite.aspect(Health, { hp: 80 }).health.hurt(10)
|
|
13
|
+
//
|
|
14
|
+
// The accessor name lives in the generic (Aspect<'name', Node>). The `chisel` bundler extracts it at
|
|
15
|
+
// compile time and injects the runtime `static aspect` on user aspects + the virtual type accessor;
|
|
16
|
+
// the SDK's own built-in aspects declare `static readonly aspect` directly (see SpriteAnimation etc.).
|
|
17
|
+
|
|
18
|
+
import { Emitter, type EventMap } from "./events"
|
|
19
|
+
import type { Node } from "../gl/Node"
|
|
20
|
+
|
|
21
|
+
// ---- type helpers -----------------------------------------------------------
|
|
22
|
+
/** The class object of an aspect — accepted even when its constructor is protected/abstract. */
|
|
23
|
+
export type AspectCtor<A extends Aspect<any, any>> = abstract new () => A
|
|
24
|
+
/** The accessor name carried in the aspect's generic. */
|
|
25
|
+
export type KeyOf<A> = A extends Aspect<infer K, any> ? K : never
|
|
26
|
+
/** The node kind an aspect targets. */
|
|
27
|
+
export type TargetOf<A> = A extends Aspect<any, infer P> ? P : never
|
|
28
|
+
/** `{ name: Aspect }` — the field an aspect contributes to its node. */
|
|
29
|
+
export type FieldOf<A extends Aspect<any, any>> = { [P in KeyOf<A>]: A }
|
|
30
|
+
|
|
31
|
+
type UnionToIntersection<U> =
|
|
32
|
+
(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A variable typed as a node KNOWN to carry the given aspect(s). Union, not a tuple — reads as
|
|
36
|
+
* English and mirrors the runtime guard `node.has(Health)`:
|
|
37
|
+
* let boss: With<Sprite, Health | Physics>
|
|
38
|
+
*/
|
|
39
|
+
export type With<N, A extends Aspect<any, any>> =
|
|
40
|
+
N & UnionToIntersection<A extends any ? FieldOf<A> : never>
|
|
41
|
+
|
|
42
|
+
// ---- per-frame update dispatch ----------------------------------------------
|
|
43
|
+
// Aspects that define update(dt) are ticked every frame from a SINGLE global loop, created LAZILY on
|
|
44
|
+
// the first such aspect (a project with no behavior aspects starts no loop). dt is seconds, like
|
|
45
|
+
// setLoop. No visibility/frustum culling yet — every registered aspect ticks regardless of on-screen
|
|
46
|
+
// state.
|
|
47
|
+
const updaters: Aspect<any, any>[] = []
|
|
48
|
+
let updateLoopId: number | undefined
|
|
49
|
+
|
|
50
|
+
const ensureUpdateLoop = (): void => {
|
|
51
|
+
if (updateLoopId !== undefined) return
|
|
52
|
+
updateLoopId = setLoop((dt) => {
|
|
53
|
+
// iterate a snapshot so an update() that attaches/detaches aspects can't corrupt this frame's pass
|
|
54
|
+
for (const a of updaters.slice()) {
|
|
55
|
+
// later (visibility culling): if (a.updateWhenVisible && nodeOffScreen(a)) continue
|
|
56
|
+
a.update?.(dt)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
const registerUpdater = (a: Aspect<any, any>): void => { updaters.push(a); ensureUpdateLoop() }
|
|
61
|
+
const unregisterUpdater = (a: Aspect<any, any>): void => {
|
|
62
|
+
const i = updaters.indexOf(a)
|
|
63
|
+
if (i >= 0) updaters.splice(i, 1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- the host: mixed into every node kind (Node2D, Node) --------------------
|
|
67
|
+
/**
|
|
68
|
+
* Base for anything that can carry aspects. Extends Emitter, so node kinds get events too. Provides
|
|
69
|
+
* the four verbs; the named accessor (node.physics, node.health, …) is set as an own property at
|
|
70
|
+
* attach time, keyed by the aspect class's runtime `aspect` name.
|
|
71
|
+
*/
|
|
72
|
+
export abstract class AspectHost<E extends EventMap = EventMap> extends Emitter<E> {
|
|
73
|
+
/** @internal class → instance, the authoritative store (named accessors mirror this). */
|
|
74
|
+
readonly _aspects = new Map<Function, Aspect<any, any>>()
|
|
75
|
+
|
|
76
|
+
/** Attach (and configure) an aspect, or reconfigure it if already present. Returns the node typed
|
|
77
|
+
* as now-having that aspect (so no guard/`?.` is needed afterwards). Rejects a wrong target node. */
|
|
78
|
+
aspect<Self extends TargetOf<A>, A extends Aspect<any, any>>(
|
|
79
|
+
this: Self,
|
|
80
|
+
ctor: AspectCtor<A>,
|
|
81
|
+
opts?: Partial<A>,
|
|
82
|
+
): Self & FieldOf<A> {
|
|
83
|
+
const host = this as unknown as AspectHost
|
|
84
|
+
let inst = host._aspects.get(ctor) as A | undefined
|
|
85
|
+
const fresh = inst === undefined
|
|
86
|
+
if (!inst) {
|
|
87
|
+
inst = new (ctor as unknown as new () => A)()
|
|
88
|
+
;(inst as { node: unknown }).node = this
|
|
89
|
+
host._aspects.set(ctor, inst)
|
|
90
|
+
const name = (ctor as { aspect?: string }).aspect
|
|
91
|
+
if (name) (host as unknown as Record<string, unknown>)[name] = inst
|
|
92
|
+
}
|
|
93
|
+
if (opts) Object.assign(inst, opts)
|
|
94
|
+
if (fresh) {
|
|
95
|
+
inst.onAttach?.()
|
|
96
|
+
if (typeof inst.update === "function") registerUpdater(inst)
|
|
97
|
+
}
|
|
98
|
+
return this as Self & FieldOf<A>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Safe access — undefined if the aspect isn't attached. */
|
|
102
|
+
get<A extends Aspect<any, any>>(ctor: AspectCtor<A>): A | undefined {
|
|
103
|
+
return this._aspects.get(ctor) as A | undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Existence check AND type guard: inside `if (node.has(Physics))`, `node.physics` is present. */
|
|
107
|
+
has<A extends Aspect<any, any>>(ctor: AspectCtor<A>): this is this & FieldOf<A> {
|
|
108
|
+
return this._aspects.has(ctor)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Detach an aspect (runs its onDetach). Named `removeAspect` so it never collides with the
|
|
112
|
+
* child-management `add`/`remove` semantics some node kinds expose. */
|
|
113
|
+
removeAspect<A extends Aspect<any, any>>(ctor: AspectCtor<A>): this {
|
|
114
|
+
const inst = this._aspects.get(ctor)
|
|
115
|
+
if (inst) {
|
|
116
|
+
inst.onDetach?.()
|
|
117
|
+
unregisterUpdater(inst)
|
|
118
|
+
this._aspects.delete(ctor)
|
|
119
|
+
const name = (ctor as { aspect?: string }).aspect
|
|
120
|
+
if (name) delete (this as unknown as Record<string, unknown>)[name]
|
|
121
|
+
}
|
|
122
|
+
return this
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- the aspect base --------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* Base for everything you attach. `K` = accessor name (extracted by chisel for the runtime). `P` =
|
|
129
|
+
* the target node kind: `this.node` is typed to it, AND attaching to a wrong node is a compile error.
|
|
130
|
+
* Initialize in `onAttach` (the node is set by then) — not a constructor; aspects are created by the
|
|
131
|
+
* engine via `node.aspect()`, never `new`.
|
|
132
|
+
*/
|
|
133
|
+
export abstract class Aspect<K extends string, P = Node> {
|
|
134
|
+
// NOTE: P has no `extends AspectHost` constraint on purpose — Emitter<E>'s generic
|
|
135
|
+
// addEventListener makes a specific-event node (AspectHost<NodeEvents>) not assignable to
|
|
136
|
+
// AspectHost<any>, which would reject every real node. P only types `this.node`; target
|
|
137
|
+
// enforcement happens in aspect() via `this: Self extends TargetOf<A>`. Defaults to Node
|
|
138
|
+
// (3D is the default engine) — a 2D aspect declares its own P, e.g. Aspect<'x', Sprite>.
|
|
139
|
+
/** The node this aspect is attached to (set at attach time). */
|
|
140
|
+
readonly node!: P
|
|
141
|
+
/** @internal phantom — lets the type system recover the accessor name `K`. Never read at runtime. */
|
|
142
|
+
declare protected readonly __key__?: K
|
|
143
|
+
onAttach?(): void
|
|
144
|
+
onDetach?(): void
|
|
145
|
+
/** Called every frame while attached (dt = seconds since the last frame), via a lazy global loop. */
|
|
146
|
+
update?(dt: number): void
|
|
147
|
+
/** Opt-in: only run update(dt) while the node is on-screen. Lives on the aspect (not the node) so
|
|
148
|
+
* aspects on the same node can choose independently. NOOP for now — visibility culling isn't wired
|
|
149
|
+
* yet, so every updater ticks regardless; declared so aspects can opt in ahead of it. */
|
|
150
|
+
updateWhenVisible = false
|
|
151
|
+
}
|