pts 1.0.0 → 1.0.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/dist/index.d.mts +283 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +283 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6018 -4596
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6017 -4597
- package/dist/index.mjs.map +1 -1
- package/dist/pts.js +4362 -2940
- package/dist/pts.js.map +1 -1
- package/dist/pts.min.js +2 -2
- package/dist/pts.min.js.map +1 -1
- package/package.json +3 -2
- package/src/Canvas.ts +99 -4
- package/src/Create.ts +288 -1
- package/src/Dom.ts +1 -1
- package/src/Num.ts +3 -1
- package/src/Op.ts +315 -0
- package/src/Pt.ts +8 -1
- package/src/Svg.ts +79 -0
- package/src/Types.ts +17 -0
- package/src/_path.ts +1402 -0
- package/src/_triangulate.ts +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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
|
-
|
|
1300
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
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/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,
|
|
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
|
/**
|
|
@@ -1545,3 +1571,264 @@ export class Flock extends Group {
|
|
|
1545
1571
|
}
|
|
1546
1572
|
}
|
|
1547
1573
|
}
|
|
1574
|
+
|
|
1575
|
+
// Cell offsets in scan order for `PoissonDisk`, nearest first
|
|
1576
|
+
const __near = [0, -1, 1, -2, 2];
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* PoissonDisk is a Group of Pts produced by Poisson-disk sampling: every point is at least
|
|
1580
|
+
* [`PoissonDisk.radius`](#link) away from every other. Create a finished set with
|
|
1581
|
+
* [`Create.sampling`](#link), or construct one directly and grow it with [`PoissonDisk.step`](#link)
|
|
1582
|
+
* (one point at a time) or [`PoissonDisk.sample`](#link) (a batch at a time), which is how the
|
|
1583
|
+
* [demo](https://ptsjs.org/demo/?name=create.sampling) shows the packing as it forms.
|
|
1584
|
+
*
|
|
1585
|
+
* The sampler is Bridson's grid-accelerated algorithm with Roberts' candidate placement: each
|
|
1586
|
+
* visit to an active point tries up to [`PoissonDisk.candidates`](#link) candidates on the circle
|
|
1587
|
+
* just outside `radius` around it, at evenly spaced angles from a random offset, and accepts the
|
|
1588
|
+
* first one with no existing point within `radius`. It runs in linear time on one small integer
|
|
1589
|
+
* grid. In a bound thinner than the radius, candidates take random positions across the thin
|
|
1590
|
+
* axis and alternate along the long one, since a circle of candidates would miss the strip.
|
|
1591
|
+
*
|
|
1592
|
+
* Three traits to know: most points sit just beyond `radius` from the point that spawned them,
|
|
1593
|
+
* which packs tighter than candidates at random distances; the candidate budget is finite, so
|
|
1594
|
+
* a finished set can still have gaps; and coordinates are compared as float32 (the precision of
|
|
1595
|
+
* a Pt), exact at pixel scales but rejecting some candidates when coordinates exceed roughly
|
|
1596
|
+
* 8000 times the radius.
|
|
1597
|
+
*
|
|
1598
|
+
* Treat the Group as read-only while sampling: pushing or moving its Pts by hand would
|
|
1599
|
+
* desynchronize the grid that enforces the spacing. A PoissonDisk produced by `map`, `filter`
|
|
1600
|
+
* or `slice` is a plain copy that reports radius 0 and `done`, and needs `setup` before sampling.
|
|
1601
|
+
*/
|
|
1602
|
+
export class PoissonDisk extends Group {
|
|
1603
|
+
private _radius = 0;
|
|
1604
|
+
private _candidates = 8;
|
|
1605
|
+
// per-setup constants of the candidate placement, hoisted out of step()
|
|
1606
|
+
private _dist = 0; // radius with an allowance for float32 rounding
|
|
1607
|
+
private _cos = 1; // rotation between candidates
|
|
1608
|
+
private _sin = 0;
|
|
1609
|
+
private _narrowX = false; // the bound is thinner than a candidate circle across x
|
|
1610
|
+
private _narrowY = false; // or across y
|
|
1611
|
+
private _x0 = 0;
|
|
1612
|
+
private _y0 = 0;
|
|
1613
|
+
private _x1 = 0;
|
|
1614
|
+
private _y1 = 0;
|
|
1615
|
+
private _cell = 1;
|
|
1616
|
+
private _cols = 0;
|
|
1617
|
+
private _rows = 0;
|
|
1618
|
+
private _grid = new Int32Array(0); // one sample index per cell, or -1
|
|
1619
|
+
private _active: number[] = []; // indices of samples that may still spawn neighbors
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Reset this sampler and place its first sample. Calling `setup` again empties the group and
|
|
1623
|
+
* starts over, which is how a sketch restarts sampling after a resize.
|
|
1624
|
+
* @param bound the rectangular boundary
|
|
1625
|
+
* @param radius minimum distance between any two points
|
|
1626
|
+
* @param options optional [`PoissonDiskOptions`](#link)
|
|
1627
|
+
* @returns this
|
|
1628
|
+
* @example `new PoissonDisk().setup( space.innerBound, 12 ).sample( 40 )`
|
|
1629
|
+
*/
|
|
1630
|
+
setup(bound: Bound, radius: number, options: PoissonDiskOptions = {}): this {
|
|
1631
|
+
if (!(radius > 0) || !Number.isFinite(radius)) {
|
|
1632
|
+
throw new Error("PoissonDisk radius must be a positive finite number");
|
|
1633
|
+
}
|
|
1634
|
+
const x0 = bound.x ?? NaN;
|
|
1635
|
+
const y0 = bound.y ?? NaN;
|
|
1636
|
+
const width = bound.width;
|
|
1637
|
+
const height = bound.height;
|
|
1638
|
+
const x1 = x0 + width;
|
|
1639
|
+
const y1 = y0 + height;
|
|
1640
|
+
if (![x0, y0, x1, y1].every(Number.isFinite) || width < 0 || height < 0) {
|
|
1641
|
+
throw new Error("PoissonDisk bound must have a finite position and size");
|
|
1642
|
+
}
|
|
1643
|
+
// A size can vanish when added to a far larger position (e.g. 1 at 1e20)
|
|
1644
|
+
if ((width > 0 && x1 <= x0) || (height > 0 && y1 <= y0)) {
|
|
1645
|
+
throw new Error(
|
|
1646
|
+
"PoissonDisk bound size must be representable at its position",
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
const k = options.candidates ?? 8;
|
|
1650
|
+
if (!(k >= 1) || !Number.isFinite(k)) {
|
|
1651
|
+
throw new Error("PoissonDisk candidates must be a number of at least 1");
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// Each grid cell is small enough to hold at most one sample
|
|
1655
|
+
const cell = radius / Math.SQRT2;
|
|
1656
|
+
const hasArea = width > 0 && height > 0;
|
|
1657
|
+
const cols = hasArea ? Math.ceil(width / cell) : 0;
|
|
1658
|
+
const rows = hasArea ? Math.ceil(height / cell) : 0;
|
|
1659
|
+
if (cols * rows > 1 << 26) {
|
|
1660
|
+
throw new Error(
|
|
1661
|
+
"PoissonDisk radius is too small for this bound: the grid would exceed 2^26 cells",
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
this.length = 0;
|
|
1666
|
+
this._active.length = 0;
|
|
1667
|
+
this._radius = radius;
|
|
1668
|
+
this._candidates = Math.floor(k);
|
|
1669
|
+
this._x0 = x0;
|
|
1670
|
+
this._y0 = y0;
|
|
1671
|
+
this._x1 = x1;
|
|
1672
|
+
this._y1 = y1;
|
|
1673
|
+
this._cell = cell;
|
|
1674
|
+
this._cols = cols;
|
|
1675
|
+
this._rows = rows;
|
|
1676
|
+
this._grid = new Int32Array(cols * rows).fill(-1);
|
|
1677
|
+
this._dist = radius * 1.001; // allow for float32 rounding at ordinary canvas scales
|
|
1678
|
+
this._cos = Math.cos(Const.two_pi / this._candidates);
|
|
1679
|
+
this._sin = Math.sin(Const.two_pi / this._candidates);
|
|
1680
|
+
// A full circle of candidates can miss a strip thinner than `dist` entirely; there,
|
|
1681
|
+
// candidates take a random position across the strip and alternate along it instead.
|
|
1682
|
+
this._narrowX = width < this._dist && width <= height;
|
|
1683
|
+
this._narrowY = height < this._dist && !this._narrowX;
|
|
1684
|
+
|
|
1685
|
+
if (options.start !== undefined) {
|
|
1686
|
+
const s = options.start;
|
|
1687
|
+
if (!this._tryAdd(Math.fround(s[0]), Math.fround(s[1]))) {
|
|
1688
|
+
throw new Error(
|
|
1689
|
+
"PoissonDisk start point must lie on or after the bound's top-left edges and before its bottom-right edges",
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
} else if (cols * rows > 0) {
|
|
1693
|
+
// Redraw on the rare float32 rounding that lands exactly on the far edge
|
|
1694
|
+
while (
|
|
1695
|
+
!this._tryAdd(
|
|
1696
|
+
Math.fround(x0 + Num.random() * width),
|
|
1697
|
+
Math.fround(y0 + Num.random() * height),
|
|
1698
|
+
)
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
return this;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Minimum distance between any two points in this set.
|
|
1706
|
+
*/
|
|
1707
|
+
get radius(): number {
|
|
1708
|
+
return this._radius;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* Maximum candidates tried per visit to an active sample before retiring it if none succeed.
|
|
1713
|
+
*/
|
|
1714
|
+
get candidates(): number {
|
|
1715
|
+
return this._candidates;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* The rectangular boundary that the samples fill.
|
|
1720
|
+
*/
|
|
1721
|
+
get bound(): Bound {
|
|
1722
|
+
return new Bound(new Pt(this._x0, this._y0), new Pt(this._x1, this._y1));
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/**
|
|
1726
|
+
* Whether no active samples remain. Gaps may still fit further points, but sampling has stopped.
|
|
1727
|
+
*/
|
|
1728
|
+
get done(): boolean {
|
|
1729
|
+
return this._active.length === 0;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Add the next sample and return it, or return `undefined` once no active samples remain.
|
|
1734
|
+
* The new Pt is also the last element of this group.
|
|
1735
|
+
* @example `let p = pd.step(); if (p) form.point( p, 2 );`
|
|
1736
|
+
*/
|
|
1737
|
+
step(): Pt | undefined {
|
|
1738
|
+
const active = this._active;
|
|
1739
|
+
const k = this._candidates;
|
|
1740
|
+
const dist = this._dist;
|
|
1741
|
+
const cos = this._cos;
|
|
1742
|
+
const sin = this._sin;
|
|
1743
|
+
const width = this._x1 - this._x0;
|
|
1744
|
+
const height = this._y1 - this._y0;
|
|
1745
|
+
const narrowX = this._narrowX;
|
|
1746
|
+
const narrowY = this._narrowY;
|
|
1747
|
+
|
|
1748
|
+
while (active.length > 0) {
|
|
1749
|
+
const ai = Math.floor(Num.random() * active.length);
|
|
1750
|
+
const p = this[active[ai]];
|
|
1751
|
+
const a0 = Num.random() * Const.two_pi;
|
|
1752
|
+
let dx = dist * Math.cos(a0);
|
|
1753
|
+
let dy = dist * Math.sin(a0);
|
|
1754
|
+
let along = a0 < Math.PI ? 1 : -1;
|
|
1755
|
+
for (let j = 0; j < k; j++) {
|
|
1756
|
+
if (narrowX) {
|
|
1757
|
+
dx = this._x0 + Num.random() * width - p[0];
|
|
1758
|
+
dy = along * Math.sqrt(Math.max(0, dist * dist - dx * dx));
|
|
1759
|
+
along = -along;
|
|
1760
|
+
} else if (narrowY) {
|
|
1761
|
+
dy = this._y0 + Num.random() * height - p[1];
|
|
1762
|
+
dx = along * Math.sqrt(Math.max(0, dist * dist - dy * dy));
|
|
1763
|
+
along = -along;
|
|
1764
|
+
} else if (j > 0) {
|
|
1765
|
+
const x = dx * cos - dy * sin;
|
|
1766
|
+
dy = dx * sin + dy * cos;
|
|
1767
|
+
dx = x;
|
|
1768
|
+
}
|
|
1769
|
+
if (this._tryAdd(Math.fround(p[0] + dx), Math.fround(p[1] + dy))) {
|
|
1770
|
+
return this[this.length - 1];
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
// This visit exhausted its candidate budget: retire the sample
|
|
1774
|
+
active[ai] = active[active.length - 1];
|
|
1775
|
+
active.pop();
|
|
1776
|
+
}
|
|
1777
|
+
return undefined;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/**
|
|
1781
|
+
* Add up to `count` more samples, or every remaining sample by default.
|
|
1782
|
+
* @param count maximum number of samples to add, rounded down; nonpositive values and NaN add none
|
|
1783
|
+
* @example `pd.sample( 20 )` adds twenty points per frame; `pd.sample()` completes the set
|
|
1784
|
+
*/
|
|
1785
|
+
sample(count: number = Infinity): this {
|
|
1786
|
+
const limit = Math.floor(count);
|
|
1787
|
+
for (let i = 0; i < limit; i++) {
|
|
1788
|
+
if (this.step() === undefined) break;
|
|
1789
|
+
}
|
|
1790
|
+
return this;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Store the point at (x, y) if it lies inside the bound and no sample is within `radius` of it.
|
|
1795
|
+
* Coordinates must already be float32 values, so what is tested is exactly what is stored.
|
|
1796
|
+
*/
|
|
1797
|
+
private _tryAdd(x: number, y: number): boolean {
|
|
1798
|
+
if (!(x >= this._x0 && x < this._x1 && y >= this._y0 && y < this._y1)) {
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
const cols = this._cols;
|
|
1802
|
+
const rows = this._rows;
|
|
1803
|
+
const cx = Math.min(cols - 1, Math.floor((x - this._x0) / this._cell));
|
|
1804
|
+
const cy = Math.min(rows - 1, Math.floor((y - this._y0) / this._cell));
|
|
1805
|
+
const grid = this._grid;
|
|
1806
|
+
|
|
1807
|
+
// A conflicting sample lies within two cells in each direction, but never in the four
|
|
1808
|
+
// corners of that window. Scan from the center outward: a rejected candidate usually
|
|
1809
|
+
// conflicts with a sample close to it, so the scan ends after a few cells.
|
|
1810
|
+
const r2 = this._radius * this._radius;
|
|
1811
|
+
for (const dj of __near) {
|
|
1812
|
+
const j = cy + dj;
|
|
1813
|
+
if (j < 0 || j >= rows) continue;
|
|
1814
|
+
const row = j * cols;
|
|
1815
|
+
const reach = dj === -2 || dj === 2 ? 1 : 2;
|
|
1816
|
+
for (const di of __near) {
|
|
1817
|
+
const i = cx + di;
|
|
1818
|
+
if (di > reach || di < -reach || i < 0 || i >= cols) continue;
|
|
1819
|
+
const s = grid[row + i];
|
|
1820
|
+
if (s >= 0) {
|
|
1821
|
+
const q = this[s];
|
|
1822
|
+
const dx = q[0] - x;
|
|
1823
|
+
const dy = q[1] - y;
|
|
1824
|
+
if (dx * dx + dy * dy < r2) return false;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
grid[cy * cols + cx] = this.length;
|
|
1830
|
+
this._active.push(this.length);
|
|
1831
|
+
this.push(new Pt(x, y));
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
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.
|
|
350
|
+
* **[Experimental]** HTMLSpace is a subclass of DOMSpace that works with HTML elements.
|
|
351
351
|
*/
|
|
352
352
|
export class HTMLSpace extends DOMSpace {
|
|
353
353
|
/**
|
package/src/Num.ts
CHANGED
|
@@ -26,7 +26,9 @@ export class Num {
|
|
|
26
26
|
* @param threshold threshold value that specifies the minimum difference within which the two numbers are considered equal
|
|
27
27
|
*/
|
|
28
28
|
static equals(a: number, b: number, threshold = 0.00001): boolean {
|
|
29
|
-
|
|
29
|
+
// A value equals itself (infinities included, whose difference is NaN),
|
|
30
|
+
// and a difference equal to the threshold is included, as in Pt.equals.
|
|
31
|
+
return a === b || Math.abs(a - b) <= threshold;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|