@flare-engine/math 0.3.1 → 0.4.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/README.md +56 -1
- package/dist/index.d.mts +96 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +96 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +267 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +256 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -271,6 +271,141 @@ const Color = {
|
|
|
271
271
|
}
|
|
272
272
|
};
|
|
273
273
|
//#endregion
|
|
274
|
+
//#region src/curves.ts
|
|
275
|
+
/**
|
|
276
|
+
* Parametric curves — quadratic/cubic Bézier and Catmull-Rom — plus approximate
|
|
277
|
+
* arc length for each.
|
|
278
|
+
*
|
|
279
|
+
* Every sampler comes in two forms, per the repo's hot-path convention: an
|
|
280
|
+
* allocating one returning a fresh `Vec2`, and an `…Into` one writing to a
|
|
281
|
+
* caller-owned buffer. Sampling a path each frame is exactly the case the
|
|
282
|
+
* `…Into` forms exist for.
|
|
283
|
+
*
|
|
284
|
+
* Control points are passed as loose numbers rather than `Vec2` objects so a
|
|
285
|
+
* caller holding coordinates in a `Float32Array` — the documented storage for
|
|
286
|
+
* bulk numeric data — can sample without building temporaries first.
|
|
287
|
+
*/
|
|
288
|
+
/** Quadratic Bézier at `t`, writing into `out`. `t` is clamped to [0, 1]. */
|
|
289
|
+
function bezier2Into(p0x, p0y, p1x, p1y, p2x, p2y, t, out) {
|
|
290
|
+
const u = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
291
|
+
const v = 1 - u;
|
|
292
|
+
const a = v * v;
|
|
293
|
+
const b = 2 * v * u;
|
|
294
|
+
const c = u * u;
|
|
295
|
+
out.x = a * p0x + b * p1x + c * p2x;
|
|
296
|
+
out.y = a * p0y + b * p1y + c * p2y;
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
/** Quadratic Bézier at `t`. Allocates — see `bezier2Into` for the hot path. */
|
|
300
|
+
function bezier2(p0x, p0y, p1x, p1y, p2x, p2y, t) {
|
|
301
|
+
return bezier2Into(p0x, p0y, p1x, p1y, p2x, p2y, t, {
|
|
302
|
+
x: 0,
|
|
303
|
+
y: 0
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/** Cubic Bézier at `t`, writing into `out`. `t` is clamped to [0, 1]. */
|
|
307
|
+
function bezier3Into(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t, out) {
|
|
308
|
+
const u = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
309
|
+
const v = 1 - u;
|
|
310
|
+
const vv = v * v;
|
|
311
|
+
const uu = u * u;
|
|
312
|
+
const a = vv * v;
|
|
313
|
+
const b = 3 * vv * u;
|
|
314
|
+
const c = 3 * v * uu;
|
|
315
|
+
const d = uu * u;
|
|
316
|
+
out.x = a * p0x + b * p1x + c * p2x + d * p3x;
|
|
317
|
+
out.y = a * p0y + b * p1y + c * p2y + d * p3y;
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
/** Cubic Bézier at `t`. Allocates — see `bezier3Into` for the hot path. */
|
|
321
|
+
function bezier3(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t) {
|
|
322
|
+
return bezier3Into(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t, {
|
|
323
|
+
x: 0,
|
|
324
|
+
y: 0
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Centripetal-free (uniform) Catmull-Rom spline at `t`, writing into `out`.
|
|
329
|
+
*
|
|
330
|
+
* The curve passes **through** `p1` and `p2`; `p0` and `p3` are the neighbouring
|
|
331
|
+
* control points that set the tangents, and `t` runs from `p1` (0) to `p2` (1).
|
|
332
|
+
* That interpolating property is the reason to reach for this over a Bézier when
|
|
333
|
+
* a path must hit its waypoints exactly.
|
|
334
|
+
*/
|
|
335
|
+
function catmullRomInto(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t, out) {
|
|
336
|
+
const u = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
337
|
+
const uu = u * u;
|
|
338
|
+
const uuu = uu * u;
|
|
339
|
+
out.x = .5 * (2 * p1x + (-p0x + p2x) * u + (2 * p0x - 5 * p1x + 4 * p2x - p3x) * uu + (-p0x + 3 * p1x - 3 * p2x + p3x) * uuu);
|
|
340
|
+
out.y = .5 * (2 * p1y + (-p0y + p2y) * u + (2 * p0y - 5 * p1y + 4 * p2y - p3y) * uu + (-p0y + 3 * p1y - 3 * p2y + p3y) * uuu);
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
/** Catmull-Rom at `t`. Allocates — see `catmullRomInto` for the hot path. */
|
|
344
|
+
function catmullRom(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t) {
|
|
345
|
+
return catmullRomInto(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t, {
|
|
346
|
+
x: 0,
|
|
347
|
+
y: 0
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/** Scratch buffer for the length helpers — they are not reentrant, which is
|
|
351
|
+
* fine because they do not call user code. */
|
|
352
|
+
const _scratch = {
|
|
353
|
+
x: 0,
|
|
354
|
+
y: 0
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* Approximate arc length by sampling `segments` chords.
|
|
358
|
+
*
|
|
359
|
+
* Always an **underestimate**: a chord is the shortest path between its
|
|
360
|
+
* endpoints, so a polyline through the curve is never longer than the curve.
|
|
361
|
+
* Error falls roughly with the square of `segments`; 16 is a reasonable default
|
|
362
|
+
* for UI paths, more for anything a camera follows closely.
|
|
363
|
+
*/
|
|
364
|
+
function bezier2Length(p0x, p0y, p1x, p1y, p2x, p2y, segments = 16) {
|
|
365
|
+
const n = segments < 1 ? 1 : segments | 0;
|
|
366
|
+
let total = 0;
|
|
367
|
+
let px = p0x;
|
|
368
|
+
let py = p0y;
|
|
369
|
+
for (let i = 1; i <= n; i++) {
|
|
370
|
+
bezier2Into(p0x, p0y, p1x, p1y, p2x, p2y, i / n, _scratch);
|
|
371
|
+
total += Math.hypot(_scratch.x - px, _scratch.y - py);
|
|
372
|
+
px = _scratch.x;
|
|
373
|
+
py = _scratch.y;
|
|
374
|
+
}
|
|
375
|
+
return total;
|
|
376
|
+
}
|
|
377
|
+
/** Approximate arc length of a cubic Bézier. See `bezier2Length`. */
|
|
378
|
+
function bezier3Length(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, segments = 16) {
|
|
379
|
+
const n = segments < 1 ? 1 : segments | 0;
|
|
380
|
+
let total = 0;
|
|
381
|
+
let px = p0x;
|
|
382
|
+
let py = p0y;
|
|
383
|
+
for (let i = 1; i <= n; i++) {
|
|
384
|
+
bezier3Into(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, i / n, _scratch);
|
|
385
|
+
total += Math.hypot(_scratch.x - px, _scratch.y - py);
|
|
386
|
+
px = _scratch.x;
|
|
387
|
+
py = _scratch.y;
|
|
388
|
+
}
|
|
389
|
+
return total;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Approximate arc length of the Catmull-Rom span between `p1` and `p2`.
|
|
393
|
+
* See `bezier2Length` for the accuracy note.
|
|
394
|
+
*/
|
|
395
|
+
function catmullRomLength(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, segments = 16) {
|
|
396
|
+
const n = segments < 1 ? 1 : segments | 0;
|
|
397
|
+
let total = 0;
|
|
398
|
+
let px = p1x;
|
|
399
|
+
let py = p1y;
|
|
400
|
+
for (let i = 1; i <= n; i++) {
|
|
401
|
+
catmullRomInto(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, i / n, _scratch);
|
|
402
|
+
total += Math.hypot(_scratch.x - px, _scratch.y - py);
|
|
403
|
+
px = _scratch.x;
|
|
404
|
+
py = _scratch.y;
|
|
405
|
+
}
|
|
406
|
+
return total;
|
|
407
|
+
}
|
|
408
|
+
//#endregion
|
|
274
409
|
//#region src/easings.ts
|
|
275
410
|
const PI = Math.PI;
|
|
276
411
|
const HALF_PI = PI / 2;
|
|
@@ -518,6 +653,102 @@ function rayVsRect(originX, originY, dirX, dirY, r) {
|
|
|
518
653
|
if (tMax < 0 || tMin > tMax) return null;
|
|
519
654
|
return tMin >= 0 ? tMin : tMax;
|
|
520
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Is `p` on the segment `a`→`b`? Assumes neither is degenerate.
|
|
658
|
+
*/
|
|
659
|
+
function pointOnSegment(px, py, ax, ay, bx, by) {
|
|
660
|
+
if ((px - ax) * (by - ay) - (py - ay) * (bx - ax) !== 0) return false;
|
|
661
|
+
const dot = (px - ax) * (bx - ax) + (py - ay) * (by - ay);
|
|
662
|
+
if (dot < 0) return false;
|
|
663
|
+
return dot <= (bx - ax) * (bx - ax) + (by - ay) * (by - ay);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Do two segments intersect, including collinear overlap?
|
|
667
|
+
*
|
|
668
|
+
* The companion to `segmentIntersection`: that returns *where* two segments
|
|
669
|
+
* cross and is `null` when there is no single crossing point, which includes
|
|
670
|
+
* collinear segments lying on top of each other. Those genuinely do intersect,
|
|
671
|
+
* so a caller who only needs the yes/no answer asks here instead.
|
|
672
|
+
*/
|
|
673
|
+
function segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
|
674
|
+
const d1x = x2 - x1;
|
|
675
|
+
const d1y = y2 - y1;
|
|
676
|
+
const d2x = x4 - x3;
|
|
677
|
+
const d2y = y4 - y3;
|
|
678
|
+
const denom = d1x * d2y - d1y * d2x;
|
|
679
|
+
const ex = x3 - x1;
|
|
680
|
+
const ey = y3 - y1;
|
|
681
|
+
if (denom === 0) {
|
|
682
|
+
const aPoint = d1x === 0 && d1y === 0;
|
|
683
|
+
const bPoint = d2x === 0 && d2y === 0;
|
|
684
|
+
if (aPoint && bPoint) return x1 === x3 && y1 === y3;
|
|
685
|
+
if (aPoint) return pointOnSegment(x1, y1, x3, y3, x4, y4);
|
|
686
|
+
if (bPoint) return pointOnSegment(x3, y3, x1, y1, x2, y2);
|
|
687
|
+
if (ex * d1y - ey * d1x !== 0) return false;
|
|
688
|
+
const useX = Math.abs(d1x) >= Math.abs(d1y);
|
|
689
|
+
return (useX ? Math.min(x1, x2) : Math.min(y1, y2)) <= (useX ? Math.max(x3, x4) : Math.max(y3, y4)) && (useX ? Math.min(x3, x4) : Math.min(y3, y4)) <= (useX ? Math.max(x1, x2) : Math.max(y1, y2));
|
|
690
|
+
}
|
|
691
|
+
const t = (ex * d2y - ey * d2x) / denom;
|
|
692
|
+
const u = (ex * d1y - ey * d1x) / denom;
|
|
693
|
+
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Where two segments cross, written into `out`.
|
|
697
|
+
*
|
|
698
|
+
* Returns `null` when there is no **single** crossing point — segments that miss
|
|
699
|
+
* each other, are parallel, or are collinear. Collinear overlap is the case
|
|
700
|
+
* worth knowing about: the segments do intersect, at infinitely many points, so
|
|
701
|
+
* there is no one answer to write into `out`. Use `segmentsIntersect` when the
|
|
702
|
+
* yes/no answer is what you need.
|
|
703
|
+
*
|
|
704
|
+
* `out` is left untouched when the result is `null`.
|
|
705
|
+
*/
|
|
706
|
+
function segmentIntersectionInto(x1, y1, x2, y2, x3, y3, x4, y4, out) {
|
|
707
|
+
const d1x = x2 - x1;
|
|
708
|
+
const d1y = y2 - y1;
|
|
709
|
+
const d2x = x4 - x3;
|
|
710
|
+
const d2y = y4 - y3;
|
|
711
|
+
const denom = d1x * d2y - d1y * d2x;
|
|
712
|
+
if (denom === 0) {
|
|
713
|
+
const aPoint = d1x === 0 && d1y === 0;
|
|
714
|
+
const bPoint = d2x === 0 && d2y === 0;
|
|
715
|
+
if (aPoint && !bPoint && pointOnSegment(x1, y1, x3, y3, x4, y4)) {
|
|
716
|
+
out.x = x1;
|
|
717
|
+
out.y = y1;
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
if (bPoint && !aPoint && pointOnSegment(x3, y3, x1, y1, x2, y2)) {
|
|
721
|
+
out.x = x3;
|
|
722
|
+
out.y = y3;
|
|
723
|
+
return out;
|
|
724
|
+
}
|
|
725
|
+
if (aPoint && bPoint && x1 === x3 && y1 === y3) {
|
|
726
|
+
out.x = x1;
|
|
727
|
+
out.y = y1;
|
|
728
|
+
return out;
|
|
729
|
+
}
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const ex = x3 - x1;
|
|
733
|
+
const ey = y3 - y1;
|
|
734
|
+
const t = (ex * d2y - ey * d2x) / denom;
|
|
735
|
+
if (t < 0 || t > 1) return null;
|
|
736
|
+
const u = (ex * d1y - ey * d1x) / denom;
|
|
737
|
+
if (u < 0 || u > 1) return null;
|
|
738
|
+
out.x = x1 + t * d1x;
|
|
739
|
+
out.y = y1 + t * d1y;
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Where two segments cross. Allocates — see `segmentIntersectionInto` for the
|
|
744
|
+
* hot path.
|
|
745
|
+
*/
|
|
746
|
+
function segmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4) {
|
|
747
|
+
return segmentIntersectionInto(x1, y1, x2, y2, x3, y3, x4, y4, {
|
|
748
|
+
x: 0,
|
|
749
|
+
y: 0
|
|
750
|
+
});
|
|
751
|
+
}
|
|
521
752
|
//#endregion
|
|
522
753
|
//#region src/mat3.ts
|
|
523
754
|
const Mat3 = {
|
|
@@ -668,6 +899,25 @@ var SeededRNG = class {
|
|
|
668
899
|
int(min, max) {
|
|
669
900
|
return Math.floor(this.float(min, max + 1));
|
|
670
901
|
}
|
|
902
|
+
/**
|
|
903
|
+
* The generator's entire internal state, as a single int32.
|
|
904
|
+
*
|
|
905
|
+
* Enough to resume the exact sequence later, which is what makes a save file
|
|
906
|
+
* or a replay reproducible: store this alongside the save, hand it back to
|
|
907
|
+
* `setState`, and the next `next()` returns what it would have returned.
|
|
908
|
+
*/
|
|
909
|
+
getState() {
|
|
910
|
+
return this.state;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Restore a state previously read from `getState`.
|
|
914
|
+
*
|
|
915
|
+
* Coerced to int32 exactly as the constructor coerces a seed, so any number
|
|
916
|
+
* is accepted and a round-trip through JSON is safe.
|
|
917
|
+
*/
|
|
918
|
+
setState(state) {
|
|
919
|
+
this.state = state | 0;
|
|
920
|
+
}
|
|
671
921
|
};
|
|
672
922
|
/** Returns a random float in [min, max). */
|
|
673
923
|
function randomFloat(min, max) {
|
|
@@ -1073,15 +1323,16 @@ const Vec2 = {
|
|
|
1073
1323
|
y: v.y * inv
|
|
1074
1324
|
};
|
|
1075
1325
|
},
|
|
1076
|
-
/**
|
|
1077
|
-
|
|
1326
|
+
/** The direction of `(dx, dy)` scaled to `length`; writes zeros when `(dx, dy)` is shorter than
|
|
1327
|
+
* 1e-6. Zero-alloc, mandatory `out`; scalar components so hot paths need no temporary Vec2. */
|
|
1328
|
+
scaleToLengthInto(dx, dy, length, out) {
|
|
1078
1329
|
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1079
1330
|
if (len <= 1e-6) {
|
|
1080
1331
|
out.x = 0;
|
|
1081
1332
|
out.y = 0;
|
|
1082
1333
|
return out;
|
|
1083
1334
|
}
|
|
1084
|
-
const inv =
|
|
1335
|
+
const inv = length / len;
|
|
1085
1336
|
out.x = dx * inv;
|
|
1086
1337
|
out.y = dy * inv;
|
|
1087
1338
|
return out;
|
|
@@ -1238,7 +1489,7 @@ const Vec2 = {
|
|
|
1238
1489
|
/** Zero-alloc homing steer: rotate velocity (vx,vy) toward direction (dx,dy) by at most maxStep
|
|
1239
1490
|
* radians, preserving speed; writes into out. Mandatory out. Byte-identical to hand-rolled
|
|
1240
1491
|
* homing-bullet steering (Math.hypot speed + atan2 + branch-form turn + cos/sin * speed). */
|
|
1241
|
-
|
|
1492
|
+
turnVelocityTowardInto(vx, vy, dx, dy, maxStep, out) {
|
|
1242
1493
|
const speed = Math.hypot(vx, vy);
|
|
1243
1494
|
const newAng = rotateTowards(Math.atan2(vy, vx), Math.atan2(dy, dx), maxStep);
|
|
1244
1495
|
out.x = Math.cos(newAng) * speed;
|
|
@@ -1257,7 +1508,16 @@ exports.Vec2 = Vec2;
|
|
|
1257
1508
|
exports.advanceWindowedStreak = advanceWindowedStreak;
|
|
1258
1509
|
exports.angleDelta = angleDelta;
|
|
1259
1510
|
exports.approxEquals = approxEquals;
|
|
1511
|
+
exports.bezier2 = bezier2;
|
|
1512
|
+
exports.bezier2Into = bezier2Into;
|
|
1513
|
+
exports.bezier2Length = bezier2Length;
|
|
1514
|
+
exports.bezier3 = bezier3;
|
|
1515
|
+
exports.bezier3Into = bezier3Into;
|
|
1516
|
+
exports.bezier3Length = bezier3Length;
|
|
1260
1517
|
exports.cappedBonus = cappedBonus;
|
|
1518
|
+
exports.catmullRom = catmullRom;
|
|
1519
|
+
exports.catmullRomInto = catmullRomInto;
|
|
1520
|
+
exports.catmullRomLength = catmullRomLength;
|
|
1261
1521
|
exports.choose = choose;
|
|
1262
1522
|
exports.circleVsCircle = circleVsCircle;
|
|
1263
1523
|
exports.clamp = clamp;
|
|
@@ -1313,6 +1573,9 @@ exports.rectVsRect = rectVsRect;
|
|
|
1313
1573
|
exports.remainingInWindow = remainingInWindow;
|
|
1314
1574
|
exports.rotateTowards = rotateTowards;
|
|
1315
1575
|
exports.seededIndex = seededIndex;
|
|
1576
|
+
exports.segmentIntersection = segmentIntersection;
|
|
1577
|
+
exports.segmentIntersectionInto = segmentIntersectionInto;
|
|
1578
|
+
exports.segmentsIntersect = segmentsIntersect;
|
|
1316
1579
|
exports.shuffle = shuffle;
|
|
1317
1580
|
exports.sign = sign;
|
|
1318
1581
|
exports.smoothStep = smoothStep;
|