@flare-engine/math 0.2.1 → 0.3.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/dist/index.mjs CHANGED
@@ -1,1094 +1,1251 @@
1
- // src/scalar.ts
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
- return value < min ? min : value > max ? max : value;
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
- return a + (b - a) * t;
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
- const range = b - a;
10
- if (range === 0) return 0;
11
- return (value - a) / range;
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
- return lerp(outMin, outMax, inverseLerp(inMin, inMax, value));
44
+ return lerp(outMin, outMax, inverseLerp(inMin, inMax, value));
15
45
  }
16
- var DEG_TO_RAD = Math.PI / 180;
17
- var RAD_TO_DEG = 180 / Math.PI;
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
- return degrees * DEG_TO_RAD;
50
+ return degrees * DEG_TO_RAD;
20
51
  }
52
+ /** Converts radians to degrees. */
21
53
  function radToDeg(radians) {
22
- return radians * RAD_TO_DEG;
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
- return Math.abs(a - b) <= epsilon;
58
+ return Math.abs(a - b) <= epsilon;
26
59
  }
60
+ /** Wraps value into [min, max) range. */
27
61
  function wrap(value, min, max) {
28
- const range = max - min;
29
- if (range === 0) return min;
30
- return min + ((value - min) % range + range) % range;
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
- if (length === 0) return 0;
34
- const tt = wrap(t, 0, length * 2);
35
- return length - Math.abs(tt - length);
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
- const range = edge1 - edge0;
39
- if (range === 0) return x >= edge1 ? 1 : 0;
40
- const t = clamp((x - edge0) / range, 0, 1);
41
- return t * t * (3 - 2 * t);
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
- if (Number.isNaN(value)) return 0;
45
- return value > 0 ? 1 : value < 0 ? -1 : 0;
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
- let diff = a - b;
49
- if (diff > Math.PI) diff -= 2 * Math.PI;
50
- else if (diff < -Math.PI) diff += 2 * Math.PI;
51
- return diff;
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
- const diff = angleDelta(target, cur);
55
- const turn = diff > maxStep ? maxStep : diff < -maxStep ? -maxStep : diff;
56
- return cur + turn;
57
- }
58
-
59
- // src/vec2.ts
60
- function vec2(x = 0, y = 0) {
61
- return { x, y };
62
- }
63
- var Vec2 = {
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
- return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
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
- return c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
528
- }
529
- var Color = {
530
- WHITE: { r: 1, g: 1, b: 1, a: 1 },
531
- BLACK: { r: 0, g: 0, b: 0, a: 1 },
532
- TRANSPARENT: { r: 0, g: 0, b: 0, a: 0 },
533
- /** Creates a Color from normalized 0-1 components. */
534
- create(r = 0, g = 0, b = 0, a = 1) {
535
- return { r, g, b, a };
536
- },
537
- /** Parses a hex string (#RGB, #RGBA, #RRGGBB, #RRGGBBAA). */
538
- fromHex(hex) {
539
- let h = hex.startsWith("#") ? hex.slice(1) : hex;
540
- if (h.length === 3) {
541
- h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
542
- } else if (h.length === 4) {
543
- h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2] + h[3] + h[3];
544
- }
545
- const r = Number.parseInt(h.slice(0, 2), 16) / 255;
546
- const g = Number.parseInt(h.slice(2, 4), 16) / 255;
547
- const b = Number.parseInt(h.slice(4, 6), 16) / 255;
548
- const a = h.length >= 8 ? Number.parseInt(h.slice(6, 8), 16) / 255 : 1;
549
- return { r, g, b, a };
550
- },
551
- /** Converts to hex string #RRGGBB or #RRGGBBAA if alpha < 1. */
552
- toHex(c) {
553
- const r = Math.round(clamp(c.r, 0, 1) * 255).toString(16).padStart(2, "0");
554
- const g = Math.round(clamp(c.g, 0, 1) * 255).toString(16).padStart(2, "0");
555
- const b = Math.round(clamp(c.b, 0, 1) * 255).toString(16).padStart(2, "0");
556
- if (c.a < 1) {
557
- const a = Math.round(clamp(c.a, 0, 1) * 255).toString(16).padStart(2, "0");
558
- return `#${r}${g}${b}${a}`;
559
- }
560
- return `#${r}${g}${b}`;
561
- },
562
- /** Linearly interpolates between two colors. */
563
- lerp(a, b, t) {
564
- return {
565
- r: a.r + (b.r - a.r) * t,
566
- g: a.g + (b.g - a.g) * t,
567
- b: a.b + (b.b - a.b) * t,
568
- a: a.a + (b.a - a.a) * t
569
- };
570
- },
571
- /** Component-wise multiplication. */
572
- multiply(a, b) {
573
- return {
574
- r: a.r * b.r,
575
- g: a.g * b.g,
576
- b: a.b * b.b,
577
- a: a.a * b.a
578
- };
579
- },
580
- /**
581
- * Converts to a Skia-compatible color number (0xAARRGGBB).
582
- * Compatible with @shopify/react-native-skia Skia.Color() format.
583
- */
584
- toSkColor(c) {
585
- const r = Math.round(clamp(c.r, 0, 1) * 255) & 255;
586
- const g = Math.round(clamp(c.g, 0, 1) * 255) & 255;
587
- const b = Math.round(clamp(c.b, 0, 1) * 255) & 255;
588
- const a = Math.round(clamp(c.a, 0, 1) * 255) & 255;
589
- return (a << 24 | r << 16 | g << 8 | b) >>> 0;
590
- },
591
- /** Converts sRGB Color to OKLab. Alpha is dropped. */
592
- toOklab(c) {
593
- const rl = srgbToLinear(c.r);
594
- const gl = srgbToLinear(c.g);
595
- const bl = srgbToLinear(c.b);
596
- const l = 0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl;
597
- const m = 0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl;
598
- const s = 0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl;
599
- const l_ = Math.cbrt(l);
600
- const m_ = Math.cbrt(m);
601
- const s_ = Math.cbrt(s);
602
- return {
603
- L: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
604
- a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
605
- b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
606
- };
607
- },
608
- /**
609
- * Converts OKLab to sRGB Color. Out-of-gamut results are clamped to [0,1];
610
- * round-trips are exact only for in-gamut colors.
611
- */
612
- fromOklab(lab, alpha = 1) {
613
- const l_ = lab.L + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
614
- const m_ = lab.L - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
615
- const s_ = lab.L - 0.0894841775 * lab.a - 1.291485548 * lab.b;
616
- const l = l_ ** 3;
617
- const m = m_ ** 3;
618
- const s = s_ ** 3;
619
- const rl = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
620
- const gl = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
621
- const bl = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
622
- return {
623
- r: clamp(linearToSrgb(rl), 0, 1),
624
- g: clamp(linearToSrgb(gl), 0, 1),
625
- b: clamp(linearToSrgb(bl), 0, 1),
626
- a: alpha
627
- };
628
- },
629
- /** Converts sRGB Color to OKLCH (polar OKLab). Achromatic colors get h = 0. */
630
- toOklch(c) {
631
- const lab = Color.toOklab(c);
632
- const C = Math.hypot(lab.a, lab.b);
633
- const h = C < 1e-7 ? 0 : (Math.atan2(lab.b, lab.a) * 180 / Math.PI + 360) % 360;
634
- return { L: lab.L, C, h };
635
- },
636
- /**
637
- * Converts OKLCH to sRGB Color. Hue is wrapped via ((h%360)+360)%360.
638
- * Out-of-gamut results are clamped; round-trips exact only in-gamut.
639
- */
640
- fromOklch(lch, alpha = 1) {
641
- const hWrapped = (lch.h % 360 + 360) % 360;
642
- const hRad = hWrapped * Math.PI / 180;
643
- return Color.fromOklab(
644
- { L: lch.L, a: lch.C * Math.cos(hRad), b: lch.C * Math.sin(hRad) },
645
- alpha
646
- );
647
- },
648
- /** Euclidean distance in OKLab space between two sRGB colors. */
649
- deltaEOk(a, b) {
650
- return Color.deltaEOkLab(Color.toOklab(a), Color.toOklab(b));
651
- },
652
- /** Euclidean distance between two OKLab values. */
653
- deltaEOkLab(a, b) {
654
- return Math.hypot(a.L - b.L, a.a - b.a, a.b - b.b);
655
- },
656
- /** WCAG 2.x relative luminance. White = 1, black = 0. */
657
- relativeLuminance(c) {
658
- return 0.2126 * srgbToLinear(c.r) + 0.7152 * srgbToLinear(c.g) + 0.0722 * srgbToLinear(c.b);
659
- },
660
- /** WCAG 2.x contrast ratio. Order-independent; range [1, 21]. */
661
- contrastRatio(fg, bg) {
662
- const l1 = Color.relativeLuminance(fg);
663
- const l2 = Color.relativeLuminance(bg);
664
- const lMax = Math.max(l1, l2);
665
- const lMin = Math.min(l1, l2);
666
- return (lMax + 0.05) / (lMin + 0.05);
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
- // src/easings.ts
671
- var PI = Math.PI;
672
- var HALF_PI = PI / 2;
673
- var TAU = PI * 2;
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
- return t;
278
+ return t;
676
279
  }
