@flare-engine/math 0.2.1 → 0.3.1
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/LICENSE +201 -21
- package/NOTICE +2 -0
- package/README.md +4 -4
- package/dist/index.d.mts +263 -245
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +263 -245
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1206 -1075
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1128 -971
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -8
package/dist/index.mjs
CHANGED
|
@@ -1,1094 +1,1251 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/circle.ts
|
|
2
|
+
const Circle = {
|
|
3
|
+
/** Returns true if point is inside or on the circle. */
|
|
4
|
+
contains(c, p) {
|
|
5
|
+
const dx = p.x - c.x;
|
|
6
|
+
const dy = p.y - c.y;
|
|
7
|
+
return dx * dx + dy * dy <= c.radius * c.radius;
|
|
8
|
+
},
|
|
9
|
+
/** Returns true if two circles overlap (share area). */
|
|
10
|
+
intersects(a, b) {
|
|
11
|
+
const dx = b.x - a.x;
|
|
12
|
+
const dy = b.y - a.y;
|
|
13
|
+
const distSq = dx * dx + dy * dy;
|
|
14
|
+
const radSum = a.radius + b.radius;
|
|
15
|
+
return distSq < radSum * radSum;
|
|
16
|
+
},
|
|
17
|
+
/** Returns true if a circle and rect overlap. */
|
|
18
|
+
intersectsRect(c, r) {
|
|
19
|
+
const closestX = c.x < r.x ? r.x : c.x > r.x + r.width ? r.x + r.width : c.x;
|
|
20
|
+
const closestY = c.y < r.y ? r.y : c.y > r.y + r.height ? r.y + r.height : c.y;
|
|
21
|
+
const dx = c.x - closestX;
|
|
22
|
+
const dy = c.y - closestY;
|
|
23
|
+
return dx * dx + dy * dy <= c.radius * c.radius;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/scalar.ts
|
|
28
|
+
/** Clamps value between min and max. */
|
|
2
29
|
function clamp(value, min, max) {
|
|
3
|
-
|
|
30
|
+
return value < min ? min : value > max ? max : value;
|
|
4
31
|
}
|
|
32
|
+
/** Linearly interpolates between a and b by t (unclamped). */
|
|
5
33
|
function lerp(a, b, t) {
|
|
6
|
-
|
|
34
|
+
return a + (b - a) * t;
|
|
7
35
|
}
|
|
36
|
+
/** Returns the interpolation factor of value between a and b. */
|
|
8
37
|
function inverseLerp(a, b, value) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
38
|
+
const range = b - a;
|
|
39
|
+
if (range === 0) return 0;
|
|
40
|
+
return (value - a) / range;
|
|
12
41
|
}
|
|
42
|
+
/** Maps value from [inMin, inMax] to [outMin, outMax]. */
|
|
13
43
|
function map(value, inMin, inMax, outMin, outMax) {
|
|
14
|
-
|
|
44
|
+
return lerp(outMin, outMax, inverseLerp(inMin, inMax, value));
|
|
15
45
|
}
|
|
16
|
-
|
|
17
|
-
|
|
46
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
47
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
48
|
+
/** Converts degrees to radians. */
|
|
18
49
|
function degToRad(degrees) {
|
|
19
|
-
|
|
50
|
+
return degrees * DEG_TO_RAD;
|
|
20
51
|
}
|
|
52
|
+
/** Converts radians to degrees. */
|
|
21
53
|
function radToDeg(radians) {
|
|
22
|
-
|
|
54
|
+
return radians * RAD_TO_DEG;
|
|
23
55
|
}
|
|
56
|
+
/** Returns true if a and b are approximately equal within epsilon. */
|
|
24
57
|
function approxEquals(a, b, epsilon = 1e-6) {
|
|
25
|
-
|
|
58
|
+
return Math.abs(a - b) <= epsilon;
|
|
26
59
|
}
|
|
60
|
+
/** Wraps value into [min, max) range. */
|
|
27
61
|
function wrap(value, min, max) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
62
|
+
const range = max - min;
|
|
63
|
+
if (range === 0) return min;
|
|
64
|
+
return min + ((value - min) % range + range) % range;
|
|
31
65
|
}
|
|
66
|
+
/** PingPong — bounces value back and forth in [0, length]. */
|
|
32
67
|
function pingPong(t, length) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
68
|
+
if (length === 0) return 0;
|
|
69
|
+
const tt = wrap(t, 0, length * 2);
|
|
70
|
+
return length - Math.abs(tt - length);
|
|
36
71
|
}
|
|
72
|
+
/** Hermite smooth interpolation (0→1) with zero first-derivative at edges. */
|
|
37
73
|
function smoothStep(edge0, edge1, x) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
74
|
+
const range = edge1 - edge0;
|
|
75
|
+
if (range === 0) return x >= edge1 ? 1 : 0;
|
|
76
|
+
const t = clamp((x - edge0) / range, 0, 1);
|
|
77
|
+
return t * t * (3 - 2 * t);
|
|
42
78
|
}
|
|
79
|
+
/** Returns -1, 0, or 1. Unlike Math.sign, returns 0 for NaN. */
|
|
43
80
|
function sign(value) {
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
if (Number.isNaN(value)) return 0;
|
|
82
|
+
return value > 0 ? 1 : value < 0 ? -1 : 0;
|
|
46
83
|
}
|
|
84
|
+
/** Shortest signed angle delta a - b, wrapped to [-pi, pi] (closed both ends — strict comparisons
|
|
85
|
+
* leave exact +/-pi unwrapped). Branch form (single +/-2pi correction)
|
|
86
|
+
* — byte-identical to hand-rolled aim/homing turn math; do NOT swap for a modulo form (it shifts
|
|
87
|
+
* results by ULPs at the +/-pi boundary). */
|
|
47
88
|
function angleDelta(a, b) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
89
|
+
let diff = a - b;
|
|
90
|
+
if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
91
|
+
else if (diff < -Math.PI) diff += 2 * Math.PI;
|
|
92
|
+
return diff;
|
|
52
93
|
}
|
|
94
|
+
/** Rotates angle cur toward target by at most maxStep radians, shortest direction. Returns the new
|
|
95
|
+
* angle (NOT re-wrapped). Branch-form step clamp — byte-identical to hand-rolled homing steering. */
|
|
53
96
|
function rotateTowards(cur, target, maxStep) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/** Returns { 0, 0 }. */
|
|
65
|
-
zero() {
|
|
66
|
-
return { x: 0, y: 0 };
|
|
67
|
-
},
|
|
68
|
-
/** Returns { 1, 1 }. */
|
|
69
|
-
one() {
|
|
70
|
-
return { x: 1, y: 1 };
|
|
71
|
-
},
|
|
72
|
-
/** Returns { 0, -1 }. */
|
|
73
|
-
up() {
|
|
74
|
-
return { x: 0, y: -1 };
|
|
75
|
-
},
|
|
76
|
-
/** Returns { 0, 1 }. */
|
|
77
|
-
down() {
|
|
78
|
-
return { x: 0, y: 1 };
|
|
79
|
-
},
|
|
80
|
-
/** Returns { -1, 0 }. */
|
|
81
|
-
left() {
|
|
82
|
-
return { x: -1, y: 0 };
|
|
83
|
-
},
|
|
84
|
-
/** Returns { 1, 0 }. */
|
|
85
|
-
right() {
|
|
86
|
-
return { x: 1, y: 0 };
|
|
87
|
-
},
|
|
88
|
-
/** Component-wise addition. Writes into out if provided. */
|
|
89
|
-
add(a, b, out) {
|
|
90
|
-
if (out) {
|
|
91
|
-
out.x = a.x + b.x;
|
|
92
|
-
out.y = a.y + b.y;
|
|
93
|
-
return out;
|
|
94
|
-
}
|
|
95
|
-
return { x: a.x + b.x, y: a.y + b.y };
|
|
96
|
-
},
|
|
97
|
-
/** Component-wise subtraction a - b. */
|
|
98
|
-
sub(a, b, out) {
|
|
99
|
-
if (out) {
|
|
100
|
-
out.x = a.x - b.x;
|
|
101
|
-
out.y = a.y - b.y;
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
return { x: a.x - b.x, y: a.y - b.y };
|
|
105
|
-
},
|
|
106
|
-
/** Scales a vector by a scalar. */
|
|
107
|
-
scale(v, s, out) {
|
|
108
|
-
if (out) {
|
|
109
|
-
out.x = v.x * s;
|
|
110
|
-
out.y = v.y * s;
|
|
111
|
-
return out;
|
|
112
|
-
}
|
|
113
|
-
return { x: v.x * s, y: v.y * s };
|
|
114
|
-
},
|
|
115
|
-
/** Returns a unit vector. Returns zero vector if length is 0. */
|
|
116
|
-
normalize(v, out) {
|
|
117
|
-
const len = Math.sqrt(v.x * v.x + v.y * v.y);
|
|
118
|
-
if (len === 0) {
|
|
119
|
-
if (out) {
|
|
120
|
-
out.x = 0;
|
|
121
|
-
out.y = 0;
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
return { x: 0, y: 0 };
|
|
125
|
-
}
|
|
126
|
-
const inv = 1 / len;
|
|
127
|
-
if (out) {
|
|
128
|
-
out.x = v.x * inv;
|
|
129
|
-
out.y = v.y * inv;
|
|
130
|
-
return out;
|
|
131
|
-
}
|
|
132
|
-
return { x: v.x * inv, y: v.y * inv };
|
|
133
|
-
},
|
|
134
|
-
/** Zero-alloc; writes into `out`. Mandatory `out` so callers avoid temporaries in hot paths. */
|
|
135
|
-
normalizeOut(dx, dy, speed, out) {
|
|
136
|
-
const len = Math.sqrt(dx * dx + dy * dy);
|
|
137
|
-
if (len <= 1e-6) {
|
|
138
|
-
out.x = 0;
|
|
139
|
-
out.y = 0;
|
|
140
|
-
return out;
|
|
141
|
-
}
|
|
142
|
-
const inv = speed / len;
|
|
143
|
-
out.x = dx * inv;
|
|
144
|
-
out.y = dy * inv;
|
|
145
|
-
return out;
|
|
146
|
-
},
|
|
147
|
-
/** Dot product. */
|
|
148
|
-
dot(a, b) {
|
|
149
|
-
return a.x * b.x + a.y * b.y;
|
|
150
|
-
},
|
|
151
|
-
/** 2D cross product (z-component of the 3D cross). */
|
|
152
|
-
cross(a, b) {
|
|
153
|
-
return a.x * b.y - a.y * b.x;
|
|
154
|
-
},
|
|
155
|
-
/** Linearly interpolates between a and b (unclamped). */
|
|
156
|
-
lerp(a, b, t, out) {
|
|
157
|
-
const x = a.x + (b.x - a.x) * t;
|
|
158
|
-
const y = a.y + (b.y - a.y) * t;
|
|
159
|
-
if (out) {
|
|
160
|
-
out.x = x;
|
|
161
|
-
out.y = y;
|
|
162
|
-
return out;
|
|
163
|
-
}
|
|
164
|
-
return { x, y };
|
|
165
|
-
},
|
|
166
|
-
/** Spherical linear interpolation for unit vectors. */
|
|
167
|
-
slerp(a, b, t, out) {
|
|
168
|
-
let d = a.x * b.x + a.y * b.y;
|
|
169
|
-
d = d < -1 ? -1 : d > 1 ? 1 : d;
|
|
170
|
-
const theta = Math.acos(d);
|
|
171
|
-
if (theta < 1e-6) {
|
|
172
|
-
return Vec2.lerp(a, b, t, out);
|
|
173
|
-
}
|
|
174
|
-
const sinTheta = Math.sin(theta);
|
|
175
|
-
const wa = Math.sin((1 - t) * theta) / sinTheta;
|
|
176
|
-
const wb = Math.sin(t * theta) / sinTheta;
|
|
177
|
-
const x = a.x * wa + b.x * wb;
|
|
178
|
-
const y = a.y * wa + b.y * wb;
|
|
179
|
-
if (out) {
|
|
180
|
-
out.x = x;
|
|
181
|
-
out.y = y;
|
|
182
|
-
return out;
|
|
183
|
-
}
|
|
184
|
-
return { x, y };
|
|
185
|
-
},
|
|
186
|
-
/** Euclidean distance between two points. */
|
|
187
|
-
distance(a, b) {
|
|
188
|
-
const dx = b.x - a.x;
|
|
189
|
-
const dy = b.y - a.y;
|
|
190
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
191
|
-
},
|
|
192
|
-
/** Squared distance (avoids sqrt). */
|
|
193
|
-
distanceSq(a, b) {
|
|
194
|
-
const dx = b.x - a.x;
|
|
195
|
-
const dy = b.y - a.y;
|
|
196
|
-
return dx * dx + dy * dy;
|
|
197
|
-
},
|
|
198
|
-
/** Length of a vector. */
|
|
199
|
-
length(v) {
|
|
200
|
-
return Math.sqrt(v.x * v.x + v.y * v.y);
|
|
201
|
-
},
|
|
202
|
-
/** Squared length (avoids sqrt). */
|
|
203
|
-
lengthSq(v) {
|
|
204
|
-
return v.x * v.x + v.y * v.y;
|
|
205
|
-
},
|
|
206
|
-
/** Angle of vector in radians (atan2). */
|
|
207
|
-
angle(v) {
|
|
208
|
-
return Math.atan2(v.y, v.x);
|
|
209
|
-
},
|
|
210
|
-
/** Rotates a vector by angle (radians) around origin. */
|
|
211
|
-
rotate(v, angle, out) {
|
|
212
|
-
const c = Math.cos(angle);
|
|
213
|
-
const s = Math.sin(angle);
|
|
214
|
-
const x = v.x * c - v.y * s;
|
|
215
|
-
const y = v.x * s + v.y * c;
|
|
216
|
-
if (out) {
|
|
217
|
-
out.x = x;
|
|
218
|
-
out.y = y;
|
|
219
|
-
return out;
|
|
220
|
-
}
|
|
221
|
-
return { x, y };
|
|
222
|
-
},
|
|
223
|
-
/** Component-wise approximate equality. */
|
|
224
|
-
equals(a, b, epsilon = 1e-6) {
|
|
225
|
-
return approxEquals(a.x, b.x, epsilon) && approxEquals(a.y, b.y, epsilon);
|
|
226
|
-
},
|
|
227
|
-
/** Returns a copy of the vector. */
|
|
228
|
-
clone(v) {
|
|
229
|
-
return { x: v.x, y: v.y };
|
|
230
|
-
},
|
|
231
|
-
/** Clamps the length of a vector to maxLength. */
|
|
232
|
-
clampLength(v, maxLength, out) {
|
|
233
|
-
const lenSq = v.x * v.x + v.y * v.y;
|
|
234
|
-
if (lenSq <= maxLength * maxLength) {
|
|
235
|
-
if (out) {
|
|
236
|
-
out.x = v.x;
|
|
237
|
-
out.y = v.y;
|
|
238
|
-
return out;
|
|
239
|
-
}
|
|
240
|
-
return { x: v.x, y: v.y };
|
|
241
|
-
}
|
|
242
|
-
const len = Math.sqrt(lenSq);
|
|
243
|
-
const inv = maxLength / len;
|
|
244
|
-
if (out) {
|
|
245
|
-
out.x = v.x * inv;
|
|
246
|
-
out.y = v.y * inv;
|
|
247
|
-
return out;
|
|
248
|
-
}
|
|
249
|
-
return { x: v.x * inv, y: v.y * inv };
|
|
250
|
-
},
|
|
251
|
-
/** Zero-alloc homing steer: rotate velocity (vx,vy) toward direction (dx,dy) by at most maxStep
|
|
252
|
-
* radians, preserving speed; writes into out. Mandatory out. Byte-identical to hand-rolled
|
|
253
|
-
* homing-bullet steering (Math.hypot speed + atan2 + branch-form turn + cos/sin * speed). */
|
|
254
|
-
turnVelocityToward(vx, vy, dx, dy, maxStep, out) {
|
|
255
|
-
const speed = Math.hypot(vx, vy);
|
|
256
|
-
const newAng = rotateTowards(Math.atan2(vy, vx), Math.atan2(dy, dx), maxStep);
|
|
257
|
-
out.x = Math.cos(newAng) * speed;
|
|
258
|
-
out.y = Math.sin(newAng) * speed;
|
|
259
|
-
return out;
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// src/rect.ts
|
|
264
|
-
var Rect = {
|
|
265
|
-
/** Returns true if point is inside rect (inclusive). */
|
|
266
|
-
contains(r, p) {
|
|
267
|
-
return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
|
|
268
|
-
},
|
|
269
|
-
/** Returns true if two rects overlap (share area). */
|
|
270
|
-
intersects(a, b) {
|
|
271
|
-
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
272
|
-
},
|
|
273
|
-
/** Returns the overlapping rect, or null if no overlap. */
|
|
274
|
-
intersection(a, b) {
|
|
275
|
-
const x = Math.max(a.x, b.x);
|
|
276
|
-
const y = Math.max(a.y, b.y);
|
|
277
|
-
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
278
|
-
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
279
|
-
if (right <= x || bottom <= y) return null;
|
|
280
|
-
return { x, y, width: right - x, height: bottom - y };
|
|
281
|
-
},
|
|
282
|
-
/** Returns the smallest rect containing both rects. */
|
|
283
|
-
union(a, b) {
|
|
284
|
-
const x = Math.min(a.x, b.x);
|
|
285
|
-
const y = Math.min(a.y, b.y);
|
|
286
|
-
const right = Math.max(a.x + a.width, b.x + b.width);
|
|
287
|
-
const bottom = Math.max(a.y + a.height, b.y + b.height);
|
|
288
|
-
return { x, y, width: right - x, height: bottom - y };
|
|
289
|
-
},
|
|
290
|
-
/** Creates a rect from an array of points (bounding box). */
|
|
291
|
-
fromPoints(points) {
|
|
292
|
-
if (points.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
|
|
293
|
-
let minX = points[0].x;
|
|
294
|
-
let minY = points[0].y;
|
|
295
|
-
let maxX = minX;
|
|
296
|
-
let maxY = minY;
|
|
297
|
-
for (let i = 1; i < points.length; i++) {
|
|
298
|
-
const p = points[i];
|
|
299
|
-
if (p.x < minX) minX = p.x;
|
|
300
|
-
if (p.y < minY) minY = p.y;
|
|
301
|
-
if (p.x > maxX) maxX = p.x;
|
|
302
|
-
if (p.y > maxY) maxY = p.y;
|
|
303
|
-
}
|
|
304
|
-
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
305
|
-
},
|
|
306
|
-
/** Returns the center point of the rect. */
|
|
307
|
-
center(r) {
|
|
308
|
-
return { x: r.x + r.width * 0.5, y: r.y + r.height * 0.5 };
|
|
309
|
-
},
|
|
310
|
-
/** Returns a new rect expanded by amount on each side. */
|
|
311
|
-
expand(r, amount) {
|
|
312
|
-
return {
|
|
313
|
-
x: r.x - amount,
|
|
314
|
-
y: r.y - amount,
|
|
315
|
-
width: r.width + amount * 2,
|
|
316
|
-
height: r.height + amount * 2
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
// src/circle.ts
|
|
322
|
-
var Circle = {
|
|
323
|
-
/** Returns true if point is inside or on the circle. */
|
|
324
|
-
contains(c, p) {
|
|
325
|
-
const dx = p.x - c.x;
|
|
326
|
-
const dy = p.y - c.y;
|
|
327
|
-
return dx * dx + dy * dy <= c.radius * c.radius;
|
|
328
|
-
},
|
|
329
|
-
/** Returns true if two circles overlap (share area). */
|
|
330
|
-
intersects(a, b) {
|
|
331
|
-
const dx = b.x - a.x;
|
|
332
|
-
const dy = b.y - a.y;
|
|
333
|
-
const distSq = dx * dx + dy * dy;
|
|
334
|
-
const radSum = a.radius + b.radius;
|
|
335
|
-
return distSq < radSum * radSum;
|
|
336
|
-
},
|
|
337
|
-
/** Returns true if a circle and rect overlap. */
|
|
338
|
-
intersectsRect(c, r) {
|
|
339
|
-
const closestX = c.x < r.x ? r.x : c.x > r.x + r.width ? r.x + r.width : c.x;
|
|
340
|
-
const closestY = c.y < r.y ? r.y : c.y > r.y + r.height ? r.y + r.height : c.y;
|
|
341
|
-
const dx = c.x - closestX;
|
|
342
|
-
const dy = c.y - closestY;
|
|
343
|
-
return dx * dx + dy * dy <= c.radius * c.radius;
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
// src/transform2d.ts
|
|
348
|
-
var Transform2D = {
|
|
349
|
-
/** Creates an identity transform. */
|
|
350
|
-
identity() {
|
|
351
|
-
return { position: { x: 0, y: 0 }, rotation: 0, scale: { x: 1, y: 1 } };
|
|
352
|
-
},
|
|
353
|
-
/** Creates a transform from components. */
|
|
354
|
-
create(x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1) {
|
|
355
|
-
return {
|
|
356
|
-
position: { x, y },
|
|
357
|
-
rotation,
|
|
358
|
-
scale: { x: scaleX, y: scaleY }
|
|
359
|
-
};
|
|
360
|
-
},
|
|
361
|
-
/** Transforms a local-space point to world-space. */
|
|
362
|
-
localToWorld(t, point) {
|
|
363
|
-
const c = Math.cos(t.rotation);
|
|
364
|
-
const s = Math.sin(t.rotation);
|
|
365
|
-
const sx = point.x * t.scale.x;
|
|
366
|
-
const sy = point.y * t.scale.y;
|
|
367
|
-
return {
|
|
368
|
-
x: sx * c - sy * s + t.position.x,
|
|
369
|
-
y: sx * s + sy * c + t.position.y
|
|
370
|
-
};
|
|
371
|
-
},
|
|
372
|
-
/** Transforms a world-space point to local-space. */
|
|
373
|
-
worldToLocal(t, point) {
|
|
374
|
-
const dx = point.x - t.position.x;
|
|
375
|
-
const dy = point.y - t.position.y;
|
|
376
|
-
const c = Math.cos(-t.rotation);
|
|
377
|
-
const s = Math.sin(-t.rotation);
|
|
378
|
-
const rx = dx * c - dy * s;
|
|
379
|
-
const ry = dx * s + dy * c;
|
|
380
|
-
return {
|
|
381
|
-
x: t.scale.x !== 0 ? rx / t.scale.x : 0,
|
|
382
|
-
y: t.scale.y !== 0 ? ry / t.scale.y : 0
|
|
383
|
-
};
|
|
384
|
-
},
|
|
385
|
-
/** Returns a copy of the transform. */
|
|
386
|
-
clone(t) {
|
|
387
|
-
return {
|
|
388
|
-
position: { x: t.position.x, y: t.position.y },
|
|
389
|
-
rotation: t.rotation,
|
|
390
|
-
scale: { x: t.scale.x, y: t.scale.y }
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
// src/mat3.ts
|
|
396
|
-
var Mat3 = {
|
|
397
|
-
/** Creates a new identity matrix. */
|
|
398
|
-
create() {
|
|
399
|
-
const m = new Float32Array(9);
|
|
400
|
-
m[0] = 1;
|
|
401
|
-
m[4] = 1;
|
|
402
|
-
m[8] = 1;
|
|
403
|
-
return m;
|
|
404
|
-
},
|
|
405
|
-
/** Sets m to identity. */
|
|
406
|
-
identity(m) {
|
|
407
|
-
m[0] = 1;
|
|
408
|
-
m[1] = 0;
|
|
409
|
-
m[2] = 0;
|
|
410
|
-
m[3] = 0;
|
|
411
|
-
m[4] = 1;
|
|
412
|
-
m[5] = 0;
|
|
413
|
-
m[6] = 0;
|
|
414
|
-
m[7] = 0;
|
|
415
|
-
m[8] = 1;
|
|
416
|
-
return m;
|
|
417
|
-
},
|
|
418
|
-
/** Multiplies a * b, writes into out (or allocates). */
|
|
419
|
-
multiply(a, b, out) {
|
|
420
|
-
const a00 = a[0], a01 = a[3], a02 = a[6];
|
|
421
|
-
const a10 = a[1], a11 = a[4], a12 = a[7];
|
|
422
|
-
const a20 = a[2], a21 = a[5], a22 = a[8];
|
|
423
|
-
const b00 = b[0], b01 = b[3], b02 = b[6];
|
|
424
|
-
const b10 = b[1], b11 = b[4], b12 = b[7];
|
|
425
|
-
const b20 = b[2], b21 = b[5], b22 = b[8];
|
|
426
|
-
const o = out ?? new Float32Array(9);
|
|
427
|
-
o[0] = a00 * b00 + a01 * b10 + a02 * b20;
|
|
428
|
-
o[1] = a10 * b00 + a11 * b10 + a12 * b20;
|
|
429
|
-
o[2] = a20 * b00 + a21 * b10 + a22 * b20;
|
|
430
|
-
o[3] = a00 * b01 + a01 * b11 + a02 * b21;
|
|
431
|
-
o[4] = a10 * b01 + a11 * b11 + a12 * b21;
|
|
432
|
-
o[5] = a20 * b01 + a21 * b11 + a22 * b21;
|
|
433
|
-
o[6] = a00 * b02 + a01 * b12 + a02 * b22;
|
|
434
|
-
o[7] = a10 * b02 + a11 * b12 + a12 * b22;
|
|
435
|
-
o[8] = a20 * b02 + a21 * b12 + a22 * b22;
|
|
436
|
-
return o;
|
|
437
|
-
},
|
|
438
|
-
/** Creates a translation matrix. */
|
|
439
|
-
translate(tx, ty, out) {
|
|
440
|
-
const m = out ?? new Float32Array(9);
|
|
441
|
-
m[0] = 1;
|
|
442
|
-
m[1] = 0;
|
|
443
|
-
m[2] = 0;
|
|
444
|
-
m[3] = 0;
|
|
445
|
-
m[4] = 1;
|
|
446
|
-
m[5] = 0;
|
|
447
|
-
m[6] = tx;
|
|
448
|
-
m[7] = ty;
|
|
449
|
-
m[8] = 1;
|
|
450
|
-
return m;
|
|
451
|
-
},
|
|
452
|
-
/** Creates a rotation matrix (radians). */
|
|
453
|
-
rotate(angle, out) {
|
|
454
|
-
const c = Math.cos(angle);
|
|
455
|
-
const s = Math.sin(angle);
|
|
456
|
-
const m = out ?? new Float32Array(9);
|
|
457
|
-
m[0] = c;
|
|
458
|
-
m[1] = s;
|
|
459
|
-
m[2] = 0;
|
|
460
|
-
m[3] = -s;
|
|
461
|
-
m[4] = c;
|
|
462
|
-
m[5] = 0;
|
|
463
|
-
m[6] = 0;
|
|
464
|
-
m[7] = 0;
|
|
465
|
-
m[8] = 1;
|
|
466
|
-
return m;
|
|
467
|
-
},
|
|
468
|
-
/** Creates a scale matrix. */
|
|
469
|
-
scale(sx, sy, out) {
|
|
470
|
-
const m = out ?? new Float32Array(9);
|
|
471
|
-
m[0] = sx;
|
|
472
|
-
m[1] = 0;
|
|
473
|
-
m[2] = 0;
|
|
474
|
-
m[3] = 0;
|
|
475
|
-
m[4] = sy;
|
|
476
|
-
m[5] = 0;
|
|
477
|
-
m[6] = 0;
|
|
478
|
-
m[7] = 0;
|
|
479
|
-
m[8] = 1;
|
|
480
|
-
return m;
|
|
481
|
-
},
|
|
482
|
-
/** Inverts the matrix. Returns null if singular. */
|
|
483
|
-
invert(m, out) {
|
|
484
|
-
const a00 = m[0], a01 = m[3], a02 = m[6];
|
|
485
|
-
const a10 = m[1], a11 = m[4], a12 = m[7];
|
|
486
|
-
const a20 = m[2], a21 = m[5], a22 = m[8];
|
|
487
|
-
const det = a00 * (a11 * a22 - a12 * a21) - a01 * (a10 * a22 - a12 * a20) + a02 * (a10 * a21 - a11 * a20);
|
|
488
|
-
if (Math.abs(det) < 1e-8) return null;
|
|
489
|
-
const invDet = 1 / det;
|
|
490
|
-
const o = out ?? new Float32Array(9);
|
|
491
|
-
o[0] = (a11 * a22 - a12 * a21) * invDet;
|
|
492
|
-
o[1] = (a12 * a20 - a10 * a22) * invDet;
|
|
493
|
-
o[2] = (a10 * a21 - a11 * a20) * invDet;
|
|
494
|
-
o[3] = (a02 * a21 - a01 * a22) * invDet;
|
|
495
|
-
o[4] = (a00 * a22 - a02 * a20) * invDet;
|
|
496
|
-
o[5] = (a01 * a20 - a00 * a21) * invDet;
|
|
497
|
-
o[6] = (a01 * a12 - a02 * a11) * invDet;
|
|
498
|
-
o[7] = (a02 * a10 - a00 * a12) * invDet;
|
|
499
|
-
o[8] = (a00 * a11 - a01 * a10) * invDet;
|
|
500
|
-
return o;
|
|
501
|
-
},
|
|
502
|
-
/** Builds a matrix from a Transform2D (TRS order: translate * rotate * scale). */
|
|
503
|
-
fromTransform(t, out) {
|
|
504
|
-
const c = Math.cos(t.rotation);
|
|
505
|
-
const s = Math.sin(t.rotation);
|
|
506
|
-
const sx = t.scale.x;
|
|
507
|
-
const sy = t.scale.y;
|
|
508
|
-
const m = out ?? new Float32Array(9);
|
|
509
|
-
m[0] = c * sx;
|
|
510
|
-
m[1] = s * sx;
|
|
511
|
-
m[2] = 0;
|
|
512
|
-
m[3] = -s * sy;
|
|
513
|
-
m[4] = c * sy;
|
|
514
|
-
m[5] = 0;
|
|
515
|
-
m[6] = t.position.x;
|
|
516
|
-
m[7] = t.position.y;
|
|
517
|
-
m[8] = 1;
|
|
518
|
-
return m;
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
// src/color.ts
|
|
97
|
+
const diff = angleDelta(target, cur);
|
|
98
|
+
return cur + (diff > maxStep ? maxStep : diff < -maxStep ? -maxStep : diff);
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/color.ts
|
|
102
|
+
/**
|
|
103
|
+
* Converts a single sRGB channel to linear light.
|
|
104
|
+
* The 0.04045 threshold (modern WCAG errata) is numerically identical to the
|
|
105
|
+
* older 0.03928 for 8-bit inputs and is shared by OKLab and WCAG luminance.
|
|
106
|
+
*/
|
|
523
107
|
function srgbToLinear(c) {
|
|
524
|
-
|
|
108
|
+
return c <= .04045 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
|
|
525
109
|
}
|
|
110
|
+
/** Converts a single linear-light channel back to sRGB. */
|
|
526
111
|
function linearToSrgb(c) {
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
112
|
+
return c <= .0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - .055;
|
|
113
|
+
}
|
|
114
|
+
const Color = {
|
|
115
|
+
WHITE: {
|
|
116
|
+
r: 1,
|
|
117
|
+
g: 1,
|
|
118
|
+
b: 1,
|
|
119
|
+
a: 1
|
|
120
|
+
},
|
|
121
|
+
BLACK: {
|
|
122
|
+
r: 0,
|
|
123
|
+
g: 0,
|
|
124
|
+
b: 0,
|
|
125
|
+
a: 1
|
|
126
|
+
},
|
|
127
|
+
TRANSPARENT: {
|
|
128
|
+
r: 0,
|
|
129
|
+
g: 0,
|
|
130
|
+
b: 0,
|
|
131
|
+
a: 0
|
|
132
|
+
},
|
|
133
|
+
/** Creates a Color from normalized 0-1 components. */
|
|
134
|
+
create(r = 0, g = 0, b = 0, a = 1) {
|
|
135
|
+
return {
|
|
136
|
+
r,
|
|
137
|
+
g,
|
|
138
|
+
b,
|
|
139
|
+
a
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
/** Parses a hex string (#RGB, #RGBA, #RRGGBB, #RRGGBBAA). */
|
|
143
|
+
fromHex(hex) {
|
|
144
|
+
let h = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
145
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
146
|
+
else if (h.length === 4) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2] + h[3] + h[3];
|
|
147
|
+
return {
|
|
148
|
+
r: Number.parseInt(h.slice(0, 2), 16) / 255,
|
|
149
|
+
g: Number.parseInt(h.slice(2, 4), 16) / 255,
|
|
150
|
+
b: Number.parseInt(h.slice(4, 6), 16) / 255,
|
|
151
|
+
a: h.length >= 8 ? Number.parseInt(h.slice(6, 8), 16) / 255 : 1
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
/** Converts to hex string #RRGGBB or #RRGGBBAA if alpha < 1. */
|
|
155
|
+
toHex(c) {
|
|
156
|
+
const r = Math.round(clamp(c.r, 0, 1) * 255).toString(16).padStart(2, "0");
|
|
157
|
+
const g = Math.round(clamp(c.g, 0, 1) * 255).toString(16).padStart(2, "0");
|
|
158
|
+
const b = Math.round(clamp(c.b, 0, 1) * 255).toString(16).padStart(2, "0");
|
|
159
|
+
if (c.a < 1) return `#${r}${g}${b}${Math.round(clamp(c.a, 0, 1) * 255).toString(16).padStart(2, "0")}`;
|
|
160
|
+
return `#${r}${g}${b}`;
|
|
161
|
+
},
|
|
162
|
+
/** Linearly interpolates between two colors. */
|
|
163
|
+
lerp(a, b, t) {
|
|
164
|
+
return {
|
|
165
|
+
r: a.r + (b.r - a.r) * t,
|
|
166
|
+
g: a.g + (b.g - a.g) * t,
|
|
167
|
+
b: a.b + (b.b - a.b) * t,
|
|
168
|
+
a: a.a + (b.a - a.a) * t
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
/** Component-wise multiplication. */
|
|
172
|
+
multiply(a, b) {
|
|
173
|
+
return {
|
|
174
|
+
r: a.r * b.r,
|
|
175
|
+
g: a.g * b.g,
|
|
176
|
+
b: a.b * b.b,
|
|
177
|
+
a: a.a * b.a
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* Converts to a Skia-compatible color number (0xAARRGGBB).
|
|
182
|
+
* Compatible with @shopify/react-native-skia Skia.Color() format.
|
|
183
|
+
*/
|
|
184
|
+
toSkColor(c) {
|
|
185
|
+
const r = Math.round(clamp(c.r, 0, 1) * 255) & 255;
|
|
186
|
+
const g = Math.round(clamp(c.g, 0, 1) * 255) & 255;
|
|
187
|
+
const b = Math.round(clamp(c.b, 0, 1) * 255) & 255;
|
|
188
|
+
return ((Math.round(clamp(c.a, 0, 1) * 255) & 255) << 24 | r << 16 | g << 8 | b) >>> 0;
|
|
189
|
+
},
|
|
190
|
+
/** Converts sRGB Color to OKLab. Alpha is dropped. */
|
|
191
|
+
toOklab(c) {
|
|
192
|
+
const rl = srgbToLinear(c.r);
|
|
193
|
+
const gl = srgbToLinear(c.g);
|
|
194
|
+
const bl = srgbToLinear(c.b);
|
|
195
|
+
const l = .4122214708 * rl + .5363325363 * gl + .0514459929 * bl;
|
|
196
|
+
const m = .2119034982 * rl + .6806995451 * gl + .1073969566 * bl;
|
|
197
|
+
const s = .0883024619 * rl + .2817188376 * gl + .6299787005 * bl;
|
|
198
|
+
const l_ = Math.cbrt(l);
|
|
199
|
+
const m_ = Math.cbrt(m);
|
|
200
|
+
const s_ = Math.cbrt(s);
|
|
201
|
+
return {
|
|
202
|
+
L: .2104542553 * l_ + .793617785 * m_ - .0040720468 * s_,
|
|
203
|
+
a: 1.9779984951 * l_ - 2.428592205 * m_ + .4505937099 * s_,
|
|
204
|
+
b: .0259040371 * l_ + .7827717662 * m_ - .808675766 * s_
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
/**
|
|
208
|
+
* Converts OKLab to sRGB Color. Out-of-gamut results are clamped to [0,1];
|
|
209
|
+
* round-trips are exact only for in-gamut colors.
|
|
210
|
+
*/
|
|
211
|
+
fromOklab(lab, alpha = 1) {
|
|
212
|
+
const l_ = lab.L + .3963377774 * lab.a + .2158037573 * lab.b;
|
|
213
|
+
const m_ = lab.L - .1055613458 * lab.a - .0638541728 * lab.b;
|
|
214
|
+
const s_ = lab.L - .0894841775 * lab.a - 1.291485548 * lab.b;
|
|
215
|
+
const l = l_ ** 3;
|
|
216
|
+
const m = m_ ** 3;
|
|
217
|
+
const s = s_ ** 3;
|
|
218
|
+
const rl = 4.0767416621 * l - 3.3077115913 * m + .2309699292 * s;
|
|
219
|
+
const gl = -1.2684380046 * l + 2.6097574011 * m - .3413193965 * s;
|
|
220
|
+
const bl = -.0041960863 * l - .7034186147 * m + 1.707614701 * s;
|
|
221
|
+
return {
|
|
222
|
+
r: clamp(linearToSrgb(rl), 0, 1),
|
|
223
|
+
g: clamp(linearToSrgb(gl), 0, 1),
|
|
224
|
+
b: clamp(linearToSrgb(bl), 0, 1),
|
|
225
|
+
a: alpha
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
/** Converts sRGB Color to OKLCH (polar OKLab). Achromatic colors get h = 0. */
|
|
229
|
+
toOklch(c) {
|
|
230
|
+
const lab = Color.toOklab(c);
|
|
231
|
+
const C = Math.hypot(lab.a, lab.b);
|
|
232
|
+
const h = C < 1e-7 ? 0 : (Math.atan2(lab.b, lab.a) * 180 / Math.PI + 360) % 360;
|
|
233
|
+
return {
|
|
234
|
+
L: lab.L,
|
|
235
|
+
C,
|
|
236
|
+
h
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
/**
|
|
240
|
+
* Converts OKLCH to sRGB Color. Hue is wrapped via ((h%360)+360)%360.
|
|
241
|
+
* Out-of-gamut results are clamped; round-trips exact only in-gamut.
|
|
242
|
+
*/
|
|
243
|
+
fromOklch(lch, alpha = 1) {
|
|
244
|
+
const hRad = (lch.h % 360 + 360) % 360 * Math.PI / 180;
|
|
245
|
+
return Color.fromOklab({
|
|
246
|
+
L: lch.L,
|
|
247
|
+
a: lch.C * Math.cos(hRad),
|
|
248
|
+
b: lch.C * Math.sin(hRad)
|
|
249
|
+
}, alpha);
|
|
250
|
+
},
|
|
251
|
+
/** Euclidean distance in OKLab space between two sRGB colors. */
|
|
252
|
+
deltaEOk(a, b) {
|
|
253
|
+
return Color.deltaEOkLab(Color.toOklab(a), Color.toOklab(b));
|
|
254
|
+
},
|
|
255
|
+
/** Euclidean distance between two OKLab values. */
|
|
256
|
+
deltaEOkLab(a, b) {
|
|
257
|
+
return Math.hypot(a.L - b.L, a.a - b.a, a.b - b.b);
|
|
258
|
+
},
|
|
259
|
+
/** WCAG 2.x relative luminance. White = 1, black = 0. */
|
|
260
|
+
relativeLuminance(c) {
|
|
261
|
+
return .2126 * srgbToLinear(c.r) + .7152 * srgbToLinear(c.g) + .0722 * srgbToLinear(c.b);
|
|
262
|
+
},
|
|
263
|
+
/** WCAG 2.x contrast ratio. Order-independent; range [1, 21]. */
|
|
264
|
+
contrastRatio(fg, bg) {
|
|
265
|
+
const l1 = Color.relativeLuminance(fg);
|
|
266
|
+
const l2 = Color.relativeLuminance(bg);
|
|
267
|
+
const lMax = Math.max(l1, l2);
|
|
268
|
+
const lMin = Math.min(l1, l2);
|
|
269
|
+
return (lMax + .05) / (lMin + .05);
|
|
270
|
+
}
|
|
668
271
|
};
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/easings.ts
|
|
274
|
+
const PI = Math.PI;
|
|
275
|
+
const HALF_PI = PI / 2;
|
|
276
|
+
const TAU = PI * 2;
|
|
674
277
|
function linear(t) {
|
|
675
|
-
|
|
278
|
+
return t;
|
|
676
279
|
}
|
|
677
280
|
function step(t) {
|
|
678
|
-
|
|
281
|
+
return t < 1 ? 0 : 1;
|
|
679
282
|
}
|
|
680
283
|
function easeInQuad(t) {
|
|
681
|
-
|
|
284
|
+
return t * t;
|
|
682
285
|
}
|
|
683
286
|
function easeOutQuad(t) {
|
|
684
|
-
|
|
287
|
+
return t * (2 - t);
|
|
685
288
|
}
|
|
686
289
|
function easeInOutQuad(t) {
|
|
687
|
-
|
|
290
|
+
return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
688
291
|
}
|
|
689
292
|
function easeInCubic(t) {
|
|
690
|
-
|
|
293
|
+
return t * t * t;
|
|
691
294
|
}
|
|
692
295
|
function easeOutCubic(t) {
|
|
693
|
-
|
|
694
|
-
|
|
296
|
+
const t1 = t - 1;
|
|
297
|
+
return t1 * t1 * t1 + 1;
|
|
695
298
|
}
|
|
696
299
|
function easeInOutCubic(t) {
|
|
697
|
-
|
|
300
|
+
return t < .5 ? 4 * t * t * t : 1 + (t - 1) * (2 * t - 2) * (2 * t - 2);
|
|
698
301
|
}
|
|
699
302
|
function easeInQuart(t) {
|
|
700
|
-
|
|
303
|
+
return t * t * t * t;
|
|
701
304
|
}
|
|
702
305
|
function easeOutQuart(t) {
|
|
703
|
-
|
|
704
|
-
|
|
306
|
+
const t1 = t - 1;
|
|
307
|
+
return 1 - t1 * t1 * t1 * t1;
|
|
705
308
|
}
|
|
706
309
|
function easeInOutQuart(t) {
|
|
707
|
-
|
|
708
|
-
|
|
310
|
+
const t1 = t - 1;
|
|
311
|
+
return t < .5 ? 8 * t * t * t * t : 1 - 8 * t1 * t1 * t1 * t1;
|
|
709
312
|
}
|
|
710
313
|
function easeInQuint(t) {
|
|
711
|
-
|
|
314
|
+
return t * t * t * t * t;
|
|
712
315
|
}
|
|
713
316
|
function easeOutQuint(t) {
|
|
714
|
-
|
|
715
|
-
|
|
317
|
+
const t1 = t - 1;
|
|
318
|
+
return 1 + t1 * t1 * t1 * t1 * t1;
|
|
716
319
|
}
|
|
717
320
|
function easeInOutQuint(t) {
|
|
718
|
-
|
|
719
|
-
|
|
321
|
+
const t1 = t - 1;
|
|
322
|
+
return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * t1 * t1 * t1 * t1 * t1;
|
|
720
323
|
}
|
|
721
324
|
function easeInSine(t) {
|
|
722
|
-
|
|
325
|
+
return 1 - Math.cos(t * HALF_PI);
|
|
723
326
|
}
|
|
724
327
|
function easeOutSine(t) {
|
|
725
|
-
|
|
328
|
+
return Math.sin(t * HALF_PI);
|
|
726
329
|
}
|
|
727
330
|
function easeInOutSine(t) {
|
|
728
|
-
|
|
331
|
+
return .5 * (1 - Math.cos(PI * t));
|
|
729
332
|
}
|
|
730
333
|
function easeInExpo(t) {
|
|
731
|
-
|
|
334
|
+
return t === 0 ? 0 : 2 ** (10 * (t - 1));
|
|
732
335
|
}
|
|
733
336
|
function easeOutExpo(t) {
|
|
734
|
-
|
|
337
|
+
return t === 1 ? 1 : 1 - 2 ** (-10 * t);
|
|
735
338
|
}
|
|
736
339
|
function easeInOutExpo(t) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
340
|
+
if (t === 0) return 0;
|
|
341
|
+
if (t === 1) return 1;
|
|
342
|
+
return t < .5 ? .5 * 2 ** (20 * t - 10) : 1 - .5 * 2 ** (-20 * t + 10);
|
|
740
343
|
}
|
|
741
344
|
function easeInCirc(t) {
|
|
742
|
-
|
|
345
|
+
return 1 - Math.sqrt(1 - t * t);
|
|
743
346
|
}
|
|
744
347
|
function easeOutCirc(t) {
|
|
745
|
-
|
|
746
|
-
|
|
348
|
+
const t1 = t - 1;
|
|
349
|
+
return Math.sqrt(1 - t1 * t1);
|
|
747
350
|
}
|
|
748
351
|
function easeInOutCirc(t) {
|
|
749
|
-
|
|
352
|
+
return t < .5 ? .5 * (1 - Math.sqrt(1 - 4 * t * t)) : .5 * (Math.sqrt(1 - (2 * t - 2) * (2 * t - 2)) + 1);
|
|
750
353
|
}
|
|
751
|
-
|
|
752
|
-
|
|
354
|
+
const S = 1.70158;
|
|
355
|
+
const S2 = S * 1.525;
|
|
753
356
|
function easeInBack(t) {
|
|
754
|
-
|
|
357
|
+
return t * t * (2.70158 * t - S);
|
|
755
358
|
}
|
|
756
359
|
function easeOutBack(t) {
|
|
757
|
-
|
|
758
|
-
|
|
360
|
+
const t1 = t - 1;
|
|
361
|
+
return t1 * t1 * (2.70158 * t1 + S) + 1;
|
|
759
362
|
}
|
|
760
363
|
function easeInOutBack(t) {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
364
|
+
if (t < .5) {
|
|
365
|
+
const t2 = 2 * t;
|
|
366
|
+
return .5 * (t2 * t2 * (3.5949095 * t2 - S2));
|
|
367
|
+
}
|
|
368
|
+
const t2 = 2 * t - 2;
|
|
369
|
+
return .5 * (t2 * t2 * (3.5949095 * t2 + S2) + 2);
|
|
767
370
|
}
|
|
768
371
|
function easeInElastic(t) {
|
|
769
|
-
|
|
770
|
-
|
|
372
|
+
if (t === 0 || t === 1) return t;
|
|
373
|
+
return -(2 ** (10 * t - 10)) * Math.sin((t * 10 - 10.75) * (TAU / 3));
|
|
771
374
|
}
|
|
772
375
|
function easeOutElastic(t) {
|
|
773
|
-
|
|
774
|
-
|
|
376
|
+
if (t === 0 || t === 1) return t;
|
|
377
|
+
return 2 ** (-10 * t) * Math.sin((t * 10 - .75) * (TAU / 3)) + 1;
|
|
775
378
|
}
|
|
776
379
|
function easeInOutElastic(t) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
380
|
+
if (t === 0 || t === 1) return t;
|
|
381
|
+
const p = TAU / 4.5;
|
|
382
|
+
return t < .5 ? -.5 * 2 ** (20 * t - 10) * Math.sin((20 * t - 11.125) * p) : .5 * 2 ** (-20 * t + 10) * Math.sin((20 * t - 11.125) * p) + 1;
|
|
780
383
|
}
|
|
781
384
|
function easeOutBounce(t) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const t2 = t - 2.625 / 2.75;
|
|
794
|
-
return 7.5625 * t2 * t2 + 0.984375;
|
|
385
|
+
if (t < 1 / 2.75) return 7.5625 * t * t;
|
|
386
|
+
if (t < 2 / 2.75) {
|
|
387
|
+
const t2 = t - 1.5 / 2.75;
|
|
388
|
+
return 7.5625 * t2 * t2 + .75;
|
|
389
|
+
}
|
|
390
|
+
if (t < 2.5 / 2.75) {
|
|
391
|
+
const t2 = t - 2.25 / 2.75;
|
|
392
|
+
return 7.5625 * t2 * t2 + .9375;
|
|
393
|
+
}
|
|
394
|
+
const t2 = t - 2.625 / 2.75;
|
|
395
|
+
return 7.5625 * t2 * t2 + .984375;
|
|
795
396
|
}
|
|
796
397
|
function easeInBounce(t) {
|
|
797
|
-
|
|
398
|
+
return 1 - easeOutBounce(1 - t);
|
|
798
399
|
}
|
|
799
400
|
function easeInOutBounce(t) {
|
|
800
|
-
|
|
401
|
+
return t < .5 ? .5 * easeInBounce(t * 2) : .5 * easeOutBounce(t * 2 - 1) + .5;
|
|
801
402
|
}
|
|
802
403
|
function springIn(t) {
|
|
803
|
-
|
|
404
|
+
return t * t * (2.70158 * t - 1.70158);
|
|
804
405
|
}
|
|
805
406
|
function springOut(t) {
|
|
806
|
-
|
|
807
|
-
|
|
407
|
+
const t1 = t - 1;
|
|
408
|
+
return t1 * t1 * (2.70158 * t1 + 1.70158) + 1;
|
|
808
409
|
}
|
|
809
410
|
function springInOut(t) {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
411
|
+
if (t < .5) {
|
|
412
|
+
const t2 = 2 * t;
|
|
413
|
+
return .5 * t2 * t2 * (3.5949095 * t2 - S2);
|
|
414
|
+
}
|
|
415
|
+
const t2 = 2 * t - 2;
|
|
416
|
+
return .5 * (t2 * t2 * (3.5949095 * t2 + S2) + 2);
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/intersection.ts
|
|
420
|
+
/** Returns true if point is inside rect (inclusive). */
|
|
421
|
+
function pointInRect(p, r) {
|
|
422
|
+
return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
|
|
816
423
|
}
|
|
817
|
-
|
|
818
|
-
|
|
424
|
+
/** Returns true if point is inside circle (inclusive). */
|
|
425
|
+
function pointInCircle(p, c) {
|
|
426
|
+
const dx = p.x - c.x;
|
|
427
|
+
const dy = p.y - c.y;
|
|
428
|
+
return dx * dx + dy * dy <= c.radius * c.radius;
|
|
429
|
+
}
|
|
430
|
+
/** Returns true if two rects overlap. */
|
|
431
|
+
function rectVsRect(a, b) {
|
|
432
|
+
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
433
|
+
}
|
|
434
|
+
/** Returns true if rect and circle overlap. */
|
|
435
|
+
function rectVsCircle(r, c) {
|
|
436
|
+
const closestX = c.x < r.x ? r.x : c.x > r.x + r.width ? r.x + r.width : c.x;
|
|
437
|
+
const closestY = c.y < r.y ? r.y : c.y > r.y + r.height ? r.y + r.height : c.y;
|
|
438
|
+
const dx = c.x - closestX;
|
|
439
|
+
const dy = c.y - closestY;
|
|
440
|
+
return dx * dx + dy * dy <= c.radius * c.radius;
|
|
441
|
+
}
|
|
442
|
+
/** Returns true if two circles overlap. */
|
|
443
|
+
function circleVsCircle(a, b) {
|
|
444
|
+
const dx = b.x - a.x;
|
|
445
|
+
const dy = b.y - a.y;
|
|
446
|
+
const radSum = a.radius + b.radius;
|
|
447
|
+
return dx * dx + dy * dy < radSum * radSum;
|
|
448
|
+
}
|
|
449
|
+
/** Line segment intersection with AABB. Uses Liang-Barsky algorithm. */
|
|
450
|
+
function lineVsRect(x1, y1, x2, y2, r) {
|
|
451
|
+
const dx = x2 - x1;
|
|
452
|
+
const dy = y2 - y1;
|
|
453
|
+
let tMin = 0;
|
|
454
|
+
let tMax = 1;
|
|
455
|
+
const edges = [
|
|
456
|
+
{
|
|
457
|
+
p: -dx,
|
|
458
|
+
q: x1 - r.x
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
p: dx,
|
|
462
|
+
q: r.x + r.width - x1
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
p: -dy,
|
|
466
|
+
q: y1 - r.y
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
p: dy,
|
|
470
|
+
q: r.y + r.height - y1
|
|
471
|
+
}
|
|
472
|
+
];
|
|
473
|
+
for (const { p, q } of edges) if (Math.abs(p) < 1e-12) {
|
|
474
|
+
if (q < 0) return false;
|
|
475
|
+
} else {
|
|
476
|
+
const t = q / p;
|
|
477
|
+
if (p < 0) {
|
|
478
|
+
if (t > tMax) return false;
|
|
479
|
+
if (t > tMin) tMin = t;
|
|
480
|
+
} else {
|
|
481
|
+
if (t < tMin) return false;
|
|
482
|
+
if (t < tMax) tMax = t;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return tMin <= tMax;
|
|
486
|
+
}
|
|
487
|
+
/** Line segment intersection with circle. */
|
|
488
|
+
function lineVsCircle(x1, y1, x2, y2, c) {
|
|
489
|
+
const dx = x2 - x1;
|
|
490
|
+
const dy = y2 - y1;
|
|
491
|
+
const fx = x1 - c.x;
|
|
492
|
+
const fy = y1 - c.y;
|
|
493
|
+
const a = dx * dx + dy * dy;
|
|
494
|
+
const b = 2 * (fx * dx + fy * dy);
|
|
495
|
+
const cc = fx * fx + fy * fy - c.radius * c.radius;
|
|
496
|
+
if (a < 1e-12) return cc <= 0;
|
|
497
|
+
let discriminant = b * b - 4 * a * cc;
|
|
498
|
+
if (discriminant < 0) return false;
|
|
499
|
+
discriminant = Math.sqrt(discriminant);
|
|
500
|
+
const t1 = (-b - discriminant) / (2 * a);
|
|
501
|
+
const t2 = (-b + discriminant) / (2 * a);
|
|
502
|
+
if (t1 >= 0 && t1 <= 1) return true;
|
|
503
|
+
if (t2 >= 0 && t2 <= 1) return true;
|
|
504
|
+
if (t1 < 0 && t2 > 1) return true;
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
/** Ray intersection with AABB. Returns t (distance) or null if no hit. */
|
|
508
|
+
function rayVsRect(originX, originY, dirX, dirY, r) {
|
|
509
|
+
const invDx = dirX !== 0 ? 1 / dirX : Number.POSITIVE_INFINITY;
|
|
510
|
+
const invDy = dirY !== 0 ? 1 / dirY : Number.POSITIVE_INFINITY;
|
|
511
|
+
const tx1 = (r.x - originX) * invDx;
|
|
512
|
+
const tx2 = (r.x + r.width - originX) * invDx;
|
|
513
|
+
const ty1 = (r.y - originY) * invDy;
|
|
514
|
+
const ty2 = (r.y + r.height - originY) * invDy;
|
|
515
|
+
const tMin = Math.max(Math.min(tx1, tx2), Math.min(ty1, ty2));
|
|
516
|
+
const tMax = Math.min(Math.max(tx1, tx2), Math.max(ty1, ty2));
|
|
517
|
+
if (tMax < 0 || tMin > tMax) return null;
|
|
518
|
+
return tMin >= 0 ? tMin : tMax;
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/mat3.ts
|
|
522
|
+
const Mat3 = {
|
|
523
|
+
/** Creates a new identity matrix. */
|
|
524
|
+
create() {
|
|
525
|
+
const m = /* @__PURE__ */ new Float32Array(9);
|
|
526
|
+
m[0] = 1;
|
|
527
|
+
m[4] = 1;
|
|
528
|
+
m[8] = 1;
|
|
529
|
+
return m;
|
|
530
|
+
},
|
|
531
|
+
/** Sets m to identity. */
|
|
532
|
+
identity(m) {
|
|
533
|
+
m[0] = 1;
|
|
534
|
+
m[1] = 0;
|
|
535
|
+
m[2] = 0;
|
|
536
|
+
m[3] = 0;
|
|
537
|
+
m[4] = 1;
|
|
538
|
+
m[5] = 0;
|
|
539
|
+
m[6] = 0;
|
|
540
|
+
m[7] = 0;
|
|
541
|
+
m[8] = 1;
|
|
542
|
+
return m;
|
|
543
|
+
},
|
|
544
|
+
/** Multiplies a * b, writes into out (or allocates). */
|
|
545
|
+
multiply(a, b, out) {
|
|
546
|
+
const a00 = a[0], a01 = a[3], a02 = a[6];
|
|
547
|
+
const a10 = a[1], a11 = a[4], a12 = a[7];
|
|
548
|
+
const a20 = a[2], a21 = a[5], a22 = a[8];
|
|
549
|
+
const b00 = b[0], b01 = b[3], b02 = b[6];
|
|
550
|
+
const b10 = b[1], b11 = b[4], b12 = b[7];
|
|
551
|
+
const b20 = b[2], b21 = b[5], b22 = b[8];
|
|
552
|
+
const o = out ?? /* @__PURE__ */ new Float32Array(9);
|
|
553
|
+
o[0] = a00 * b00 + a01 * b10 + a02 * b20;
|
|
554
|
+
o[1] = a10 * b00 + a11 * b10 + a12 * b20;
|
|
555
|
+
o[2] = a20 * b00 + a21 * b10 + a22 * b20;
|
|
556
|
+
o[3] = a00 * b01 + a01 * b11 + a02 * b21;
|
|
557
|
+
o[4] = a10 * b01 + a11 * b11 + a12 * b21;
|
|
558
|
+
o[5] = a20 * b01 + a21 * b11 + a22 * b21;
|
|
559
|
+
o[6] = a00 * b02 + a01 * b12 + a02 * b22;
|
|
560
|
+
o[7] = a10 * b02 + a11 * b12 + a12 * b22;
|
|
561
|
+
o[8] = a20 * b02 + a21 * b12 + a22 * b22;
|
|
562
|
+
return o;
|
|
563
|
+
},
|
|
564
|
+
/** Creates a translation matrix. */
|
|
565
|
+
translate(tx, ty, out) {
|
|
566
|
+
const m = out ?? /* @__PURE__ */ new Float32Array(9);
|
|
567
|
+
m[0] = 1;
|
|
568
|
+
m[1] = 0;
|
|
569
|
+
m[2] = 0;
|
|
570
|
+
m[3] = 0;
|
|
571
|
+
m[4] = 1;
|
|
572
|
+
m[5] = 0;
|
|
573
|
+
m[6] = tx;
|
|
574
|
+
m[7] = ty;
|
|
575
|
+
m[8] = 1;
|
|
576
|
+
return m;
|
|
577
|
+
},
|
|
578
|
+
/** Creates a rotation matrix (radians). */
|
|
579
|
+
rotate(angle, out) {
|
|
580
|
+
const c = Math.cos(angle);
|
|
581
|
+
const s = Math.sin(angle);
|
|
582
|
+
const m = out ?? /* @__PURE__ */ new Float32Array(9);
|
|
583
|
+
m[0] = c;
|
|
584
|
+
m[1] = s;
|
|
585
|
+
m[2] = 0;
|
|
586
|
+
m[3] = -s;
|
|
587
|
+
m[4] = c;
|
|
588
|
+
m[5] = 0;
|
|
589
|
+
m[6] = 0;
|
|
590
|
+
m[7] = 0;
|
|
591
|
+
m[8] = 1;
|
|
592
|
+
return m;
|
|
593
|
+
},
|
|
594
|
+
/** Creates a scale matrix. */
|
|
595
|
+
scale(sx, sy, out) {
|
|
596
|
+
const m = out ?? /* @__PURE__ */ new Float32Array(9);
|
|
597
|
+
m[0] = sx;
|
|
598
|
+
m[1] = 0;
|
|
599
|
+
m[2] = 0;
|
|
600
|
+
m[3] = 0;
|
|
601
|
+
m[4] = sy;
|
|
602
|
+
m[5] = 0;
|
|
603
|
+
m[6] = 0;
|
|
604
|
+
m[7] = 0;
|
|
605
|
+
m[8] = 1;
|
|
606
|
+
return m;
|
|
607
|
+
},
|
|
608
|
+
/** Inverts the matrix. Returns null if singular. */
|
|
609
|
+
invert(m, out) {
|
|
610
|
+
const a00 = m[0], a01 = m[3], a02 = m[6];
|
|
611
|
+
const a10 = m[1], a11 = m[4], a12 = m[7];
|
|
612
|
+
const a20 = m[2], a21 = m[5], a22 = m[8];
|
|
613
|
+
const det = a00 * (a11 * a22 - a12 * a21) - a01 * (a10 * a22 - a12 * a20) + a02 * (a10 * a21 - a11 * a20);
|
|
614
|
+
if (Math.abs(det) < 1e-8) return null;
|
|
615
|
+
const invDet = 1 / det;
|
|
616
|
+
const o = out ?? /* @__PURE__ */ new Float32Array(9);
|
|
617
|
+
o[0] = (a11 * a22 - a12 * a21) * invDet;
|
|
618
|
+
o[1] = (a12 * a20 - a10 * a22) * invDet;
|
|
619
|
+
o[2] = (a10 * a21 - a11 * a20) * invDet;
|
|
620
|
+
o[3] = (a02 * a21 - a01 * a22) * invDet;
|
|
621
|
+
o[4] = (a00 * a22 - a02 * a20) * invDet;
|
|
622
|
+
o[5] = (a01 * a20 - a00 * a21) * invDet;
|
|
623
|
+
o[6] = (a01 * a12 - a02 * a11) * invDet;
|
|
624
|
+
o[7] = (a02 * a10 - a00 * a12) * invDet;
|
|
625
|
+
o[8] = (a00 * a11 - a01 * a10) * invDet;
|
|
626
|
+
return o;
|
|
627
|
+
},
|
|
628
|
+
/** Builds a matrix from a Transform2D (TRS order: translate * rotate * scale). */
|
|
629
|
+
fromTransform(t, out) {
|
|
630
|
+
const c = Math.cos(t.rotation);
|
|
631
|
+
const s = Math.sin(t.rotation);
|
|
632
|
+
const sx = t.scale.x;
|
|
633
|
+
const sy = t.scale.y;
|
|
634
|
+
const m = out ?? /* @__PURE__ */ new Float32Array(9);
|
|
635
|
+
m[0] = c * sx;
|
|
636
|
+
m[1] = s * sx;
|
|
637
|
+
m[2] = 0;
|
|
638
|
+
m[3] = -s * sy;
|
|
639
|
+
m[4] = c * sy;
|
|
640
|
+
m[5] = 0;
|
|
641
|
+
m[6] = t.position.x;
|
|
642
|
+
m[7] = t.position.y;
|
|
643
|
+
m[8] = 1;
|
|
644
|
+
return m;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/random.ts
|
|
649
|
+
/** Seeded PRNG using mulberry32 algorithm. */
|
|
819
650
|
var SeededRNG = class {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
651
|
+
state;
|
|
652
|
+
constructor(seed) {
|
|
653
|
+
this.state = seed | 0;
|
|
654
|
+
}
|
|
655
|
+
/** Returns a float in [0, 1). */
|
|
656
|
+
next() {
|
|
657
|
+
this.state = this.state + 1831565813 | 0;
|
|
658
|
+
let t = Math.imul(this.state ^ this.state >>> 15, 1 | this.state);
|
|
659
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
660
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
661
|
+
}
|
|
662
|
+
/** Returns a float in [min, max). */
|
|
663
|
+
float(min, max) {
|
|
664
|
+
return min + this.next() * (max - min);
|
|
665
|
+
}
|
|
666
|
+
/** Returns an integer in [min, max] (inclusive). */
|
|
667
|
+
int(min, max) {
|
|
668
|
+
return Math.floor(this.float(min, max + 1));
|
|
669
|
+
}
|
|
839
670
|
};
|
|
671
|
+
/** Returns a random float in [min, max). */
|
|
840
672
|
function randomFloat(min, max) {
|
|
841
|
-
|
|
673
|
+
return min + Math.random() * (max - min);
|
|
842
674
|
}
|
|
675
|
+
/** Returns a random integer in [min, max] (inclusive). */
|
|
843
676
|
function randomInt(min, max) {
|
|
844
|
-
|
|
677
|
+
return Math.floor(randomFloat(min, max + 1));
|
|
845
678
|
}
|
|
679
|
+
/** Returns a random Vec2 within a rect. */
|
|
846
680
|
function randomVec2InRect(r) {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
681
|
+
return {
|
|
682
|
+
x: r.x + Math.random() * r.width,
|
|
683
|
+
y: r.y + Math.random() * r.height
|
|
684
|
+
};
|
|
851
685
|
}
|
|
686
|
+
/** Returns a random point inside a circle (uniform distribution). */
|
|
852
687
|
function randomPointInCircle(cx, cy, radius) {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
688
|
+
const r = radius * Math.sqrt(Math.random());
|
|
689
|
+
const angle = Math.random() * Math.PI * 2;
|
|
690
|
+
return {
|
|
691
|
+
x: cx + r * Math.cos(angle),
|
|
692
|
+
y: cy + r * Math.sin(angle)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
/** Fisher-Yates shuffle (in-place). Returns the array. */
|
|
860
696
|
function shuffle(array) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}
|
|
697
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
698
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
699
|
+
const tmp = array[i];
|
|
700
|
+
array[i] = array[j];
|
|
701
|
+
array[j] = tmp;
|
|
702
|
+
}
|
|
703
|
+
return array;
|
|
704
|
+
}
|
|
705
|
+
/** Returns a random element from the array. */
|
|
869
706
|
function choose(array) {
|
|
870
|
-
|
|
871
|
-
|
|
707
|
+
if (array.length === 0) return void 0;
|
|
708
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
872
709
|
}
|
|
710
|
+
/**
|
|
711
|
+
* Picks an index from a weight array using a cumulative walk.
|
|
712
|
+
* `roll01` must be in [0, 1); returns 0 for an empty or all-zero array.
|
|
713
|
+
*/
|
|
873
714
|
function weightedIndex(weights, roll01) {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
715
|
+
let total = 0;
|
|
716
|
+
for (let i = 0; i < weights.length; i++) total += weights[i] ?? 0;
|
|
717
|
+
if (total <= 0) return 0;
|
|
718
|
+
const target = roll01 * total;
|
|
719
|
+
let acc = 0;
|
|
720
|
+
for (let i = 0; i < weights.length; i++) {
|
|
721
|
+
acc += weights[i] ?? 0;
|
|
722
|
+
if (target < acc) return i;
|
|
723
|
+
}
|
|
724
|
+
return weights.length - 1;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Picks an index from `items` using a per-item weight accessor.
|
|
728
|
+
* `roll01` must be in [0, 1); returns 0 for an empty or all-zero-weight array.
|
|
729
|
+
*/
|
|
885
730
|
function weightedIndexBy(items, weight, roll01) {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
}
|
|
731
|
+
let total = 0;
|
|
732
|
+
for (let i = 0; i < items.length; i++) total += weight(items[i]);
|
|
733
|
+
if (total <= 0) return 0;
|
|
734
|
+
const target = roll01 * total;
|
|
735
|
+
let acc = 0;
|
|
736
|
+
for (let i = 0; i < items.length; i++) {
|
|
737
|
+
acc += weight(items[i]);
|
|
738
|
+
if (target < acc) return i;
|
|
739
|
+
}
|
|
740
|
+
return items.length - 1;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Picks an item from `items` using a per-item weight accessor.
|
|
744
|
+
* Returns `undefined` for an empty array; returns the last item on float-edge.
|
|
745
|
+
*/
|
|
897
746
|
function weightedPick(items, weight, roll01) {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
}
|
|
747
|
+
if (items.length === 0) return void 0;
|
|
748
|
+
return items[weightedIndexBy(items, weight, roll01)];
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* 32-bit avalanche finalizer (lowbias32). Mixes `seed` so that adjacent seeds
|
|
752
|
+
* decorrelate into different output values. Returns an unsigned 32-bit integer.
|
|
753
|
+
*
|
|
754
|
+
* Constants from https://nullprogram.com/blog/2018/07/31/ (lowbias32).
|
|
755
|
+
*/
|
|
901
756
|
function hashU32(seed) {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
}
|
|
757
|
+
let h = (seed | 0) >>> 0;
|
|
758
|
+
h = Math.imul(h ^ h >>> 16, 73244475) >>> 0;
|
|
759
|
+
h = Math.imul(h ^ h >>> 16, 73244475) >>> 0;
|
|
760
|
+
return (h ^ h >>> 16) >>> 0;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Deterministically maps `seed` to an index in [0, count).
|
|
764
|
+
* Single-entry pools short-circuit to 0. Uses `hashU32` to decorrelate
|
|
765
|
+
* adjacent seeds before the modulo.
|
|
766
|
+
*/
|
|
907
767
|
function seededIndex(seed, count) {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
|
|
768
|
+
if (count <= 1) return 0;
|
|
769
|
+
return hashU32(seed) % count;
|
|
770
|
+
}
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/rect.ts
|
|
773
|
+
const Rect = {
|
|
774
|
+
/** Returns true if point is inside rect (inclusive). */
|
|
775
|
+
contains(r, p) {
|
|
776
|
+
return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
|
|
777
|
+
},
|
|
778
|
+
/** Returns true if two rects overlap (share area). */
|
|
779
|
+
intersects(a, b) {
|
|
780
|
+
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
781
|
+
},
|
|
782
|
+
/** Returns the overlapping rect, or null if no overlap. */
|
|
783
|
+
intersection(a, b) {
|
|
784
|
+
const x = Math.max(a.x, b.x);
|
|
785
|
+
const y = Math.max(a.y, b.y);
|
|
786
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
787
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
788
|
+
if (right <= x || bottom <= y) return null;
|
|
789
|
+
return {
|
|
790
|
+
x,
|
|
791
|
+
y,
|
|
792
|
+
width: right - x,
|
|
793
|
+
height: bottom - y
|
|
794
|
+
};
|
|
795
|
+
},
|
|
796
|
+
/** Returns the smallest rect containing both rects. */
|
|
797
|
+
union(a, b) {
|
|
798
|
+
const x = Math.min(a.x, b.x);
|
|
799
|
+
const y = Math.min(a.y, b.y);
|
|
800
|
+
const right = Math.max(a.x + a.width, b.x + b.width);
|
|
801
|
+
const bottom = Math.max(a.y + a.height, b.y + b.height);
|
|
802
|
+
return {
|
|
803
|
+
x,
|
|
804
|
+
y,
|
|
805
|
+
width: right - x,
|
|
806
|
+
height: bottom - y
|
|
807
|
+
};
|
|
808
|
+
},
|
|
809
|
+
/** Creates a rect from an array of points (bounding box). */
|
|
810
|
+
fromPoints(points) {
|
|
811
|
+
if (points.length === 0) return {
|
|
812
|
+
x: 0,
|
|
813
|
+
y: 0,
|
|
814
|
+
width: 0,
|
|
815
|
+
height: 0
|
|
816
|
+
};
|
|
817
|
+
let minX = points[0].x;
|
|
818
|
+
let minY = points[0].y;
|
|
819
|
+
let maxX = minX;
|
|
820
|
+
let maxY = minY;
|
|
821
|
+
for (let i = 1; i < points.length; i++) {
|
|
822
|
+
const p = points[i];
|
|
823
|
+
if (p.x < minX) minX = p.x;
|
|
824
|
+
if (p.y < minY) minY = p.y;
|
|
825
|
+
if (p.x > maxX) maxX = p.x;
|
|
826
|
+
if (p.y > maxY) maxY = p.y;
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
x: minX,
|
|
830
|
+
y: minY,
|
|
831
|
+
width: maxX - minX,
|
|
832
|
+
height: maxY - minY
|
|
833
|
+
};
|
|
834
|
+
},
|
|
835
|
+
/** Returns the center point of the rect. */
|
|
836
|
+
center(r) {
|
|
837
|
+
return {
|
|
838
|
+
x: r.x + r.width * .5,
|
|
839
|
+
y: r.y + r.height * .5
|
|
840
|
+
};
|
|
841
|
+
},
|
|
842
|
+
/** Returns a new rect expanded by amount on each side. */
|
|
843
|
+
expand(r, amount) {
|
|
844
|
+
return {
|
|
845
|
+
x: r.x - amount,
|
|
846
|
+
y: r.y - amount,
|
|
847
|
+
width: r.width + amount * 2,
|
|
848
|
+
height: r.height + amount * 2
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
//#endregion
|
|
853
|
+
//#region src/reward.ts
|
|
854
|
+
/**
|
|
855
|
+
* Timed-window and capped-bonus math — pure, zero-alloc scoring helpers.
|
|
856
|
+
*/
|
|
857
|
+
/**
|
|
858
|
+
* Bonus pay floored to an integer: `pay × min(frac, capFrac)`.
|
|
859
|
+
* `frac` is the current multiplier (e.g. `streak * fracPerStep`);
|
|
860
|
+
* `capFrac` bounds the multiplier so it cannot grow unboundedly.
|
|
861
|
+
* Returns 0 for zero or negative `pay`.
|
|
862
|
+
*/
|
|
913
863
|
function cappedBonus(pay, frac, capFrac) {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
864
|
+
if (pay <= 0) return 0;
|
|
865
|
+
return Math.floor(pay * Math.min(frac, capFrac));
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Advances a timed streak counter. A new event within `windowSec` of `lastT`
|
|
869
|
+
* increments `prev`; a gap (or `lastT < 0` meaning "no prior event") resets
|
|
870
|
+
* to 1. Pure — the caller stores the returned value and stamps the new time.
|
|
871
|
+
* `elapsed` is the monotonic clock at the new event.
|
|
872
|
+
*/
|
|
917
873
|
function advanceWindowedStreak(prev, elapsed, lastT, windowSec) {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
874
|
+
if (lastT >= 0 && elapsed - lastT <= windowSec) return prev + 1;
|
|
875
|
+
return 1;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Returns true when a streak window has lapsed: a window was open (`lastT >= 0`)
|
|
879
|
+
* and more than `windowSec` seconds have passed since `lastT`.
|
|
880
|
+
* `lastT < 0` means no event has ever occurred — never expired.
|
|
881
|
+
*/
|
|
921
882
|
function windowExpired(elapsed, lastT, windowSec) {
|
|
922
|
-
|
|
883
|
+
return lastT >= 0 && elapsed - lastT > windowSec;
|
|
923
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* How many more events may be awarded in the current window given `awarded`
|
|
887
|
+
* already counted. Clamped to 0 — never negative.
|
|
888
|
+
*/
|
|
924
889
|
function remainingInWindow(awarded, capPerWindow) {
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
if (t1 >= 0 && t1 <= 1) return true;
|
|
997
|
-
if (t2 >= 0 && t2 <= 1) return true;
|
|
998
|
-
if (t1 < 0 && t2 > 1) return true;
|
|
999
|
-
return false;
|
|
1000
|
-
}
|
|
1001
|
-
function rayVsRect(originX, originY, dirX, dirY, r) {
|
|
1002
|
-
const invDx = dirX !== 0 ? 1 / dirX : Number.POSITIVE_INFINITY;
|
|
1003
|
-
const invDy = dirY !== 0 ? 1 / dirY : Number.POSITIVE_INFINITY;
|
|
1004
|
-
const tx1 = (r.x - originX) * invDx;
|
|
1005
|
-
const tx2 = (r.x + r.width - originX) * invDx;
|
|
1006
|
-
const ty1 = (r.y - originY) * invDy;
|
|
1007
|
-
const ty2 = (r.y + r.height - originY) * invDy;
|
|
1008
|
-
const tMin = Math.max(Math.min(tx1, tx2), Math.min(ty1, ty2));
|
|
1009
|
-
const tMax = Math.min(Math.max(tx1, tx2), Math.max(ty1, ty2));
|
|
1010
|
-
if (tMax < 0 || tMin > tMax) return null;
|
|
1011
|
-
return tMin >= 0 ? tMin : tMax;
|
|
1012
|
-
}
|
|
1013
|
-
export {
|
|
1014
|
-
Circle,
|
|
1015
|
-
Color,
|
|
1016
|
-
Mat3,
|
|
1017
|
-
Rect,
|
|
1018
|
-
SeededRNG,
|
|
1019
|
-
Transform2D,
|
|
1020
|
-
Vec2,
|
|
1021
|
-
advanceWindowedStreak,
|
|
1022
|
-
angleDelta,
|
|
1023
|
-
approxEquals,
|
|
1024
|
-
cappedBonus,
|
|
1025
|
-
choose,
|
|
1026
|
-
circleVsCircle,
|
|
1027
|
-
clamp,
|
|
1028
|
-
degToRad,
|
|
1029
|
-
easeInBack,
|
|
1030
|
-
easeInBounce,
|
|
1031
|
-
easeInCirc,
|
|
1032
|
-
easeInCubic,
|
|
1033
|
-
easeInElastic,
|
|
1034
|
-
easeInExpo,
|
|
1035
|
-
easeInOutBack,
|
|
1036
|
-
easeInOutBounce,
|
|
1037
|
-
easeInOutCirc,
|
|
1038
|
-
easeInOutCubic,
|
|
1039
|
-
easeInOutElastic,
|
|
1040
|
-
easeInOutExpo,
|
|
1041
|
-
easeInOutQuad,
|
|
1042
|
-
easeInOutQuart,
|
|
1043
|
-
easeInOutQuint,
|
|
1044
|
-
easeInOutSine,
|
|
1045
|
-
easeInQuad,
|
|
1046
|
-
easeInQuart,
|
|
1047
|
-
easeInQuint,
|
|
1048
|
-
easeInSine,
|
|
1049
|
-
easeOutBack,
|
|
1050
|
-
easeOutBounce,
|
|
1051
|
-
easeOutCirc,
|
|
1052
|
-
easeOutCubic,
|
|
1053
|
-
easeOutElastic,
|
|
1054
|
-
easeOutExpo,
|
|
1055
|
-
easeOutQuad,
|
|
1056
|
-
easeOutQuart,
|
|
1057
|
-
easeOutQuint,
|
|
1058
|
-
easeOutSine,
|
|
1059
|
-
hashU32,
|
|
1060
|
-
inverseLerp,
|
|
1061
|
-
lerp,
|
|
1062
|
-
lineVsCircle,
|
|
1063
|
-
lineVsRect,
|
|
1064
|
-
linear,
|
|
1065
|
-
map,
|
|
1066
|
-
pingPong,
|
|
1067
|
-
pointInCircle,
|
|
1068
|
-
pointInRect,
|
|
1069
|
-
radToDeg,
|
|
1070
|
-
randomFloat,
|
|
1071
|
-
randomInt,
|
|
1072
|
-
randomPointInCircle,
|
|
1073
|
-
randomVec2InRect,
|
|
1074
|
-
rayVsRect,
|
|
1075
|
-
rectVsCircle,
|
|
1076
|
-
rectVsRect,
|
|
1077
|
-
remainingInWindow,
|
|
1078
|
-
rotateTowards,
|
|
1079
|
-
seededIndex,
|
|
1080
|
-
shuffle,
|
|
1081
|
-
sign,
|
|
1082
|
-
smoothStep,
|
|
1083
|
-
springIn,
|
|
1084
|
-
springInOut,
|
|
1085
|
-
springOut,
|
|
1086
|
-
step,
|
|
1087
|
-
vec2,
|
|
1088
|
-
weightedIndex,
|
|
1089
|
-
weightedIndexBy,
|
|
1090
|
-
weightedPick,
|
|
1091
|
-
windowExpired,
|
|
1092
|
-
wrap
|
|
890
|
+
return Math.max(0, capPerWindow - awarded);
|
|
891
|
+
}
|
|
892
|
+
//#endregion
|
|
893
|
+
//#region src/transform2d.ts
|
|
894
|
+
const Transform2D = {
|
|
895
|
+
/** Creates an identity transform. */
|
|
896
|
+
identity() {
|
|
897
|
+
return {
|
|
898
|
+
position: {
|
|
899
|
+
x: 0,
|
|
900
|
+
y: 0
|
|
901
|
+
},
|
|
902
|
+
rotation: 0,
|
|
903
|
+
scale: {
|
|
904
|
+
x: 1,
|
|
905
|
+
y: 1
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
},
|
|
909
|
+
/** Creates a transform from components. */
|
|
910
|
+
create(x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1) {
|
|
911
|
+
return {
|
|
912
|
+
position: {
|
|
913
|
+
x,
|
|
914
|
+
y
|
|
915
|
+
},
|
|
916
|
+
rotation,
|
|
917
|
+
scale: {
|
|
918
|
+
x: scaleX,
|
|
919
|
+
y: scaleY
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
},
|
|
923
|
+
/** Transforms a local-space point to world-space. */
|
|
924
|
+
localToWorld(t, point) {
|
|
925
|
+
const c = Math.cos(t.rotation);
|
|
926
|
+
const s = Math.sin(t.rotation);
|
|
927
|
+
const sx = point.x * t.scale.x;
|
|
928
|
+
const sy = point.y * t.scale.y;
|
|
929
|
+
return {
|
|
930
|
+
x: sx * c - sy * s + t.position.x,
|
|
931
|
+
y: sx * s + sy * c + t.position.y
|
|
932
|
+
};
|
|
933
|
+
},
|
|
934
|
+
/** Transforms a world-space point to local-space. */
|
|
935
|
+
worldToLocal(t, point) {
|
|
936
|
+
const dx = point.x - t.position.x;
|
|
937
|
+
const dy = point.y - t.position.y;
|
|
938
|
+
const c = Math.cos(-t.rotation);
|
|
939
|
+
const s = Math.sin(-t.rotation);
|
|
940
|
+
const rx = dx * c - dy * s;
|
|
941
|
+
const ry = dx * s + dy * c;
|
|
942
|
+
return {
|
|
943
|
+
x: t.scale.x !== 0 ? rx / t.scale.x : 0,
|
|
944
|
+
y: t.scale.y !== 0 ? ry / t.scale.y : 0
|
|
945
|
+
};
|
|
946
|
+
},
|
|
947
|
+
/** Returns a copy of the transform. */
|
|
948
|
+
clone(t) {
|
|
949
|
+
return {
|
|
950
|
+
position: {
|
|
951
|
+
x: t.position.x,
|
|
952
|
+
y: t.position.y
|
|
953
|
+
},
|
|
954
|
+
rotation: t.rotation,
|
|
955
|
+
scale: {
|
|
956
|
+
x: t.scale.x,
|
|
957
|
+
y: t.scale.y
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
}
|
|
1093
961
|
};
|
|
962
|
+
//#endregion
|
|
963
|
+
//#region src/vec2.ts
|
|
964
|
+
/** Creates a Vec2 plain object. */
|
|
965
|
+
function vec2(x = 0, y = 0) {
|
|
966
|
+
return {
|
|
967
|
+
x,
|
|
968
|
+
y
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const Vec2 = {
|
|
972
|
+
/** Returns { 0, 0 }. */
|
|
973
|
+
zero() {
|
|
974
|
+
return {
|
|
975
|
+
x: 0,
|
|
976
|
+
y: 0
|
|
977
|
+
};
|
|
978
|
+
},
|
|
979
|
+
/** Returns { 1, 1 }. */
|
|
980
|
+
one() {
|
|
981
|
+
return {
|
|
982
|
+
x: 1,
|
|
983
|
+
y: 1
|
|
984
|
+
};
|
|
985
|
+
},
|
|
986
|
+
/** Returns { 0, -1 }. */
|
|
987
|
+
up() {
|
|
988
|
+
return {
|
|
989
|
+
x: 0,
|
|
990
|
+
y: -1
|
|
991
|
+
};
|
|
992
|
+
},
|
|
993
|
+
/** Returns { 0, 1 }. */
|
|
994
|
+
down() {
|
|
995
|
+
return {
|
|
996
|
+
x: 0,
|
|
997
|
+
y: 1
|
|
998
|
+
};
|
|
999
|
+
},
|
|
1000
|
+
/** Returns { -1, 0 }. */
|
|
1001
|
+
left() {
|
|
1002
|
+
return {
|
|
1003
|
+
x: -1,
|
|
1004
|
+
y: 0
|
|
1005
|
+
};
|
|
1006
|
+
},
|
|
1007
|
+
/** Returns { 1, 0 }. */
|
|
1008
|
+
right() {
|
|
1009
|
+
return {
|
|
1010
|
+
x: 1,
|
|
1011
|
+
y: 0
|
|
1012
|
+
};
|
|
1013
|
+
},
|
|
1014
|
+
/** Component-wise addition. Writes into out if provided. */
|
|
1015
|
+
add(a, b, out) {
|
|
1016
|
+
if (out) {
|
|
1017
|
+
out.x = a.x + b.x;
|
|
1018
|
+
out.y = a.y + b.y;
|
|
1019
|
+
return out;
|
|
1020
|
+
}
|
|
1021
|
+
return {
|
|
1022
|
+
x: a.x + b.x,
|
|
1023
|
+
y: a.y + b.y
|
|
1024
|
+
};
|
|
1025
|
+
},
|
|
1026
|
+
/** Component-wise subtraction a - b. */
|
|
1027
|
+
sub(a, b, out) {
|
|
1028
|
+
if (out) {
|
|
1029
|
+
out.x = a.x - b.x;
|
|
1030
|
+
out.y = a.y - b.y;
|
|
1031
|
+
return out;
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
x: a.x - b.x,
|
|
1035
|
+
y: a.y - b.y
|
|
1036
|
+
};
|
|
1037
|
+
},
|
|
1038
|
+
/** Scales a vector by a scalar. */
|
|
1039
|
+
scale(v, s, out) {
|
|
1040
|
+
if (out) {
|
|
1041
|
+
out.x = v.x * s;
|
|
1042
|
+
out.y = v.y * s;
|
|
1043
|
+
return out;
|
|
1044
|
+
}
|
|
1045
|
+
return {
|
|
1046
|
+
x: v.x * s,
|
|
1047
|
+
y: v.y * s
|
|
1048
|
+
};
|
|
1049
|
+
},
|
|
1050
|
+
/** Returns a unit vector. Returns zero vector if length is 0. */
|
|
1051
|
+
normalize(v, out) {
|
|
1052
|
+
const len = Math.sqrt(v.x * v.x + v.y * v.y);
|
|
1053
|
+
if (len === 0) {
|
|
1054
|
+
if (out) {
|
|
1055
|
+
out.x = 0;
|
|
1056
|
+
out.y = 0;
|
|
1057
|
+
return out;
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
x: 0,
|
|
1061
|
+
y: 0
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
const inv = 1 / len;
|
|
1065
|
+
if (out) {
|
|
1066
|
+
out.x = v.x * inv;
|
|
1067
|
+
out.y = v.y * inv;
|
|
1068
|
+
return out;
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
x: v.x * inv,
|
|
1072
|
+
y: v.y * inv
|
|
1073
|
+
};
|
|
1074
|
+
},
|
|
1075
|
+
/** Zero-alloc; writes into `out`. Mandatory `out` so callers avoid temporaries in hot paths. */
|
|
1076
|
+
normalizeOut(dx, dy, speed, out) {
|
|
1077
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1078
|
+
if (len <= 1e-6) {
|
|
1079
|
+
out.x = 0;
|
|
1080
|
+
out.y = 0;
|
|
1081
|
+
return out;
|
|
1082
|
+
}
|
|
1083
|
+
const inv = speed / len;
|
|
1084
|
+
out.x = dx * inv;
|
|
1085
|
+
out.y = dy * inv;
|
|
1086
|
+
return out;
|
|
1087
|
+
},
|
|
1088
|
+
/** Dot product. */
|
|
1089
|
+
dot(a, b) {
|
|
1090
|
+
return a.x * b.x + a.y * b.y;
|
|
1091
|
+
},
|
|
1092
|
+
/** 2D cross product (z-component of the 3D cross). */
|
|
1093
|
+
cross(a, b) {
|
|
1094
|
+
return a.x * b.y - a.y * b.x;
|
|
1095
|
+
},
|
|
1096
|
+
/** Perpendicular: (-y, x). In this engine's y-down screen space this is a 90° clockwise turn,
|
|
1097
|
+
* matching the Skia rotation convention used by `@flare-engine/render`. Writes into out if
|
|
1098
|
+
* provided. */
|
|
1099
|
+
perp(v, out) {
|
|
1100
|
+
const x = -v.y;
|
|
1101
|
+
const y = v.x;
|
|
1102
|
+
if (out) {
|
|
1103
|
+
out.x = x;
|
|
1104
|
+
out.y = y;
|
|
1105
|
+
return out;
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
x,
|
|
1109
|
+
y
|
|
1110
|
+
};
|
|
1111
|
+
},
|
|
1112
|
+
/** Scalar × vector cross product: s × v = (-s·y, s·x). Writes into out if provided. */
|
|
1113
|
+
crossSV(s, v, out) {
|
|
1114
|
+
const x = -s * v.y;
|
|
1115
|
+
const y = s * v.x;
|
|
1116
|
+
if (out) {
|
|
1117
|
+
out.x = x;
|
|
1118
|
+
out.y = y;
|
|
1119
|
+
return out;
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
x,
|
|
1123
|
+
y
|
|
1124
|
+
};
|
|
1125
|
+
},
|
|
1126
|
+
/** Linearly interpolates between a and b (unclamped). */
|
|
1127
|
+
lerp(a, b, t, out) {
|
|
1128
|
+
const x = a.x + (b.x - a.x) * t;
|
|
1129
|
+
const y = a.y + (b.y - a.y) * t;
|
|
1130
|
+
if (out) {
|
|
1131
|
+
out.x = x;
|
|
1132
|
+
out.y = y;
|
|
1133
|
+
return out;
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
x,
|
|
1137
|
+
y
|
|
1138
|
+
};
|
|
1139
|
+
},
|
|
1140
|
+
/** Spherical linear interpolation for unit vectors. */
|
|
1141
|
+
slerp(a, b, t, out) {
|
|
1142
|
+
let d = a.x * b.x + a.y * b.y;
|
|
1143
|
+
d = d < -1 ? -1 : d > 1 ? 1 : d;
|
|
1144
|
+
const theta = Math.acos(d);
|
|
1145
|
+
if (theta < 1e-6) return Vec2.lerp(a, b, t, out);
|
|
1146
|
+
const sinTheta = Math.sin(theta);
|
|
1147
|
+
const wa = Math.sin((1 - t) * theta) / sinTheta;
|
|
1148
|
+
const wb = Math.sin(t * theta) / sinTheta;
|
|
1149
|
+
const x = a.x * wa + b.x * wb;
|
|
1150
|
+
const y = a.y * wa + b.y * wb;
|
|
1151
|
+
if (out) {
|
|
1152
|
+
out.x = x;
|
|
1153
|
+
out.y = y;
|
|
1154
|
+
return out;
|
|
1155
|
+
}
|
|
1156
|
+
return {
|
|
1157
|
+
x,
|
|
1158
|
+
y
|
|
1159
|
+
};
|
|
1160
|
+
},
|
|
1161
|
+
/** Euclidean distance between two points. */
|
|
1162
|
+
distance(a, b) {
|
|
1163
|
+
const dx = b.x - a.x;
|
|
1164
|
+
const dy = b.y - a.y;
|
|
1165
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
1166
|
+
},
|
|
1167
|
+
/** Squared distance (avoids sqrt). */
|
|
1168
|
+
distanceSq(a, b) {
|
|
1169
|
+
const dx = b.x - a.x;
|
|
1170
|
+
const dy = b.y - a.y;
|
|
1171
|
+
return dx * dx + dy * dy;
|
|
1172
|
+
},
|
|
1173
|
+
/** Length of a vector. */
|
|
1174
|
+
length(v) {
|
|
1175
|
+
return Math.sqrt(v.x * v.x + v.y * v.y);
|
|
1176
|
+
},
|
|
1177
|
+
/** Squared length (avoids sqrt). */
|
|
1178
|
+
lengthSq(v) {
|
|
1179
|
+
return v.x * v.x + v.y * v.y;
|
|
1180
|
+
},
|
|
1181
|
+
/** Angle of vector in radians (atan2). */
|
|
1182
|
+
angle(v) {
|
|
1183
|
+
return Math.atan2(v.y, v.x);
|
|
1184
|
+
},
|
|
1185
|
+
/** Rotates a vector by angle (radians) around origin. */
|
|
1186
|
+
rotate(v, angle, out) {
|
|
1187
|
+
const c = Math.cos(angle);
|
|
1188
|
+
const s = Math.sin(angle);
|
|
1189
|
+
const x = v.x * c - v.y * s;
|
|
1190
|
+
const y = v.x * s + v.y * c;
|
|
1191
|
+
if (out) {
|
|
1192
|
+
out.x = x;
|
|
1193
|
+
out.y = y;
|
|
1194
|
+
return out;
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
x,
|
|
1198
|
+
y
|
|
1199
|
+
};
|
|
1200
|
+
},
|
|
1201
|
+
/** Component-wise approximate equality. */
|
|
1202
|
+
equals(a, b, epsilon = 1e-6) {
|
|
1203
|
+
return approxEquals(a.x, b.x, epsilon) && approxEquals(a.y, b.y, epsilon);
|
|
1204
|
+
},
|
|
1205
|
+
/** Returns a copy of the vector. */
|
|
1206
|
+
clone(v) {
|
|
1207
|
+
return {
|
|
1208
|
+
x: v.x,
|
|
1209
|
+
y: v.y
|
|
1210
|
+
};
|
|
1211
|
+
},
|
|
1212
|
+
/** Clamps the length of a vector to maxLength. */
|
|
1213
|
+
clampLength(v, maxLength, out) {
|
|
1214
|
+
const lenSq = v.x * v.x + v.y * v.y;
|
|
1215
|
+
if (lenSq <= maxLength * maxLength) {
|
|
1216
|
+
if (out) {
|
|
1217
|
+
out.x = v.x;
|
|
1218
|
+
out.y = v.y;
|
|
1219
|
+
return out;
|
|
1220
|
+
}
|
|
1221
|
+
return {
|
|
1222
|
+
x: v.x,
|
|
1223
|
+
y: v.y
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
const inv = maxLength / Math.sqrt(lenSq);
|
|
1227
|
+
if (out) {
|
|
1228
|
+
out.x = v.x * inv;
|
|
1229
|
+
out.y = v.y * inv;
|
|
1230
|
+
return out;
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
x: v.x * inv,
|
|
1234
|
+
y: v.y * inv
|
|
1235
|
+
};
|
|
1236
|
+
},
|
|
1237
|
+
/** Zero-alloc homing steer: rotate velocity (vx,vy) toward direction (dx,dy) by at most maxStep
|
|
1238
|
+
* radians, preserving speed; writes into out. Mandatory out. Byte-identical to hand-rolled
|
|
1239
|
+
* homing-bullet steering (Math.hypot speed + atan2 + branch-form turn + cos/sin * speed). */
|
|
1240
|
+
turnVelocityToward(vx, vy, dx, dy, maxStep, out) {
|
|
1241
|
+
const speed = Math.hypot(vx, vy);
|
|
1242
|
+
const newAng = rotateTowards(Math.atan2(vy, vx), Math.atan2(dy, dx), maxStep);
|
|
1243
|
+
out.x = Math.cos(newAng) * speed;
|
|
1244
|
+
out.y = Math.sin(newAng) * speed;
|
|
1245
|
+
return out;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
//#endregion
|
|
1249
|
+
export { Circle, Color, Mat3, Rect, SeededRNG, Transform2D, Vec2, advanceWindowedStreak, angleDelta, approxEquals, cappedBonus, choose, circleVsCircle, clamp, degToRad, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, hashU32, inverseLerp, lerp, lineVsCircle, lineVsRect, linear, map, pingPong, pointInCircle, pointInRect, radToDeg, randomFloat, randomInt, randomPointInCircle, randomVec2InRect, rayVsRect, rectVsCircle, rectVsRect, remainingInWindow, rotateTowards, seededIndex, shuffle, sign, smoothStep, springIn, springInOut, springOut, step, vec2, weightedIndex, weightedIndexBy, weightedPick, windowExpired, wrap };
|
|
1250
|
+
|
|
1094
1251
|
//# sourceMappingURL=index.mjs.map
|