pts 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pts",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A library for visualization and creative coding.",
5
5
  "type": "commonjs",
6
6
  "main": "./dist/index.js",
@@ -62,7 +62,8 @@
62
62
  "prepack": "pnpm build",
63
63
  "docs": "node scripts/build-docs-runtime.mjs && node scripts/generate-docs.mjs && node scripts/generate-guides.mjs && node scripts/generate-markdown-docs.mjs",
64
64
  "build:docs-runtime": "node scripts/build-docs-runtime.mjs",
65
- "build:editor": "node scripts/build-editor.mjs"
65
+ "build:editor": "node scripts/build-editor.mjs",
66
+ "test:path:fuzz": "node scripts/fuzz-path.mjs"
66
67
  },
67
68
  "keywords": [
68
69
  "canvas",
package/src/Canvas.ts CHANGED
@@ -1296,8 +1296,44 @@ export class CanvasForm<
1296
1296
  * @param pts a Group or an Iterable<PtLike> representing a line
1297
1297
  */
1298
1298
  line(pts: PtLikeIterable): this {
1299
- CanvasForm.line(this._ctx, pts);
1300
- this._paint();
1299
+ const p = Util.iterToArray(pts);
1300
+ if (Util.arrayCheck(p)) {
1301
+ CanvasForm.line(this._ctx, p);
1302
+ this._paint();
1303
+ }
1304
+ return this;
1305
+ }
1306
+
1307
+ /**
1308
+ * A static function to draw a chain of cubic Bezier curves as one native path.
1309
+ * @param ctx canvas rendering context
1310
+ * @param pts a Group or an Iterable<PtLike> in the layout of [`Curve.bezier`](#link): an anchor followed by 2 control points and an anchor per segment
1311
+ */
1312
+ static bezier(ctx: RenderingContext2D, pts: PtLikeIterable) {
1313
+ const p = Util.iterToArray(pts);
1314
+ if (p.length < 4) return;
1315
+ ctx.beginPath();
1316
+ ctx.moveTo(p[0][0], p[0][1]);
1317
+ for (let i = 3; i < p.length; i += 3) {
1318
+ const c1 = p[i - 2];
1319
+ const c2 = p[i - 1];
1320
+ ctx.bezierCurveTo(c1[0], c1[1], c2[0], c2[1], p[i][0], p[i][1]);
1321
+ }
1322
+ }
1323
+
1324
+ /**
1325
+ * Draw a chain of cubic Bezier curves as one native path. Unlike a polyline from [`Curve.bezier`](#link),
1326
+ * the path stays smooth at any zoom and exports as compact SVG. Use [`Curve.cardinalToBezier`](#link) or
1327
+ * [`Curve.bsplineToBezier`](#link) to draw those curves this way.
1328
+ * @param pts a Group or an Iterable<PtLike> in the layout of [`Curve.bezier`](#link): an anchor followed by 2 control points and an anchor per segment
1329
+ * @example `form.bezier( Curve.cardinalToBezier( pts ) )`
1330
+ */
1331
+ bezier(pts: PtLikeIterable): this {
1332
+ const p = Util.iterToArray(pts);
1333
+ if (Util.arrayCheck(p, 4)) {
1334
+ CanvasForm.bezier(this._ctx, p);
1335
+ this._paint();
1336
+ }
1301
1337
  return this;
1302
1338
  }
1303
1339
 
@@ -1317,8 +1353,67 @@ export class CanvasForm<
1317
1353
  * @param pts a Group or an Iterable<PtLike> representingg a polygon
1318
1354
  */
1319
1355
  polygon(pts: PtLikeIterable): this {
1320
- CanvasForm.polygon(this._ctx, pts);
1321
- this._paint();
1356
+ const p = Util.iterToArray(pts);
1357
+ if (Util.arrayCheck(p)) {
1358
+ CanvasForm.polygon(this._ctx, p);
1359
+ this._paint();
1360
+ }
1361
+ return this;
1362
+ }
1363
+
1364
+ /**
1365
+ * A static function to draw a compound polygon: several rings as one path, so that a ring inside another with the opposite orientation becomes a hole (the nonzero winding rule).
1366
+ * @param ctx canvas rendering context
1367
+ * @param rings an Array/Iterable of rings, each a Group or an Iterable<PtLike>; rings with fewer than 2 points are skipped
1368
+ */
1369
+ static compound(ctx: RenderingContext2D, rings: Iterable<PtLikeIterable>) {
1370
+ let started = false;
1371
+ for (const ring of rings) {
1372
+ const p = Util.iterToArray(ring);
1373
+ if (p.length < 2) continue;
1374
+ if (typeof p[0][0] !== "number") {
1375
+ // a list of polygons (such as a Path.divide result) instead of a list of rings
1376
+ Util.warn(
1377
+ "compound expects rings of points; draw each polygon of a divide or crop result separately",
1378
+ );
1379
+ return;
1380
+ }
1381
+ if (!started) {
1382
+ ctx.beginPath();
1383
+ started = true;
1384
+ }
1385
+ ctx.moveTo(p[0][0], p[0][1]);
1386
+ for (let i = 1, len = p.length; i < len; i++)
1387
+ ctx.lineTo(p[i][0], p[i][1]);
1388
+ ctx.closePath();
1389
+ }
1390
+ }
1391
+
1392
+ /**
1393
+ * Draw a compound polygon: several rings as one path, so that a ring inside another with the opposite orientation becomes a hole
1394
+ * (the nonzero winding rule). This is how a [`Path`](#link) result is drawn; [`CanvasForm.polygons`](#link) would fill the holes.
1395
+ * @param rings an Array/Iterable of rings, each a Group or an Iterable<PtLike>; rings with fewer than 2 points are skipped, and nothing is drawn if no ring remains
1396
+ * @example `form.fillOnly("#f03").compound( Path.minusFront( [disc, hole] ) )`
1397
+ */
1398
+ compound(rings: Iterable<PtLikeIterable>): this {
1399
+ const list: PtLike[][] = [];
1400
+ let drawable = false;
1401
+ for (const ring of rings) {
1402
+ const p = Util.iterToArray(ring);
1403
+ if (p.length > 0 && typeof p[0][0] !== "number") {
1404
+ // a list of polygons (such as a Path.divide result) instead of a list of rings
1405
+ return Util.warn(
1406
+ "compound expects rings of points; draw each polygon of a divide or crop result separately",
1407
+ this,
1408
+ );
1409
+ }
1410
+ if (p.length >= 2) drawable = true;
1411
+ list.push(p);
1412
+ }
1413
+ if (drawable) {
1414
+ CanvasForm.compound(this._ctx, list);
1415
+ this._paint();
1416
+ }
1322
1417
  return this;
1323
1418
  }
1324
1419
 
package/src/Color.ts CHANGED
@@ -5,6 +5,32 @@ import { Util } from "./Util";
5
5
  import { Num, Geom } from "./Num";
6
6
  import { type PtLike, type ColorType } from "./Types";
7
7
 
8
+ // Module state behind Color's static accessors: a static field would be
9
+ // emitted as an assignment after the class, which bundlers keep, and with it
10
+ // the class and everything it references (see Util.ts). The initializers are
11
+ // marked pure so a bundle without Color drops them. Internal code reads
12
+ // `Color.ranges`, not `_colorRanges`, so redefining the static still reaches
13
+ // the conversions.
14
+
15
+ // XYZ property for Standard Observer 2deg, Daylight/sRGB illuminant D65
16
+ const D65 = /* @__PURE__ */ new Pt(95.047, 100, 108.883, 1);
17
+
18
+ let _colorRanges: { [name: string]: Group } = /* @__PURE__ */ colorRanges();
19
+
20
+ function colorRanges(): { [name: string]: Group } {
21
+ return {
22
+ rgb: new Group(new Pt(0, 255), new Pt(0, 255), new Pt(0, 255)),
23
+ hsl: new Group(new Pt(0, 360), new Pt(0, 1), new Pt(0, 1)),
24
+ hsb: new Group(new Pt(0, 360), new Pt(0, 1), new Pt(0, 1)),
25
+ lab: new Group(new Pt(0, 100), new Pt(-128, 127), new Pt(-128, 127)),
26
+ lch: new Group(new Pt(0, 100), new Pt(0, 100), new Pt(0, 360)),
27
+ luv: new Group(new Pt(0, 100), new Pt(-134, 220), new Pt(-140, 122)),
28
+ xyz: new Group(new Pt(0, 100), new Pt(0, 100), new Pt(0, 100)),
29
+ oklab: new Group(new Pt(0, 1), new Pt(-0.4, 0.4), new Pt(-0.4, 0.4)),
30
+ oklch: new Group(new Pt(0, 1), new Pt(0, 0.4), new Pt(0, 360)),
31
+ };
32
+ }
33
+
8
34
  /**
9
35
  * Color is a subclass of Pt. Since a color in a color space is analogous to a point or vector in a space, you can apply all Pt operations to colors too. The Color class provides support for many color spaces like HSL and LAB.
10
36
  * Convert non-RGB colors to RGB before using `.hex`, `.rgb`, or `.rgba` for rendering. These getters format the channels; they don't convert between color spaces.
@@ -15,26 +41,28 @@ import { type PtLike, type ColorType } from "./Types";
15
41
  * ```
16
42
  */
17
43
  export class Color extends Pt {
18
- // XYZ property for Standard Observer 2deg, Daylight/sRGB illuminant D65
19
- private static D65: PtLike = new Pt(95.047, 100, 108.883, 1);
20
-
21
44
  protected _mode: ColorType = "rgb";
22
45
  private _isNorm: boolean = false;
23
46
 
24
47
  /**
25
48
  * Value range for each color space
26
49
  */
27
- static ranges: { [name: string]: Group } = {
28
- rgb: new Group(new Pt(0, 255), new Pt(0, 255), new Pt(0, 255)),
29
- hsl: new Group(new Pt(0, 360), new Pt(0, 1), new Pt(0, 1)),
30
- hsb: new Group(new Pt(0, 360), new Pt(0, 1), new Pt(0, 1)),
31
- lab: new Group(new Pt(0, 100), new Pt(-128, 127), new Pt(-128, 127)),
32
- lch: new Group(new Pt(0, 100), new Pt(0, 100), new Pt(0, 360)),
33
- luv: new Group(new Pt(0, 100), new Pt(-134, 220), new Pt(-140, 122)),
34
- xyz: new Group(new Pt(0, 100), new Pt(0, 100), new Pt(0, 100)),
35
- oklab: new Group(new Pt(0, 1), new Pt(-0.4, 0.4), new Pt(-0.4, 0.4)),
36
- oklch: new Group(new Pt(0, 1), new Pt(0, 0.4), new Pt(0, 360)),
37
- };
50
+ static get ranges(): { [name: string]: Group } {
51
+ return _colorRanges;
52
+ }
53
+ static set ranges(value: { [name: string]: Group }) {
54
+ // as with a static field, an assignment on a subclass stays on the subclass
55
+ if (this === Color) {
56
+ _colorRanges = value;
57
+ } else {
58
+ Object.defineProperty(this, "ranges", {
59
+ value,
60
+ writable: true,
61
+ enumerable: true,
62
+ configurable: true,
63
+ });
64
+ }
65
+ }
38
66
 
39
67
  /**
40
68
  * Create a Color. Same as creating a Pt. Optionally you may use [`Color.from`](#link) to create a color.
@@ -779,7 +807,7 @@ export class Color extends Pt {
779
807
  const kap = 24389 / 27;
780
808
 
781
809
  // adjust for D65
782
- c.divide(Color.D65);
810
+ c.divide(D65);
783
811
 
784
812
  const fn = (n: number) => (n > eps ? Math.cbrt(n) : (kap * n + 16) / 116);
785
813
  const cy = fn(c[1]);
@@ -812,7 +840,7 @@ export class Color extends Pt {
812
840
  const eps = 216 / 24389;
813
841
  const kap = 24389 / 27;
814
842
 
815
- const d = Color.D65;
843
+ const d = D65;
816
844
  const xxx = Math.pow(x, 3);
817
845
  const zzz = Math.pow(z, 3);
818
846
 
@@ -850,12 +878,8 @@ export class Color extends Pt {
850
878
  y = y / 100;
851
879
  const L = y > eps ? 116 * Math.cbrt(y) - 16 : kap * y;
852
880
 
853
- const refU =
854
- (4 * Color.D65[0]) /
855
- (Color.D65[0] + 15 * Color.D65[1] + 3 * Color.D65[2]);
856
- const refV =
857
- (9 * Color.D65[1]) /
858
- (Color.D65[0] + 15 * Color.D65[1] + 3 * Color.D65[2]);
881
+ const refU = (4 * D65[0]) / (D65[0] + 15 * D65[1] + 3 * D65[2]);
882
+ const refV = (9 * D65[1]) / (D65[0] + 15 * D65[1] + 3 * D65[2]);
859
883
 
860
884
  const cc = Color.luv(
861
885
  L,
@@ -891,12 +915,8 @@ export class Color extends Pt {
891
915
  const fy = (l + 16) / 116;
892
916
  let y = l > kap * eps ? fy * fy * fy : l / kap;
893
917
 
894
- const refU =
895
- (4 * Color.D65[0]) /
896
- (Color.D65[0] + 15 * Color.D65[1] + 3 * Color.D65[2]);
897
- const refV =
898
- (9 * Color.D65[1]) /
899
- (Color.D65[0] + 15 * Color.D65[1] + 3 * Color.D65[2]);
918
+ const refU = (4 * D65[0]) / (D65[0] + 15 * D65[1] + 3 * D65[2]);
919
+ const refV = (9 * D65[1]) / (D65[0] + 15 * D65[1] + 3 * D65[2]);
900
920
 
901
921
  u = u / (13 * l) + refU;
902
922
  v = v / (13 * l) + refV;
package/src/Create.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */
2
2
 
3
- import { Pt, Group, type Bound } from "./Pt";
3
+ import { Pt, Group, Bound } from "./Pt";
4
4
  import { Line, Triangle } from "./Op";
5
5
  import { Const, Util } from "./Util";
6
6
  import { Num, Geom } from "./Num";
@@ -14,6 +14,7 @@ import {
14
14
  type DelaunayShape,
15
15
  type FlockBoundary,
16
16
  type FlockOptions,
17
+ type PoissonDiskOptions,
17
18
  } from "./Types";
18
19
 
19
20
  /**
@@ -190,6 +191,31 @@ export class Create {
190
191
  for (const p of pts) flock.addBoid(p);
191
192
  return flock;
192
193
  }
194
+
195
+ /**
196
+ * Create a set of Pts that are randomly placed but never closer than `radius` to each other,
197
+ * using Poisson-disk sampling (also called blue noise).
198
+ * Compared with [`Create.distributeRandom`](#link), the points avoid clumping.
199
+ * Sampling uses a finite candidate budget, so gaps can remain when it finishes.
200
+ * The returned [`PoissonDisk`](#link) is a complete Group; to grow a set gradually instead,
201
+ * construct a `PoissonDisk` and call its [`PoissonDisk.step`](#link) or [`PoissonDisk.sample`](#link).
202
+ * See a [demo here](https://ptsjs.org/demo/?name=create.sampling).
203
+ *
204
+ * Randomness comes from [`Num.random`](#link), so seeding with [`Num.seed`](#link) makes the set reproducible.
205
+ *
206
+ * @param bound the rectangular boundary
207
+ * @param radius minimum distance between any two points
208
+ * @param options optional [`PoissonDiskOptions`](#link)
209
+ * @returns an instance of the PoissonDisk class, which is a Group of Pts
210
+ * @example `Create.sampling( space.innerBound, 10 )`
211
+ */
212
+ static sampling(
213
+ bound: Bound,
214
+ radius: number,
215
+ options: PoissonDiskOptions = {},
216
+ ): PoissonDisk {
217
+ return new PoissonDisk().setup(bound, radius, options).sample();
218
+ }
193
219
  }
194
220
 
195
221
  /**
@@ -235,7 +261,8 @@ const __noise_permTable = [
235
261
  // The doubled base permutation table, built once and shared by every unseeded
236
262
  // Noise instance (a per-instance copy would allocate 512 entries per point in
237
263
  // `Create.noisePts`). `seed()` swaps in a seeded table instead of mutating.
238
- const __noise_permDoubled = __noise_permTable.concat(__noise_permTable);
264
+ const __noise_permDoubled =
265
+ /* @__PURE__ */ __noise_permTable.concat(__noise_permTable);
239
266
 
240
267
  // Memoize the last seeded table: `Create.noisePts` seeds every point with the
241
268
  // same value, so all its Noise Pts share one table.
@@ -1545,3 +1572,264 @@ export class Flock extends Group {
1545
1572
  }
1546
1573
  }