677
280
  function step(t) {
678
- return t < 1 ? 0 : 1;
281
+ return t < 1 ? 0 : 1;
679
282
  }
680
283
  function easeInQuad(t) {
681
- return t * t;
284
+ return t * t;
682
285
  }
683
286
  function easeOutQuad(t) {
684
- return t * (2 - t);
287
+ return t * (2 - t);
685
288
  }
686
289
  function easeInOutQuad(t) {
687
- return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
290
+ return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
688
291
  }
689
292
  function easeInCubic(t) {
690
- return t * t * t;
293
+ return t * t * t;
691
294
  }
692
295
  function easeOutCubic(t) {
693
- const t1 = t - 1;
694
- return t1 * t1 * t1 + 1;
296
+ const t1 = t - 1;
297
+ return t1 * t1 * t1 + 1;
695
298
  }
696
299
  function easeInOutCubic(t) {
697
- return t < 0.5 ? 4 * t * t * t : 1 + (t - 1) * (2 * t - 2) * (2 * t - 2);
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
- return t * t * t * t;
303
+ return t * t * t * t;
701
304
  }
702
305
  function easeOutQuart(t) {
703
- const t1 = t - 1;
704
- return 1 - t1 * t1 * t1 * t1;
306
+ const t1 = t - 1;
307
+ return 1 - t1 * t1 * t1 * t1;
705
308
  }
