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