@flare-engine/math 0.3.0 → 0.4.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.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
- /** Zero-alloc; writes into `out`. Mandatory `out` so callers avoid temporaries in hot paths. */
1077
- normalizeOut(dx, dy, speed, out) {
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 = speed / len;
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
- turnVelocityToward(vx, vy, dx, dy, maxStep, out) {
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;