706
309
  function easeInOutQuart(t) {
707
- const t1 = t - 1;
708
- return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * t1 * t1 * t1 * t1;
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
- return t * t * t * t * t;
314
+ return t * t * t * t * t;
712
315
  }
713
316
  function easeOutQuint(t) {
714
- const t1 = t - 1;
715
- return 1 + t1 * t1 * t1 * t1 * t1;
317
+ const t1 = t - 1;
318
+ return 1 + t1 * t1 * t1 * t1 * t1;
716
319
  }
717
320
  function easeInOutQuint(t) {
718
- const t1 = t - 1;
719
- return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * t1 * t1 * t1 * t1 * t1;
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
- return 1 - Math.cos(t * HALF_PI);
325
+ return 1 - Math.cos(t * HALF_PI);
723
326
  }
724
327
  function easeOutSine(t) {
725
- return Math.sin(t * HALF_PI);
328
+ return Math.sin(t * HALF_PI);
726
329
  }
727
330
  function easeInOutSine(t) {
728
- return 0.5 * (1 - Math.cos(PI * t));
331
+ return .5 * (1 - Math.cos(PI * t));
729
332
  }
730
333
  function easeInExpo(t) {
731
- return t === 0 ? 0 : 2 ** (10 * (t - 1));
334
+ return t === 0 ? 0 : 2 ** (10 * (t - 1));
732
335
  }
733
336
  function easeOutExpo(t) {
734
- return t === 1 ? 1 : 1 - 2 ** (-10 * t);
337
+ return t === 1 ? 1 : 1 - 2 ** (-10 * t);
735
338
  }
736
339
  function easeInOutExpo(t) {
737
- if (t === 0) return 0;
738
- if (t === 1) return 1;
739
- return t < 0.5 ? 0.5 * 2 ** (20 * t - 10) : 1 - 0.5 * 2 ** (-20 * t + 10);
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
- return 1 - Math.sqrt(1 - t * t);
345
+ return 1 - Math.sqrt(1 - t * t);
743
346
  }
744
347
  function easeOutCirc(t) {
745
- const t1 = t - 1;
746
- return Math.sqrt(1 - t1 * t1);
348
+ const t1 = t - 1;
349
+ return Math.sqrt(1 - t1 * t1);
747
350
  }