1547
1574
  }
1575
+
1576
+ // Cell offsets in scan order for `PoissonDisk`, nearest first
1577
+ const __near = [0, -1, 1, -2, 2];
1578
+
1579
+ /**
1580
+ * PoissonDisk is a Group of Pts produced by Poisson-disk sampling: every point is at least
1581
+ * [`PoissonDisk.radius`](#link) away from every other. Create a finished set with
1582
+ * [`Create.sampling`](#link), or construct one directly and grow it with [`PoissonDisk.step`](#link)
1583
+ * (one point at a time) or [`PoissonDisk.sample`](#link) (a batch at a time), which is how the
1584
+ * [demo](https://ptsjs.org/demo/?name=create.sampling) shows the packing as it forms.
1585
+ *
1586
+ * The sampler is Bridson's grid-accelerated algorithm with Roberts' candidate placement: each
1587
+ * visit to an active point tries up to [`PoissonDisk.candidates`](#link) candidates on the circle
1588
+ * just outside `radius` around it, at evenly spaced angles from a random offset, and accepts the
1589
+ * first one with no existing point within `radius`. It runs in linear time on one small integer
1590
+ * grid. In a bound thinner than the radius, candidates take random positions across the thin
1591
+ * axis and alternate along the long one, since a circle of candidates would miss the strip.
1592
+ *
1593
+ * Three traits to know: most points sit just beyond `radius` from the point that spawned them,
1594
+ * which packs tighter than candidates at random distances; the candidate budget is finite, so
1595
+ * a finished set can still have gaps; and coordinates are compared as float32 (the precision of
1596
+ * a Pt), exact at pixel scales but rejecting some candidates when coordinates exceed roughly
1597
+ * 8000 times the radius.
1598
+ *
1599
+ * Treat the Group as read-only while sampling: pushing or moving its Pts by hand would
1600
+ * desynchronize the grid that enforces the spacing. A PoissonDisk produced by `map`, `filter`
1601
+ * or `slice` is a plain copy that reports radius 0 and `done`, and needs `setup` before sampling.
1602
+ */
1603
+ export class PoissonDisk extends Group {
1604
+ private _radius = 0;
1605
+ private _candidates = 8;
1606
+ // per-setup constants of the candidate placement, hoisted out of step()
1607
+ private _dist = 0; // radius with an allowance for float32 rounding
1608
+ private _cos = 1; // rotation between candidates
1609
+ private _sin = 0;
1610
+ private _narrowX = false; // the bound is thinner than a candidate circle across x
1611
+ private _narrowY = false; // or across y
1612
+ private _x0 = 0;
1613
+ private _y0 = 0;
1614
+ private _x1 = 0;
1615
+ private _y1 = 0;
1616
+ private _cell = 1;
1617
+ private _cols = 0;
1618
+ private _rows = 0;
1619
+ private _grid = new Int32Array(0); // one sample index per cell, or -1
1620
+ private _active: number[] = []; // indices of samples that may still spawn neighbors
1621
+
1622
+ /**
1623
+ * Reset this sampler and place its first sample. Calling `setup` again empties the group and
1624
+ * starts over, which is how a sketch restarts sampling after a resize.
1625
+ * @param bound the rectangular boundary
1626
+ * @param radius minimum distance between any two points
1627
+ * @param options optional [`PoissonDiskOptions`](#link)
1628
+ * @returns this
1629
+ * @example `new PoissonDisk().setup( space.innerBound, 12 ).sample( 40 )`
1630
+ */
1631
+ setup(bound: Bound, radius: number, options: PoissonDiskOptions = {}): this {
1632
+ if (!(radius > 0) || !Number.isFinite(radius)) {
1633
+ throw new Error("PoissonDisk radius must be a positive finite number");
1634
+ }
1635
+ const x0 = bound.x ?? NaN;
1636
+ const y0 = bound.y ?? NaN;
1637
+ const width = bound.width;
1638
+ const height = bound.height;
1639
+ const x1 = x0 + width;
1640
+ const y1 = y0 + height;
1641
+ if (![x0, y0, x1, y1].every(Number.isFinite) || width < 0 || height < 0) {
1642
+ throw new Error("PoissonDisk bound must have a finite position and size");
1643
+ }
1644
+ // A size can vanish when added to a far larger position (e.g. 1 at 1e20)
1645
+ if ((width > 0 && x1 <= x0) || (height > 0 && y1 <= y0)) {
1646
+ throw new Error(
1647
+ "PoissonDisk bound size must be representable at its position",
1648
+ );
1649
+ }
1650
+ const k = options.candidates ?? 8;
1651
+ if (!(k >= 1) || !Number.isFinite(k)) {
1652
+ throw new Error("PoissonDisk candidates must be a number of at least 1");
1653
+ }
1654
+
1655
+ // Each grid cell is small enough to hold at most one sample
1656
+ const cell = radius / Math.SQRT2;
1657
+ const hasArea = width > 0 && height > 0;
1658
+ const cols = hasArea ? Math.ceil(width / cell) : 0;
1659
+ const rows = hasArea ? Math.ceil(height / cell) : 0;
1660
+ if (cols * rows > 1 << 26) {
1661
+ throw new Error(
1662
+ "PoissonDisk radius is too small for this bound: the grid would exceed 2^26 cells",
1663
+ );
1664
+ }
1665
+
1666
+ this.length = 0;
1667
+ this._active.length = 0;
1668
+ this._radius = radius;
1669
+ this._candidates = Math.floor(k);
1670
+ this._x0 = x0;
1671
+ this._y0 = y0;
1672
+ this._x1 = x1;
1673
+ this._y1 = y1;
1674
+ this._cell = cell;
1675
+ this._cols = cols;
1676
+ this._rows = rows;
1677
+ this._grid = new Int32Array(cols * rows).fill(-1);
1678
+ this._dist = radius * 1.001; // allow for float32 rounding at ordinary canvas scales
1679
+ this._cos = Math.cos(Const.two_pi / this._candidates);
1680
+ this._sin = Math.sin(Const.two_pi / this._candidates);
1681
+ // A full circle of candidates can miss a strip thinner than `dist` entirely; there,
1682
+ // candidates take a random position across the strip and alternate along it instead.
1683
+ this._narrowX = width < this._dist && width <= height;
1684
+ this._narrowY = height < this._dist && !this._narrowX;
1685
+
1686
+ if (options.start !== undefined) {
1687
+ const s = options.start;
1688
+ if (!this._tryAdd(Math.fround(s[0]), Math.fround(s[1]))) {
1689
+ throw new Error(
1690
+ "PoissonDisk start point must lie on or after the bound's top-left edges and before its bottom-right edges",
1691
+ );
1692
+ }
1693
+ } else if (cols * rows > 0) {
1694
+ // Redraw on the rare float32 rounding that lands exactly on the far edge
1695
+ while (
1696
+ !this._tryAdd(
1697
+ Math.fround(x0 + Num.random() * width),
1698
+ Math.fround(y0 + Num.random() * height),
1699
+ )
1700
+ );
1701
+ }
1702
+ return this;
1703
+ }
1704
+
1705
+ /**
1706
+ * Minimum distance between any two points in this set.
1707
+ */
1708
+ get radius(): number {
1709
+ return this._radius;
1710
+ }
1711
+
1712
+ /**
1713
+ * Maximum candidates tried per visit to an active sample before retiring it if none succeed.
1714
+ */
1715
+ get candidates(): number {
1716
+ return this._candidates;
1717
+ }
1718
+
1719
+ /**
1720
+ * The rectangular boundary that the samples fill.
1721
+ */
1722
+ get bound(): Bound {
1723
+ return new Bound(new Pt(this._x0, this._y0), new Pt(this._x1, this._y1));
1724
+ }
1725
+
1726
+ /**
1727
+ * Whether no active samples remain. Gaps may still fit further points, but sampling has stopped.
1728
+ */
1729
+ get done(): boolean {
1730
+ return this._active.length === 0;
1731
+ }
1732
+
1733
+ /**
1734
+ * Add the next sample and return it, or return `undefined` once no active samples remain.
1735
+ * The new Pt is also the last element of this group.
1736
+ * @example `let p = pd.step(); if (p) form.point( p, 2 );`
1737
+ */
1738
+ step(): Pt | undefined {
1739
+ const active = this._active;
1740
+ const k = this._candidates;
1741
+ const dist = this._dist;
1742
+ const cos = this._cos;
1743
+ const sin = this._sin;
1744
+ const width = this._x1 - this._x0;
1745
+ const height = this._y1 - this._y0;
1746
+ const narrowX = this._narrowX;
1747
+ const narrowY = this._narrowY;
1748
+
1749
+ while (active.length > 0) {
1750
+ const ai = Math.floor(Num.random() * active.length);
1751
+ const p = this[active[ai]];
1752
+ const a0 = Num.random() * Const.two_pi;
1753
+ let dx = dist * Math.cos(a0);
1754
+ let dy = dist * Math.sin(a0);
1755
+ let along = a0 < Math.PI ? 1 : -1;
1756
+ for (let j = 0; j < k; j++) {
1757
+ if (narrowX) {
1758
+ dx = this._x0 + Num.random() * width - p[0];
1759
+ dy = along * Math.sqrt(Math.max(0, dist * dist - dx * dx));
1760
+ along = -along;
1761
+ } else if (narrowY) {
1762
+ dy = this._y0 + Num.random() * height - p[1];
1763
+ dx = along * Math.sqrt(Math.max(0, dist * dist - dy * dy));
1764
+ along = -along;
1765
+ } else if (j > 0) {
1766
+ const x = dx * cos - dy * sin;
1767
+ dy = dx * sin + dy * cos;
1768
+ dx = x;
1769
+ }
1770
+ if (this._tryAdd(Math.fround(p[0] + dx), Math.fround(p[1] + dy))) {
1771
+ return this[this.length - 1];
1772
+ }
1773
+ }
1774
+ // This visit exhausted its candidate budget: retire the sample
1775
+ active[ai] = active[active.length - 1];
1776
+ active.pop();
1777
+ }
1778
+ return undefined;
1779
+ }
1780
+
1781
+ /**
1782
+ * Add up to `count` more samples, or every remaining sample by default.
1783
+ * @param count maximum number of samples to add, rounded down; nonpositive values and NaN add none
1784
+ * @example `pd.sample( 20 )` adds twenty points per frame; `pd.sample()` completes the set
1785
+ */
1786
+ sample(count: number = Infinity): this {
1787
+ const limit = Math.floor(count);
1788
+ for (let i = 0; i < limit; i++) {
1789
+ if (this.step() === undefined) break;
1790
+ }
1791
+ return this;
1792
+ }
1793
+
1794
+ /**
1795
+ * Store the point at (x, y) if it lies inside the bound and no sample is within `radius` of it.
1796
+ * Coordinates must already be float32 values, so what is tested is exactly what is stored.
1797
+ */
1798
+ private _tryAdd(x: number, y: number): boolean {
1799
+ if (!(x >= this._x0 && x < this._x1 && y >= this._y0 && y < this._y1)) {
1800
+ return false;
1801
+ }
1802
+ const cols = this._cols;
1803
+ const rows = this._rows;
1804
+ const cx = Math.min(cols - 1, Math.floor((x - this._x0) / this._cell));
1805
+ const cy = Math.min(rows - 1, Math.floor((y - this._y0) / this._cell));
1806
+ const grid = this._grid;
1807
+
1808
+ // A conflicting sample lies within two cells in each direction, but never in the four
1809
+ // corners of that window. Scan from the center outward: a rejected candidate usually
1810
+ // conflicts with a sample close to it, so the scan ends after a few cells.
1811
+ const r2 = this._radius * this._radius;
1812
+ for (const dj of __near) {
1813
+ const j = cy + dj;
1814
+ if (j < 0 || j >= rows) continue;
1815
+ const row = j * cols;
1816
+ const reach = dj === -2 || dj === 2 ? 1 : 2;
1817
+ for (const di of __near) {
1818
+ const i = cx + di;
1819
+ if (di > reach || di < -reach || i < 0 || i >= cols) continue;
1820
+ const s = grid[row + i];
1821
+ if (s >= 0) {
1822
+ const q = this[s];
1823
+ const dx = q[0] - x;
1824
+ const dy = q[1] - y;
1825
+ if (dx * dx + dy * dy < r2) return false;
1826
+ }
1827
+ }
1828
+ }
1829
+
1830
+ grid[cy * cols + cx] = this.length;
1831
+ this._active.push(this.length);
1832
+ this.push(new Pt(x, y));
1833
+ return true;
1834
+ }
1835
+ }
package/src/Dom.ts CHANGED
@@ -347,7 +347,7 @@ export class DOMSpace extends MultiTouchSpace {
347
347
 
348
348
  /**
349
349
  * @deprecated HTML rendering is deprecated and will be removed in a future major version. Use [`SVGSpace`](#link) for DOM-based output instead — it shares the supported subset of the [`CanvasForm`](#link) drawing API.
350
- * **[Experimental]** HTMLSpace is a subclass of DOMSpace that works with HTML elements. See [a demo here](https://ptsjs.org/demo/?name=htmlform.scope).
350
+ * **[Experimental]** HTMLSpace is a subclass of DOMSpace that works with HTML elements.
351
351
  */
352
352
  export class HTMLSpace extends DOMSpace {
353
353
  /**
@@ -1,7 +1,6 @@
1
1
  /*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */
2
2
 
3
3
  import { Pt, Group } from "./Pt";
4
- import { Line } from "./Op";
5
4
  import { type PtLike, type GroupLike } from "./Types";
6
5
 
7
6
  /**
@@ -506,17 +505,19 @@ export class Mat {
506
505
  * @param p1 second end point to define the reflection line
507
506
  */
508
507
  static reflectAt2DMatrix(p1: PtLike, p2: PtLike) {
509
- const intercept = Line.intercept(p1, p2);
510
-
511
- if (intercept == undefined) {
508
+ // The slope and y-intercept as `Line.intercept` computes them, inlined so
509
+ // this module does not import Op: a Pt-only bundle would otherwise keep
510
+ // every geometry class.
511
+ if (p2[0] - p1[0] === 0) {
512
512
  return [
513
513
  new Pt([-1, 0, 0]),
514
514
  new Pt([0, 1, 0]),
515
515
  new Pt([p1[0] + p2[0], 0, 1]),
516
516
  ];
517
517
  } else {
518
- const yi = intercept.yi;
519
- const ang2 = Math.atan(intercept.slope) * 2;
518
+ const slope = (p2[1] - p1[1]) / (p2[0] - p1[0]);
519
+ const yi = p1[1] - slope * p1[0];
520
+ const ang2 = Math.atan(slope) * 2;
520
521
  const cosA = Math.cos(ang2);
521
522
  const sinA = Math.sin(ang2);
522
523
 
package/src/Num.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */
2
2
 
3
3
  import { Const, Util } from "./Util";
4
- import { Curve } from "./Op";
5
4
  import { Pt, Group } from "./Pt";
6
5
  import { Vec, Mat } from "./LinearAlgebra";
7
6
  import {
@@ -26,7 +25,9 @@ export class Num {
26
25
  * @param threshold threshold value that specifies the minimum difference within which the two numbers are considered equal
27
26
  */
28
27
  static equals(a: number, b: number, threshold = 0.00001): boolean {
29
- return Math.abs(a - b) < threshold;
28
+ // A value equals itself (infinities included, whose difference is NaN),
29
+ // and a difference equal to the threshold is included, as in Pt.equals.
30
+ return a === b || Math.abs(a - b) <= threshold;
30
31
  }
31
32
 
32
33
  /**
@@ -888,7 +889,7 @@ export class Shaping {
888
889
  }
889
890
 
890
891
  /**
891
- * Cubic bezier curve. This reuses the bezier functions in Curve class. Note that `t` is the curve parameter, not the x position: unlike CSS `cubic-bezier(...)`, this returns the curve's y value at parameter `t` rather than solving y at x = t.
892
+ * Cubic bezier curve from (0, 0) to (1, 1) with two control points. Note that `t` is the curve parameter, not the x position: unlike CSS `cubic-bezier(...)`, this returns the curve's y value at parameter `t` rather than solving y at x = t.
892
893
  * @param t a value between 0 to 1
893
894
  * @param c the value to shape, default is 1
894
895
  * @param p1` a Pt object specifying the first control Pt. Default is `Pt(0.1, 0.7).
@@ -900,14 +901,20 @@ export class Shaping {
900
901
  p1: PtLike = [0.1, 0.7],
901
902
  p2: PtLike = [0.9, 0.2],
902
903
  ): number {
903
- const curve = new Group(new Pt(0, 0), new Pt(p1), new Pt(p2), new Pt(1, 1));
904
- return (
905
- c *
906
- Curve.bezierStep(
907
- new Pt(t * t * t, t * t, t, 1),
908
- Curve.controlPoints(curve),
909
- ).y
910
- );
904
+ // `Curve.bezierStep` on the controls (0, 0), p1, p2, (1, 1), inlined so
905
+ // this module does not import Op (a Pt-only bundle would otherwise keep
906
+ // every geometry class). The float32 rounding of that Pt-based evaluation
907
+ // is kept: the powers of t, the control values and the result are float32,
908
+ // and the weighted sum has the same terms in the same order.
909
+ const t3 = Math.fround(t * t * t);
910
+ const t2 = Math.fround(t * t);
911
+ const t1 = Math.fround(t);
912
+ const y =
913
+ (-t3 + 3 * t2 - 3 * t1 + 1) * 0 +
914
+ (3 * t3 - 6 * t2 + 3 * t1) * new Pt(p1)[1] +
915
+ (-3 * t3 + 3 * t2) * new Pt(p2)[1] +
916
+ t3 * 1;
917
+ return c * Math.fround(y);
911
918
  }
912
919
 
913
920
  /**