748
351
  function easeInOutCirc(t) {
749
- return t < 0.5 ? 0.5 * (1 - Math.sqrt(1 - 4 * t * t)) : 0.5 * (Math.sqrt(1 - (2 * t - 2) * (2 * t - 2)) + 1);
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
- var S = 1.70158;
752
- var S2 = S * 1.525;
354
+ const S = 1.70158;
355
+ const S2 = S * 1.525;
753
356
  function easeInBack(t) {
754
- return t * t * ((S + 1) * t - S);
357
+ return t * t * (2.70158 * t - S);
755
358
  }
756
359
  function easeOutBack(t) {
757
- const t1 = t - 1;
758
- return t1 * t1 * ((S + 1) * t1 + S) + 1;
360
+ const t1 = t - 1;
361
+ return t1 * t1 * (2.70158 * t1 + S) + 1;
759
362
  }
760
363
  function easeInOutBack(t) {
761
- if (t < 0.5) {
762
- const t22 = 2 * t;
763
- return 0.5 * (t22 * t22 * ((S2 + 1) * t22 - S2));
764
- }
765
- const t2 = 2 * t - 2;
766
- return 0.5 * (t2 * t2 * ((S2 + 1) * t2 + S2) + 2);
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
- if (t === 0 || t === 1) return t;
770
- return -(2 ** (10 * t - 10)) * Math.sin((t * 10 - 10.75) * (TAU / 3));
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
- if (t === 0 || t === 1) return t;
774
- return 2 ** (-10 * t) * Math.sin((t * 10 - 0.75) * (TAU / 3)) + 1;
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
- if (t === 0 || t === 1) return t;
778
- const p = TAU / 4.5;
779
- return t < 0.5 ? -0.5 * 2 ** (20 * t - 10) * Math.sin((20 * t - 11.125) * p) : 0.5 * 2 ** (-20 * t + 10) * Math.sin((20 * t - 11.125) * p) + 1;
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
- if (t < 1 / 2.75) {
783
- return 7.5625 * t * t;
784
- }
785
- if (t < 2 / 2.75) {
786
- const t22 = t - 1.5 / 2.75;
787
- return 7.5625 * t22 * t22 + 0.75;
788
- }
789
- if (t < 2.5 / 2.75) {
790
- const t22 = t - 2.25 / 2.75;
791
- return 7.5625 * t22 * t22 + 0.9375;
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
- return 1 - easeOutBounce(1 - t);
398
+ return 1 - easeOutBounce(1 - t);
798
399
  }
799
400
  function easeInOutBounce(t) {
800
- return t < 0.5 ? 0.5 * easeInBounce(t * 2) : 0.5 * easeOutBounce(t * 2 - 1) + 0.5;
401
+ return t < .5 ? .5 * easeInBounce(t * 2) : .5 * easeOutBounce(t * 2 - 1) + .5;
801
402
  }
802
403
  function springIn(t) {
803
- return t * t * ((1.70158 + 1) * t - 1.70158);
404
+ return t * t * (2.70158 * t - 1.70158);
804
405
  }
805
406
  function springOut(t) {
806
- const t1 = t - 1;
807
- return t1 * t1 * ((1.70158 + 1) * t1 + 1.70158) + 1;
407
+ const t1 = t - 1;
408
+ return t1 * t1 * (2.70158 * t1 + 1.70158) + 1;
808
409
  }
809
410
  function springInOut(t) {
810
- if (t < 0.5) {
811
- const t22 = 2 * t;
812
- return 0.5 * t22 * t22 * ((S2 + 1) * t22 - S2);
813
- }
814
- const t2 = 2 * t - 2;
815
- return 0.5 * (t2 * t2 * ((S2 + 1) * t2 + S2) + 2);
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
- // src/random.ts
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
- state;
821
- constructor(seed) {
822
- this.state = seed | 0;
823
- }
824
- /** Returns a float in [0, 1). */
825
- next() {
826
- this.state = this.state + 1831565813 | 0;
827
- let t = Math.imul(this.state ^ this.state >>> 15, 1 | this.state);
828
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
829
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
830
- }
831
- /** Returns a float in [min, max). */
832
- float(min, max) {
833
- return min + this.next() * (max - min);
834
- }
835
- /** Returns an integer in [min, max] (inclusive). */
836
- int(min, max) {
837
- return Math.floor(this.float(min, max + 1));
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
- return min + Math.random() * (max - min);
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
- return Math.floor(randomFloat(min, max + 1));
677
+ return Math.floor(randomFloat(min, max + 1));
845
678
  }
679
+ /** Returns a random Vec2 within a rect. */
846
680
  function randomVec2InRect(r) {
847
- return {
848
- x: r.x + Math.random() * r.width,
849
- y: r.y + Math.random() * r.height
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
- const r = radius * Math.sqrt(Math.random());
854
- const angle = Math.random() * Math.PI * 2;
855
- return {
856
- x: cx + r * Math.cos(angle),
857
- y: cy + r * Math.sin(angle)
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
- for (let i = array.length - 1; i > 0; i--) {
862
- const j = Math.floor(Math.random() * (i + 1));
863
- const tmp = array[i];
864
- array[i] = array[j];
865
- array[j] = tmp;
866
- }
867
- return array;
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
- if (array.length === 0) return void 0;
871
- return array[Math.floor(Math.random() * array.length)];
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
- let total = 0;
875
- for (let i = 0; i < weights.length; i++) total += weights[i] ?? 0;
876
- if (total <= 0) return 0;
877
- const target = roll01 * total;
878
- let acc = 0;
879
- for (let i = 0; i < weights.length; i++) {
880
- acc += weights[i] ?? 0;
881
- if (target < acc) return i;
882
- }
883
- return weights.length - 1;
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
- let total = 0;
887
- for (let i = 0; i < items.length; i++) total += weight(items[i]);
888
- if (total <= 0) return 0;
889
- const target = roll01 * total;
890
- let acc = 0;
891
- for (let i = 0; i < items.length; i++) {
892
- acc += weight(items[i]);
893
- if (target < acc) return i;
894
- }
895
- return items.length - 1;
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
- if (items.length === 0) return void 0;
899
- return items[weightedIndexBy(items, weight, roll01)];
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
- let h = (seed | 0) >>> 0;
903
- h = Math.imul(h ^ h >>> 16, 73244475) >>> 0;
904
- h = Math.imul(h ^ h >>> 16, 73244475) >>> 0;
905
- return (h ^ h >>> 16) >>> 0;
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
- if (count <= 1) return 0;
909
- return hashU32(seed) % count;
910
- }
911
-
912
- // src/reward.ts
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
- if (pay <= 0) return 0;
915
- return Math.floor(pay * Math.min(frac, capFrac));
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
- if (lastT >= 0 && elapsed - lastT <= windowSec) return prev + 1;
919
- return 1;
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
- return lastT >= 0 && elapsed - lastT > windowSec;
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
- return Math.max(0, capPerWindow - awarded);
926
- }
927
-
928
- // src/intersection.ts
929
- function pointInRect(p, r) {
930
- return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
931
- }
932
- function pointInCircle(p, c) {
933
- const dx = p.x - c.x;
934
- const dy = p.y - c.y;
935
- return dx * dx + dy * dy <= c.radius * c.radius;
936
- }
937
- function rectVsRect(a, b) {
938
- 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;
939
- }
940
- function rectVsCircle(r, c) {
941
- const closestX = c.x < r.x ? r.x : c.x > r.x + r.width ? r.x + r.width : c.x;
942
- const closestY = c.y < r.y ? r.y : c.y > r.y + r.height ? r.y + r.height : c.y;
943
- const dx = c.x - closestX;
944
- const dy = c.y - closestY;
945
- return dx * dx + dy * dy <= c.radius * c.radius;
946
- }
947
- function circleVsCircle(a, b) {
948
- const dx = b.x - a.x;
949
- const dy = b.y - a.y;
950
- const radSum = a.radius + b.radius;
951
- return dx * dx + dy * dy < radSum * radSum;
952
- }
953
- function lineVsRect(x1, y1, x2, y2, r) {
954
- const dx = x2 - x1;
955
- const dy = y2 - y1;
956
- let tMin = 0;
957
- let tMax = 1;
958
- const edges = [
959
- { p: -dx, q: x1 - r.x },
960
- { p: dx, q: r.x + r.width - x1 },
961
- { p: -dy, q: y1 - r.y },
962
- { p: dy, q: r.y + r.height - y1 }
963
- ];
964
- for (const { p, q } of edges) {
965
- if (Math.abs(p) < 1e-12) {
966
- if (q < 0) return false;
967
- } else {
968
- const t = q / p;
969
- if (p < 0) {
970
- if (t > tMax) return false;
971
- if (t > tMin) tMin = t;
972
- } else {
973
- if (t < tMin) return false;
974
- if (t < tMax) tMax = t;
975
- }
976
- }
977
- }
978
- return tMin <= tMax;
979
- }
980
- function lineVsCircle(x1, y1, x2, y2, c) {
981
- const dx = x2 - x1;
982
- const dy = y2 - y1;
983
- const fx = x1 - c.x;
984
- const fy = y1 - c.y;
985
- const a = dx * dx + dy * dy;
986
- const b = 2 * (fx * dx + fy * dy);
987
- const cc = fx * fx + fy * fy - c.radius * c.radius;
988
- if (a < 1e-12) {
989
- return cc <= 0;
990
- }
991
- let discriminant = b * b - 4 * a * cc;
992
- if (discriminant < 0) return false;
993
- discriminant = Math.sqrt(discriminant);
994
- const t1 = (-b - discriminant) / (2 * a);
995
- const t2 = (-b + discriminant) / (2 * a);
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