pts 0.12.8 → 1.0.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/README.md +92 -80
- package/dist/index.d.mts +6208 -1254
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +6208 -1254
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9314 -10680
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +9266 -10611
- package/dist/index.mjs.map +1 -0
- package/dist/pts.js +9446 -10777
- package/dist/pts.js.map +1 -0
- package/dist/pts.min.js +3 -5
- package/dist/pts.min.js.map +1 -0
- package/package.json +91 -31
- package/src/Canvas.ts +1642 -0
- package/src/Color.ts +1109 -0
- package/src/Create.ts +1547 -0
- package/src/Dom.ts +940 -0
- package/src/Form.ts +312 -0
- package/src/Image.ts +722 -0
- package/src/LinearAlgebra.ts +530 -0
- package/src/Num.ts +1091 -0
- package/src/Op.ts +2127 -0
- package/src/Physics.ts +1233 -0
- package/src/Play.ts +861 -0
- package/src/Pt.ts +1303 -0
- package/src/Space.ts +898 -0
- package/src/Svg.ts +1573 -0
- package/src/Types.ts +301 -0
- package/src/Typography.ts +228 -0
- package/src/UI.ts +757 -0
- package/src/Util.ts +454 -0
- package/src/_module.ts +18 -0
- package/src/_script.ts +94 -0
- package/src/_triangulate.ts +884 -0
- package/src/uheprng.ts +153 -0
package/src/Create.ts
ADDED
|
@@ -0,0 +1,1547 @@
|
|
|
1
|
+
/*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */
|
|
2
|
+
|
|
3
|
+
import { Pt, Group, type Bound } from "./Pt";
|
|
4
|
+
import { Line, Triangle } from "./Op";
|
|
5
|
+
import { Const, Util } from "./Util";
|
|
6
|
+
import { Num, Geom } from "./Num";
|
|
7
|
+
import { triangulate, type Triangulation } from "./_triangulate";
|
|
8
|
+
import {
|
|
9
|
+
type PtLike,
|
|
10
|
+
type PtLikeIterable,
|
|
11
|
+
type GroupLike,
|
|
12
|
+
type PtIterable,
|
|
13
|
+
type DelaunayMesh,
|
|
14
|
+
type DelaunayShape,
|
|
15
|
+
type FlockBoundary,
|
|
16
|
+
type FlockOptions,
|
|
17
|
+
} from "./Types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The `Create` class helps you create structures from sets of points.
|
|
21
|
+
*/
|
|
22
|
+
export class Create {
|
|
23
|
+
/**
|
|
24
|
+
* Create a set of random points inside a bounday.
|
|
25
|
+
* @param bound the rectangular boundary
|
|
26
|
+
* @param count number of random points to create
|
|
27
|
+
* @param dimensions number of dimensions in each point
|
|
28
|
+
*/
|
|
29
|
+
static distributeRandom(
|
|
30
|
+
bound: Bound,
|
|
31
|
+
count: number,
|
|
32
|
+
dimensions: number = 2,
|
|
33
|
+
): Group {
|
|
34
|
+
let pts = new Group();
|
|
35
|
+
for (let i = 0; i < count; i++) {
|
|
36
|
+
let p = [bound.x! + Num.random() * bound.width];
|
|
37
|
+
if (dimensions > 1) p.push(bound.y! + Num.random() * bound.height);
|
|
38
|
+
if (dimensions > 2) p.push(bound.z! + Num.random() * bound.depth);
|
|
39
|
+
pts.push(new Pt(p));
|
|
40
|
+
}
|
|
41
|
+
return pts;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a set of points that distribute evenly on a line. Similar to [`Line.subpoints`](#link) but includes the end points.
|
|
46
|
+
* @param line a Group or an Iterable<Pt> representing a line
|
|
47
|
+
* @param count number of points to create
|
|
48
|
+
*/
|
|
49
|
+
static distributeLinear(line: PtIterable, count: number): Group {
|
|
50
|
+
if (count <= 0) return new Group();
|
|
51
|
+
let _line = Util.iterToArray(line);
|
|
52
|
+
if (count === 1) return new Group(_line[0]);
|
|
53
|
+
let ln = Line.subpoints(_line, count - 2);
|
|
54
|
+
ln.unshift(_line[0]);
|
|
55
|
+
ln.push(_line[_line.length - 1]);
|
|
56
|
+
return ln;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create an evenly distributed set of points (like a grid of points) inside a boundary.
|
|
61
|
+
* @param bound the rectangular boundary
|
|
62
|
+
* @param columns number of columns
|
|
63
|
+
* @param rows number of rows
|
|
64
|
+
* @param orientation a Pt or number array to specify where the point should be inside a cell. Default is [0.5, 0.5] which places the point in the middle.
|
|
65
|
+
* @returns a Group of Pts
|
|
66
|
+
*/
|
|
67
|
+
static gridPts(
|
|
68
|
+
bound: Bound,
|
|
69
|
+
columns: number,
|
|
70
|
+
rows: number,
|
|
71
|
+
orientation: PtLike = [0.5, 0.5],
|
|
72
|
+
): Group {
|
|
73
|
+
if (columns === 0 || rows === 0)
|
|
74
|
+
throw new Error("grid columns and rows cannot be 0");
|
|
75
|
+
let unit = bound.size.$subtract(1).$divide(columns, rows);
|
|
76
|
+
let offset = unit.$multiply(orientation);
|
|
77
|
+
let g = new Group();
|
|
78
|
+
for (let r = 0; r < rows; r++) {
|
|
79
|
+
for (let c = 0; c < columns; c++) {
|
|
80
|
+
g.push(bound.topLeft.$add(unit.$multiply(c, r)).add(offset));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return g;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a grid of cells inside a boundary, where each cell is defined by a group of 2 Pt.
|
|
88
|
+
* @param bound the rectangular boundary
|
|
89
|
+
* @param columns number of columns
|
|
90
|
+
* @param rows number of rows
|
|
91
|
+
* @returns an array of Groups, where each group represents a rectangular cell
|
|
92
|
+
*/
|
|
93
|
+
static gridCells(bound: Bound, columns: number, rows: number): Group[] {
|
|
94
|
+
if (columns === 0 || rows === 0)
|
|
95
|
+
throw new Error("grid columns and rows cannot be 0");
|
|
96
|
+
let unit = bound.size.$subtract(1).divide(columns, rows); // subtract 1 to fill whole border of rectangles
|
|
97
|
+
let g = [];
|
|
98
|
+
for (let r = 0; r < rows; r++) {
|
|
99
|
+
for (let c = 0; c < columns; c++) {
|
|
100
|
+
g.push(
|
|
101
|
+
new Group(
|
|
102
|
+
bound.topLeft.$add(unit.$multiply(c, r)),
|
|
103
|
+
bound.topLeft.$add(unit.$multiply(c, r).add(unit)),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return g;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Create a set of Pts around a circular path.
|
|
113
|
+
* @param center circle center
|
|
114
|
+
* @param radius circle radius
|
|
115
|
+
* @param count number of Pts to create
|
|
116
|
+
* @param angleOffset offset starting angle
|
|
117
|
+
*/
|
|
118
|
+
static radialPts(
|
|
119
|
+
center: PtLike,
|
|
120
|
+
radius: number,
|
|
121
|
+
count: number,
|
|
122
|
+
angleOffset: number = -Const.half_pi,
|
|
123
|
+
): Group {
|
|
124
|
+
let g = new Group();
|
|
125
|
+
let a = Const.two_pi / count;
|
|
126
|
+
for (let i = 0; i < count; i++) {
|
|
127
|
+
g.push(new Pt(center).toAngle(a * i + angleOffset, radius, true));
|
|
128
|
+
}
|
|
129
|
+
return g;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Given a group of Pts, return a new group of `Noise` Pts.
|
|
134
|
+
* @param pts a Group or an Iterable<Pt>, in row-major order when treated as a grid
|
|
135
|
+
* @param dx small increment value in x dimension
|
|
136
|
+
* @param dy small increment value in y dimension
|
|
137
|
+
* @param rows Optional row count to generate 2D noise
|
|
138
|
+
* @param columns Optional column count (points per row) to generate 2D noise. When provided, each point's noise offset is (dx·column, dy·row) with row = floor(i/columns); when only `rows` is provided it is used as the points-per-row divisor instead.
|
|
139
|
+
*/
|
|
140
|
+
static noisePts(
|
|
141
|
+
pts: PtIterable,
|
|
142
|
+
dx = 0.01,
|
|
143
|
+
dy = 0.01,
|
|
144
|
+
rows = 0,
|
|
145
|
+
columns = 0,
|
|
146
|
+
): Group {
|
|
147
|
+
let seed = Num.random();
|
|
148
|
+
let g = new Group();
|
|
149
|
+
let i = 0;
|
|
150
|
+
// row-major grid: one consistent per-row divisor for both row and column
|
|
151
|
+
const perRow = columns > 0 ? columns : rows > 0 ? rows : 0;
|
|
152
|
+
for (let p of pts) {
|
|
153
|
+
let np = new Noise(p);
|
|
154
|
+
let r = perRow > 0 ? Math.floor(i / perRow) : i;
|
|
155
|
+
let c = perRow > 0 ? i % perRow : i;
|
|
156
|
+
np.initNoise(dx * c, dy * r);
|
|
157
|
+
np.seed(seed);
|
|
158
|
+
g.push(np);
|
|
159
|
+
i++;
|
|
160
|
+
}
|
|
161
|
+
return g;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a Delaunay Group. Use the [`Delaunay.delaunay()`](#link) and [`Delaunay.voronoi()`](#link) functions in the returned group to generate tessellations.
|
|
166
|
+
* @param pts a Group or an array of Pts
|
|
167
|
+
* @returns an instance of the Delaunay class
|
|
168
|
+
*/
|
|
169
|
+
static delaunay(pts: GroupLike): Delaunay {
|
|
170
|
+
return Delaunay.from(pts) as Delaunay;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create a [`Flock`](#link) of [`Boid`](#link) agents that simulate flocking (also known as "boids"),
|
|
175
|
+
* where each agent steers by three local rules: separation, alignment, and cohesion.
|
|
176
|
+
* Advance the simulation by calling [`Flock.step`](#link) with the elapsed time.
|
|
177
|
+
* See a [flocking demo here](https://ptsjs.org/demo/?name=create.flock).
|
|
178
|
+
*
|
|
179
|
+
* Each agent starts with a random heading, drawn from [`Num.random`](#link), so seeding with
|
|
180
|
+
* [`Num.seed`](#link) makes a flock reproducible.
|
|
181
|
+
*
|
|
182
|
+
* @param pts a Group or an Iterable<Pt> of starting positions
|
|
183
|
+
* @param options optional [`FlockOptions`](#link) to tune the behavior
|
|
184
|
+
* @returns an instance of the Flock class, which is a Group of Boids
|
|
185
|
+
* @example `Create.flock( Create.distributeRandom( space.innerBound, 200 ), { bound: space.innerBound } )`
|
|
186
|
+
*/
|
|
187
|
+
static flock(pts: PtLikeIterable, options: FlockOptions = {}): Flock {
|
|
188
|
+
const flock = new Flock();
|
|
189
|
+
flock.setup(options);
|
|
190
|
+
for (const p of pts) flock.addBoid(p);
|
|
191
|
+
return flock;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Perlin noise gradient indices
|
|
197
|
+
*/
|
|
198
|
+
const __noise_grad3 = [
|
|
199
|
+
[1, 1, 0],
|
|
200
|
+
[-1, 1, 0],
|
|
201
|
+
[1, -1, 0],
|
|
202
|
+
[-1, -1, 0],
|
|
203
|
+
[1, 0, 1],
|
|
204
|
+
[-1, 0, 1],
|
|
205
|
+
[1, 0, -1],
|
|
206
|
+
[-1, 0, -1],
|
|
207
|
+
[0, 1, 1],
|
|
208
|
+
[0, -1, 1],
|
|
209
|
+
[0, 1, -1],
|
|
210
|
+
[0, -1, -1],
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Perlin noise permutation table
|
|
215
|
+
*/
|
|
216
|
+
const __noise_permTable = [
|
|
217
|
+
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140,
|
|
218
|
+
36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234,
|
|
219
|
+
75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237,
|
|
220
|
+
149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48,
|
|
221
|
+
27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105,
|
|
222
|
+
92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73,
|
|
223
|
+
209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86,
|
|
224
|
+
164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38,
|
|
225
|
+
147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189,
|
|
226
|
+
28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101,
|
|
227
|
+
155, 167, 43, 172, 9, 129, 22, 39, 253, 9, 98, 108, 110, 79, 113, 224, 232,
|
|
228
|
+
178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12,
|
|
229
|
+
191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31,
|
|
230
|
+
181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
|
|
231
|
+
138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215,
|
|
232
|
+
61, 156, 180,
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
// The doubled base permutation table, built once and shared by every unseeded
|
|
236
|
+
// Noise instance (a per-instance copy would allocate 512 entries per point in
|
|
237
|
+
// `Create.noisePts`). `seed()` swaps in a seeded table instead of mutating.
|
|
238
|
+
const __noise_permDoubled = __noise_permTable.concat(__noise_permTable);
|
|
239
|
+
|
|
240
|
+
// Memoize the last seeded table: `Create.noisePts` seeds every point with the
|
|
241
|
+
// same value, so all its Noise Pts share one table.
|
|
242
|
+
let __noise_lastSeed: number | undefined = undefined;
|
|
243
|
+
let __noise_lastPerm: number[] | null = null;
|
|
244
|
+
|
|
245
|
+
function __noise_seededPerm(seed: number): number[] {
|
|
246
|
+
if (seed === __noise_lastSeed && __noise_lastPerm) return __noise_lastPerm;
|
|
247
|
+
|
|
248
|
+
let s = seed;
|
|
249
|
+
if (s > 0 && s < 1) s *= 65536;
|
|
250
|
+
s = Math.floor(s);
|
|
251
|
+
if (s < 256) s |= s << 8;
|
|
252
|
+
|
|
253
|
+
const perm = new Array<number>(512);
|
|
254
|
+
for (let i = 0; i < 256; i++) {
|
|
255
|
+
const v =
|
|
256
|
+
i & 1
|
|
257
|
+
? __noise_permTable[i] ^ (s & 255)
|
|
258
|
+
: __noise_permTable[i] ^ ((s >> 8) & 255);
|
|
259
|
+
perm[i] = perm[i + 256] = v;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
__noise_lastSeed = seed;
|
|
263
|
+
__noise_lastPerm = perm;
|
|
264
|
+
return perm;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Noise is a subclass of Pt that generates Perlin noise. Current implementation supports basic 2D noise.
|
|
269
|
+
* This implementation is based on this [gist](https://gist.github.com/banksean/304522).
|
|
270
|
+
*/
|
|
271
|
+
export class Noise extends Pt {
|
|
272
|
+
protected perm: number[] = [];
|
|
273
|
+
private _n: Pt = new Pt(0.01, 0.01);
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Create a Noise Pt that can generate noise continuously. See a [Noise demo here](https://ptsjs.org/demo/?name=create.noisePts).
|
|
277
|
+
* @param args a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties
|
|
278
|
+
*/
|
|
279
|
+
constructor(...args: any[]) {
|
|
280
|
+
super(...args);
|
|
281
|
+
|
|
282
|
+
// shared doubled table for easy index wrapping; replaced by seed()
|
|
283
|
+
this.perm = __noise_permDoubled;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Set the initial dimensional values of the noise.
|
|
288
|
+
* @param args a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties
|
|
289
|
+
* @example `noise.initNoise( 0.01, 0.1 )`
|
|
290
|
+
*/
|
|
291
|
+
initNoise(...args: any[]) {
|
|
292
|
+
this._n = new Pt(...args);
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Add a small increment to the noise values.
|
|
298
|
+
* @param x step in x dimension
|
|
299
|
+
* @param y step in y dimension
|
|
300
|
+
*/
|
|
301
|
+
step(x = 0, y = 0) {
|
|
302
|
+
this._n.add(x, y);
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Specify a seed for this Noise.
|
|
308
|
+
* @param s seed value
|
|
309
|
+
*/
|
|
310
|
+
seed(s: number) {
|
|
311
|
+
this.perm = __noise_seededPerm(s);
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Generate a 2D Perlin noise value.
|
|
317
|
+
*/
|
|
318
|
+
noise2D() {
|
|
319
|
+
const perm = this.perm;
|
|
320
|
+
const nx = this._n[0];
|
|
321
|
+
const ny = this._n[1];
|
|
322
|
+
|
|
323
|
+
// integer cell (wrapped to the table via two's-complement &, which also
|
|
324
|
+
// handles negative coordinates seamlessly) and the position within it
|
|
325
|
+
const cx = Math.floor(nx);
|
|
326
|
+
const cy = Math.floor(ny);
|
|
327
|
+
const i = cx & 255;
|
|
328
|
+
const j = cy & 255;
|
|
329
|
+
const x = nx - cx;
|
|
330
|
+
const y = ny - cy;
|
|
331
|
+
|
|
332
|
+
// standard Perlin gradient hashing through the permutation table; the
|
|
333
|
+
// doubled table makes i + perm[j + 1] safe without extra wrapping
|
|
334
|
+
const g00 = __noise_grad3[perm[i + perm[j]] % 12];
|
|
335
|
+
const g01 = __noise_grad3[perm[i + perm[j + 1]] % 12];
|
|
336
|
+
const g10 = __noise_grad3[perm[i + 1 + perm[j]] % 12];
|
|
337
|
+
const g11 = __noise_grad3[perm[i + 1 + perm[j + 1]] % 12];
|
|
338
|
+
|
|
339
|
+
const n00 = g00[0] * x + g00[1] * y;
|
|
340
|
+
const n01 = g01[0] * x + g01[1] * (y - 1);
|
|
341
|
+
const n10 = g10[0] * (x - 1) + g10[1] * y;
|
|
342
|
+
const n11 = g11[0] * (x - 1) + g11[1] * (y - 1);
|
|
343
|
+
|
|
344
|
+
const _fade = (f: number) => f * f * f * (f * (f * 6 - 15) + 10);
|
|
345
|
+
const tx = _fade(x);
|
|
346
|
+
const u = n00 + tx * (n10 - n00);
|
|
347
|
+
const v = n01 + tx * (n11 - n01);
|
|
348
|
+
return u + _fade(y) * (v - u);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Clip a convex cell polygon against an axis-aligned rectangle
|
|
354
|
+
* (Sutherland–Hodgman). Returns the input Group unchanged (shared Pt
|
|
355
|
+
* references) when every vertex is already inside.
|
|
356
|
+
*/
|
|
357
|
+
function _clipCellToRect(
|
|
358
|
+
cell: Group,
|
|
359
|
+
x0: number,
|
|
360
|
+
y0: number,
|
|
361
|
+
x1: number,
|
|
362
|
+
y1: number,
|
|
363
|
+
): Group {
|
|
364
|
+
// fast path: fully inside
|
|
365
|
+
let inside = true;
|
|
366
|
+
for (let i = 0, len = cell.length; i < len; i++) {
|
|
367
|
+
const px = cell[i][0];
|
|
368
|
+
const py = cell[i][1];
|
|
369
|
+
if (px < x0 || px > x1 || py < y0 || py > y1 || !Number.isFinite(px + py)) {
|
|
370
|
+
inside = false;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (inside) return cell;
|
|
375
|
+
|
|
376
|
+
// degenerate hull fragments (1-2 vertices) cannot form a polygon to clip;
|
|
377
|
+
// keep only their in-bound vertices
|
|
378
|
+
if (cell.length < 3) {
|
|
379
|
+
const kept = new Group();
|
|
380
|
+
for (let i = 0, len = cell.length; i < len; i++) {
|
|
381
|
+
const px = cell[i][0];
|
|
382
|
+
const py = cell[i][1];
|
|
383
|
+
if (px >= x0 && px <= x1 && py >= y0 && py <= y1) kept.push(cell[i]);
|
|
384
|
+
}
|
|
385
|
+
return kept;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// clamp non-finite coordinates so intersection math stays finite
|
|
389
|
+
const big = 1e7;
|
|
390
|
+
let pts: number[][] = [];
|
|
391
|
+
for (let i = 0, len = cell.length; i < len; i++) {
|
|
392
|
+
let px = cell[i][0];
|
|
393
|
+
let py = cell[i][1];
|
|
394
|
+
if (!Number.isFinite(px)) px = px > 0 ? big : -big;
|
|
395
|
+
if (!Number.isFinite(py)) py = py > 0 ? big : -big;
|
|
396
|
+
if (Number.isNaN(px) || Number.isNaN(py)) continue;
|
|
397
|
+
pts.push([px, py]);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// clip against each rect edge: keep(p) tests inside, cross(a,b) intersects
|
|
401
|
+
const clip = (
|
|
402
|
+
input: number[][],
|
|
403
|
+
keep: (p: number[]) => boolean,
|
|
404
|
+
cross: (a: number[], b: number[]) => number[],
|
|
405
|
+
): number[][] => {
|
|
406
|
+
const output: number[][] = [];
|
|
407
|
+
for (let i = 0, len = input.length; i < len; i++) {
|
|
408
|
+
const a = input[i === 0 ? len - 1 : i - 1];
|
|
409
|
+
const b = input[i];
|
|
410
|
+
const keepB = keep(b);
|
|
411
|
+
if (keep(a)) {
|
|
412
|
+
if (keepB) output.push(b);
|
|
413
|
+
else output.push(cross(a, b));
|
|
414
|
+
} else if (keepB) {
|
|
415
|
+
output.push(cross(a, b), b);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return output;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const lerpAt = (a: number[], b: number[], t: number): number[] => [
|
|
422
|
+
a[0] + (b[0] - a[0]) * t,
|
|
423
|
+
a[1] + (b[1] - a[1]) * t,
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
pts = clip(
|
|
427
|
+
pts,
|
|
428
|
+
(p) => p[0] >= x0,
|
|
429
|
+
(a, b) => lerpAt(a, b, (x0 - a[0]) / (b[0] - a[0])),
|
|
430
|
+
);
|
|
431
|
+
pts = clip(
|
|
432
|
+
pts,
|
|
433
|
+
(p) => p[0] <= x1,
|
|
434
|
+
(a, b) => lerpAt(a, b, (x1 - a[0]) / (b[0] - a[0])),
|
|
435
|
+
);
|
|
436
|
+
pts = clip(
|
|
437
|
+
pts,
|
|
438
|
+
(p) => p[1] >= y0,
|
|
439
|
+
(a, b) => lerpAt(a, b, (y0 - a[1]) / (b[1] - a[1])),
|
|
440
|
+
);
|
|
441
|
+
pts = clip(
|
|
442
|
+
pts,
|
|
443
|
+
(p) => p[1] <= y1,
|
|
444
|
+
(a, b) => lerpAt(a, b, (y1 - a[1]) / (b[1] - a[1])),
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const out = new Group();
|
|
448
|
+
for (let i = 0, len = pts.length; i < len; i++) {
|
|
449
|
+
out.push(new Pt(pts[i]));
|
|
450
|
+
}
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Keep the part of a convex cell nearer to site a than site b. */
|
|
455
|
+
function _clipCellToBisector(cell: Group, a: Pt, b: Pt): Group {
|
|
456
|
+
const dx = b[0] - a[0];
|
|
457
|
+
const dy = b[1] - a[1];
|
|
458
|
+
const mx = a[0] + dx / 2;
|
|
459
|
+
const my = a[1] + dy / 2;
|
|
460
|
+
const out = new Group();
|
|
461
|
+
for (let i = 0; i < cell.length; i++) {
|
|
462
|
+
const p = cell[i === 0 ? cell.length - 1 : i - 1];
|
|
463
|
+
const q = cell[i];
|
|
464
|
+
const dp = (p[0] - mx) * dx + (p[1] - my) * dy;
|
|
465
|
+
const dq = (q[0] - mx) * dx + (q[1] - my) * dy;
|
|
466
|
+
if (dp <= 0 !== dq <= 0) {
|
|
467
|
+
const t = dp / (dp - dq);
|
|
468
|
+
out.push(new Pt(p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t));
|
|
469
|
+
}
|
|
470
|
+
if (dq <= 0) out.push(q);
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Circumcenter and radius of a triangle, as `[x, y, r]`. */
|
|
476
|
+
function _circumcircle(
|
|
477
|
+
ax: number,
|
|
478
|
+
ay: number,
|
|
479
|
+
bx: number,
|
|
480
|
+
by: number,
|
|
481
|
+
cx: number,
|
|
482
|
+
cy: number,
|
|
483
|
+
): [number, number, number] {
|
|
484
|
+
const bx2 = bx - ax;
|
|
485
|
+
const by2 = by - ay;
|
|
486
|
+
const cx2 = cx - ax;
|
|
487
|
+
const cy2 = cy - ay;
|
|
488
|
+
const d = 2 * (bx2 * cy2 - by2 * cx2);
|
|
489
|
+
const bl = bx2 * bx2 + by2 * by2;
|
|
490
|
+
const cl = cx2 * cx2 + cy2 * cy2;
|
|
491
|
+
const ux = (cy2 * bl - by2 * cl) / d;
|
|
492
|
+
const uy = (bx2 * cl - cx2 * bl) / d;
|
|
493
|
+
return [ax + ux, ay + uy, Math.sqrt(ux * ux + uy * uy)];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Delaunay is a [`Group`](#link) of Pts that generates Delaunay and Voronoi tessellations.
|
|
498
|
+
* Points are triangulated by incremental insertion in Hilbert-curve order with exact
|
|
499
|
+
* orientation and in-circle tests, so grids, collinear runs, points on edges, and duplicate
|
|
500
|
+
* points are handled without degenerate triangles.
|
|
501
|
+
*/
|
|
502
|
+
export class Delaunay extends Group {
|
|
503
|
+
private _mesh: DelaunayMesh = [];
|
|
504
|
+
private _meshBuilt = true;
|
|
505
|
+
private _count = 0;
|
|
506
|
+
private _tri: Triangulation | null = null;
|
|
507
|
+
private _shapes: DelaunayShape[] | null = null;
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Generate Delaunay triangles. This function also caches the mesh that is used to generate Voronoi tessellation in `voronoi()`. See a [Delaunay demo here](https://ptsjs.org/demo/?name=create.delaunay).
|
|
511
|
+
* @param triangleOnly if true, returns an array of triangles in Groups, otherwise return the whole DelaunayShape
|
|
512
|
+
* @returns an array of Groups or an array of DelaunayShapes `{i, j, k, triangle, circle}` which records the indices of the vertices, and the calculated triangles and circumcircles
|
|
513
|
+
*/
|
|
514
|
+
delaunay(triangleOnly: boolean = true): GroupLike[] | DelaunayShape[] {
|
|
515
|
+
const n = this.length;
|
|
516
|
+
this._count = n;
|
|
517
|
+
this._mesh = [];
|
|
518
|
+
this._meshBuilt = false;
|
|
519
|
+
this._tri = null;
|
|
520
|
+
this._shapes = null;
|
|
521
|
+
if (n < 3) return [];
|
|
522
|
+
|
|
523
|
+
const coords = new Float64Array(n * 2);
|
|
524
|
+
for (let i = 0; i < n; i++) {
|
|
525
|
+
coords[2 * i] = this[i][0];
|
|
526
|
+
coords[2 * i + 1] = this[i][1];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const tri = triangulate(coords, n);
|
|
530
|
+
if (tri.triangles.length === 0) return [];
|
|
531
|
+
this._tri = tri;
|
|
532
|
+
const indices = tri.triangles;
|
|
533
|
+
|
|
534
|
+
const shapes: DelaunayShape[] = [];
|
|
535
|
+
const tris: GroupLike[] = [];
|
|
536
|
+
for (let t = 0, len = indices.length; t < len; t += 3) {
|
|
537
|
+
const i = indices[t];
|
|
538
|
+
const j = indices[t + 1];
|
|
539
|
+
const k = indices[t + 2];
|
|
540
|
+
const triangle = this._triangle(i, j, k);
|
|
541
|
+
// scalar circumcircle, matching the shape of `Triangle.circumcircle`
|
|
542
|
+
const [ccx, ccy, r] = _circumcircle(
|
|
543
|
+
coords[2 * i],
|
|
544
|
+
coords[2 * i + 1],
|
|
545
|
+
coords[2 * j],
|
|
546
|
+
coords[2 * j + 1],
|
|
547
|
+
coords[2 * k],
|
|
548
|
+
coords[2 * k + 1],
|
|
549
|
+
);
|
|
550
|
+
const circle = new Group(new Pt(ccx, ccy), new Pt(r, r));
|
|
551
|
+
|
|
552
|
+
shapes.push({ i, j, k, triangle, circle });
|
|
553
|
+
tris.push(triangle);
|
|
554
|
+
}
|
|
555
|
+
this._shapes = shapes;
|
|
556
|
+
|
|
557
|
+
return triangleOnly ? tris : shapes;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* The per-point mesh cache is keyed by neighbor-pair strings, which costs
|
|
562
|
+
* more than the triangulation itself; build it the first time it is read.
|
|
563
|
+
*/
|
|
564
|
+
private _ensureMesh(): DelaunayMesh {
|
|
565
|
+
if (!this._meshBuilt) {
|
|
566
|
+
this._meshBuilt = true;
|
|
567
|
+
this._mesh = [];
|
|
568
|
+
for (let i = 0; i < this._count; i++) this._mesh[i] = {};
|
|
569
|
+
if (this._shapes) {
|
|
570
|
+
for (let s = 0, len = this._shapes.length; s < len; s++) {
|
|
571
|
+
this._cache(this._shapes[s]);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return this._mesh;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Generate Voronoi cells. `delaunay()` must be called before calling this function. See a [Voronoi demo here](https://ptsjs.org/demo/?name=create.delaunay).
|
|
580
|
+
* @param bound Optionally provide a rectangular bound (eg, `space.innerBound`) to clip the cells against, including the unbounded cells on the convex hull.
|
|
581
|
+
* Without a bound, cells around sliver triangles can extend to enormous coordinates (circumcenters of
|
|
582
|
+
* nearly-collinear points), which is technically correct but extremely slow to draw.
|
|
583
|
+
* @returns an array of Groups, each of which represents a Voronoi cell. Unclipped cells share their vertex Pts with the cached mesh (see [`Delaunay.mesh`](#link)), so treat them as read-only or clone before mutating.
|
|
584
|
+
*/
|
|
585
|
+
voronoi(bound?: PtIterable): Group[] {
|
|
586
|
+
const cells = this._voronoiCells();
|
|
587
|
+
if (!bound) return cells;
|
|
588
|
+
|
|
589
|
+
const _bound = Geom.boundingBox(Util.iterToArray(bound) as Group);
|
|
590
|
+
const x0 = _bound[0][0];
|
|
591
|
+
const y0 = _bound[0][1];
|
|
592
|
+
const x1 = _bound[1][0];
|
|
593
|
+
const y1 = _bound[1][1];
|
|
594
|
+
const hull = new Set<number>();
|
|
595
|
+
if (this._tri) for (const i of this._tri.hull) hull.add(i);
|
|
596
|
+
const seen = new Set<string>();
|
|
597
|
+
for (let i = 0, len = cells.length; i < len; i++) {
|
|
598
|
+
const key = `${this[i][0]},${this[i][1]}`;
|
|
599
|
+
if (seen.has(key)) {
|
|
600
|
+
cells[i] = new Group();
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
seen.add(key);
|
|
604
|
+
if (!hull.has(i) && cells[i].length >= 3) {
|
|
605
|
+
cells[i] = _clipCellToRect(cells[i], x0, y0, x1, y1);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Hull fans do not enclose their unbounded Voronoi regions. Start
|
|
610
|
+
// with the bound and intersect the half-planes of neighboring sites.
|
|
611
|
+
const neighbors = new Set<number>();
|
|
612
|
+
for (const shape of this.neighbors(i)) {
|
|
613
|
+
for (const index of [shape.i, shape.j, shape.k]) {
|
|
614
|
+
if (index !== i) neighbors.add(index);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// Collinear and small point sets have no triangles; every site is a
|
|
618
|
+
// candidate neighbor. Duplicate sites leave the first cell unchanged.
|
|
619
|
+
if (neighbors.size === 0) {
|
|
620
|
+
for (let j = 0; j < this.length; j++) if (j !== i) neighbors.add(j);
|
|
621
|
+
}
|
|
622
|
+
let cell = Group.fromArray([
|
|
623
|
+
[x0, y0],
|
|
624
|
+
[x1, y0],
|
|
625
|
+
[x1, y1],
|
|
626
|
+
[x0, y1],
|
|
627
|
+
]);
|
|
628
|
+
for (const j of neighbors)
|
|
629
|
+
cell = _clipCellToBisector(cell, this[i], this[j]);
|
|
630
|
+
cells[i] = cell;
|
|
631
|
+
}
|
|
632
|
+
return cells;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Assemble unclipped Voronoi cells. */
|
|
636
|
+
private _voronoiCells(): Group[] {
|
|
637
|
+
const tri = this._tri;
|
|
638
|
+
const shapes = this._shapes;
|
|
639
|
+
if (!tri || !shapes) {
|
|
640
|
+
// fallback (eg, subclasses bypassing delaunay()): sort per cell
|
|
641
|
+
const vs: Group[] = [];
|
|
642
|
+
const n = this._ensureMesh();
|
|
643
|
+
for (let i = 0, len = n.length; i < len; i++) {
|
|
644
|
+
vs.push(this.neighborPts(i, true) as Group);
|
|
645
|
+
}
|
|
646
|
+
return vs;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Walk the triangles around each point counterclockwise, so its
|
|
650
|
+
// circumcenters come out already in polygon order. A hull point's fan is
|
|
651
|
+
// open: start it at the triangle whose outgoing edge is on the hull.
|
|
652
|
+
const n = this._count;
|
|
653
|
+
const triangles = tri.triangles;
|
|
654
|
+
const neighbors = tri.neighbors;
|
|
655
|
+
const start = new Int32Array(n).fill(-1);
|
|
656
|
+
for (let h = 0, len = triangles.length; h < len; h++) {
|
|
657
|
+
const p = triangles[h];
|
|
658
|
+
if (neighbors[h] === -1 || start[p] === -1) start[p] = h;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const vs: Group[] = [];
|
|
662
|
+
for (let i = 0; i < n; i++) {
|
|
663
|
+
const cell = new Group();
|
|
664
|
+
let h = start[i];
|
|
665
|
+
if (h !== -1) {
|
|
666
|
+
const first = (h / 3) | 0;
|
|
667
|
+
for (;;) {
|
|
668
|
+
const t = (h / 3) | 0;
|
|
669
|
+
cell.push(shapes[t].circle[0]);
|
|
670
|
+
// the edge entering this point leads to the next triangle around it
|
|
671
|
+
const twin = neighbors[3 * t + ((h + 2) % 3)];
|
|
672
|
+
if (twin === -1 || ((twin / 3) | 0) === first) break;
|
|
673
|
+
h = twin;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
vs.push(cell);
|
|
677
|
+
}
|
|
678
|
+
return vs;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get the cached mesh. The mesh is an array of objects, each of which representing the enclosing triangles around a Pt in this Delaunay group.
|
|
683
|
+
* @return an array of objects that store a series of DelaunayShapes
|
|
684
|
+
*/
|
|
685
|
+
mesh(): DelaunayMesh {
|
|
686
|
+
return this._ensureMesh();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Given an index of a Pt in this Delaunay Group, returns its neighboring Pts in the network.
|
|
691
|
+
* @param i index of a Pt
|
|
692
|
+
* @param sort if true, sort the neighbors so that their edges will form a polygon
|
|
693
|
+
* @returns an array of Pts
|
|
694
|
+
*/
|
|
695
|
+
neighborPts(i: number, sort = false): GroupLike {
|
|
696
|
+
let cs = new Group();
|
|
697
|
+
let n = this._ensureMesh();
|
|
698
|
+
for (let k in n[i]) {
|
|
699
|
+
if (n[i].hasOwnProperty(k)) cs.push(n[i][k].circle[0]);
|
|
700
|
+
}
|
|
701
|
+
return sort && cs.length > 1 ? Geom.sortEdges(cs) : cs;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Given an index of a Pt in this Delaunay Group, returns its neighboring DelaunayShapes.
|
|
706
|
+
* @param i index of a Pt
|
|
707
|
+
* @returns an array of DelaunayShapes `{i, j, k, triangle, circle}`
|
|
708
|
+
*/
|
|
709
|
+
neighbors(i: number): DelaunayShape[] {
|
|
710
|
+
let cs = [];
|
|
711
|
+
let n = this._ensureMesh();
|
|
712
|
+
for (let k in n[i]) {
|
|
713
|
+
if (n[i].hasOwnProperty(k)) cs.push(n[i][k]);
|
|
714
|
+
}
|
|
715
|
+
return cs;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Record a DelaunayShape in the mesh.
|
|
720
|
+
* @param o DelaunayShape instance
|
|
721
|
+
*/
|
|
722
|
+
protected _cache(o: DelaunayShape): void {
|
|
723
|
+
this._mesh[o.i][`${Math.min(o.j, o.k)}-${Math.max(o.j, o.k)}`] = o;
|
|
724
|
+
this._mesh[o.j][`${Math.min(o.i, o.k)}-${Math.max(o.i, o.k)}`] = o;
|
|
725
|
+
this._mesh[o.k][`${Math.min(o.i, o.j)}-${Math.max(o.i, o.j)}`] = o;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Get the initial "super triangle" that contains all the points in this set.
|
|
730
|
+
* Not used by the current triangulation core; kept for subclass compatibility.
|
|
731
|
+
* @returns a Group representing a triangle
|
|
732
|
+
*/
|
|
733
|
+
protected _superTriangle(): Group {
|
|
734
|
+
let minPt = this[0];
|
|
735
|
+
let maxPt = this[0];
|
|
736
|
+
for (let i = 1, len = this.length; i < len; i++) {
|
|
737
|
+
minPt = minPt.$min(this[i]);
|
|
738
|
+
maxPt = maxPt.$max(this[i]);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
let d = maxPt.$subtract(minPt);
|
|
742
|
+
let mid = minPt.$add(maxPt).divide(2);
|
|
743
|
+
let dmax = Math.max(d[0], d[1]);
|
|
744
|
+
|
|
745
|
+
return new Group(
|
|
746
|
+
mid.$subtract(20 * dmax, dmax),
|
|
747
|
+
mid.$add(0, 20 * dmax),
|
|
748
|
+
mid.$add(20 * dmax, -dmax),
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Get a triangle from 3 points in a list of points
|
|
754
|
+
* @param i index 1
|
|
755
|
+
* @param j index 2
|
|
756
|
+
* @param k index 3
|
|
757
|
+
* @param pts a Group of Pts
|
|
758
|
+
*/
|
|
759
|
+
protected _triangle(
|
|
760
|
+
i: number,
|
|
761
|
+
j: number,
|
|
762
|
+
k: number,
|
|
763
|
+
pts: GroupLike = this,
|
|
764
|
+
): Group {
|
|
765
|
+
return new Group(pts[i], pts[j], pts[k]);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Get a circumcircle and triangle from 3 points in a list of points
|
|
770
|
+
* @param i index 1
|
|
771
|
+
* @param j index 2
|
|
772
|
+
* @param k index 3
|
|
773
|
+
* @param tri a Group representing a triangle, or `false` to create it from indices
|
|
774
|
+
* @param pts a Group of Pts
|
|
775
|
+
*/
|
|
776
|
+
protected _circum(
|
|
777
|
+
i: number,
|
|
778
|
+
j: number,
|
|
779
|
+
k: number,
|
|
780
|
+
tri: GroupLike | false,
|
|
781
|
+
pts: GroupLike = this,
|
|
782
|
+
): DelaunayShape {
|
|
783
|
+
let t = tri || this._triangle(i, j, k, pts);
|
|
784
|
+
return {
|
|
785
|
+
i: i,
|
|
786
|
+
j: j,
|
|
787
|
+
k: k,
|
|
788
|
+
triangle: t,
|
|
789
|
+
circle: Triangle.circumcircle(t)!,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Dedupe the edges array
|
|
795
|
+
* @param edges
|
|
796
|
+
*/
|
|
797
|
+
protected static _dedupe(edges: number[]): number[] {
|
|
798
|
+
let j = edges.length;
|
|
799
|
+
|
|
800
|
+
while (j > 1) {
|
|
801
|
+
let b = edges[--j];
|
|
802
|
+
let a = edges[--j];
|
|
803
|
+
let i = j;
|
|
804
|
+
|
|
805
|
+
while (i > 1) {
|
|
806
|
+
let n = edges[--i];
|
|
807
|
+
let m = edges[--i];
|
|
808
|
+
|
|
809
|
+
if ((a == m && b == n) || (a == n && b == m)) {
|
|
810
|
+
edges.splice(j, 2);
|
|
811
|
+
edges.splice(i, 2);
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return edges;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Accumulate one behavior's contribution as a weighted unit vector.
|
|
823
|
+
*
|
|
824
|
+
* The three behaviors are blended as *directions* and only then turned into a single steering
|
|
825
|
+
* force, rather than each producing its own `desired - velocity` steer that are then summed.
|
|
826
|
+
* Summing them would apply the `- velocity` damping term once per behavior, so with weights
|
|
827
|
+
* totalling ~3.8 an agent is dragged toward a standstill whenever the behaviors disagree —
|
|
828
|
+
* which is exactly what happens inside a tight cluster, where cohesion and separation nearly
|
|
829
|
+
* oppose. Blending first also bounds separation structurally: its inverse-square sum is
|
|
830
|
+
* normalized here, so no amount of piled-up neighbors can dominate the result.
|
|
831
|
+
*
|
|
832
|
+
* Does nothing when the direction is degenerate (zero length), which happens whenever neighbor
|
|
833
|
+
* velocities cancel out or an agent sits exactly on its neighbors' center.
|
|
834
|
+
*/
|
|
835
|
+
function __flock_accumulateUnit(
|
|
836
|
+
out: Float32Array,
|
|
837
|
+
dx: number,
|
|
838
|
+
dy: number,
|
|
839
|
+
weight: number,
|
|
840
|
+
): void {
|
|
841
|
+
const mag2 = dx * dx + dy * dy;
|
|
842
|
+
if (mag2 <= 0) return;
|
|
843
|
+
const scale = weight / Math.sqrt(mag2);
|
|
844
|
+
out[0] += dx * scale;
|
|
845
|
+
out[1] += dy * scale;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Two agents at the exact same position have no direction to separate along. Rather than
|
|
850
|
+
* leaving them fused forever, separation falls back to this offset, which the antisymmetric
|
|
851
|
+
* accumulation splits into +x for one agent and -x for the other.
|
|
852
|
+
*/
|
|
853
|
+
const __flock_tieBreak = 1;
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Separation weights each neighbor by inverse-square distance, so a pair that is nearly
|
|
857
|
+
* coincident would produce an unbounded force. Distances are clamped to this fraction of the
|
|
858
|
+
* separation radius. (The per-behavior `maxForce` clamp is what ultimately bounds the sum.)
|
|
859
|
+
*/
|
|
860
|
+
const __flock_minSepFraction = 0.01;
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Cell indices are hashed through `Math.imul`, which truncates to int32, so anything past this
|
|
864
|
+
* carries no information. It is also the guard that keeps the 3x3 cell walk terminating: a
|
|
865
|
+
* non-finite position yields a non-finite cell index, and `gy <= cy + 1` is either always true
|
|
866
|
+
* (Infinity) or leaves `gy++` unable to advance (magnitudes where adding 1 is a no-op in
|
|
867
|
+
* float64). The comparison is written so NaN fails it too.
|
|
868
|
+
*/
|
|
869
|
+
const __flock_maxCell = 0x7fffffff;
|
|
870
|
+
/** Cell coordinate of an agent with no finite neighborhood; below every valid cell. */
|
|
871
|
+
const __flock_noCell = -0x80000000;
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Boid is a subclass of [`Pt`](#link) that represents a single agent in a [`Flock`](#link).
|
|
875
|
+
* Its own values are the agent's position, and it carries a `velocity` that [`Flock.step`](#link)
|
|
876
|
+
* integrates. Create them through [`Create.flock`](#link) or [`Flock.addBoid`](#link).
|
|
877
|
+
* See [a demo here](https://ptsjs.org/demo/?name=create.flock).
|
|
878
|
+
*/
|
|
879
|
+
export class Boid extends Pt {
|
|
880
|
+
protected _vel: Pt = new Pt(0, 0);
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* This agent's velocity, in units per second.
|
|
884
|
+
*/
|
|
885
|
+
get velocity(): Pt {
|
|
886
|
+
return this._vel;
|
|
887
|
+
}
|
|
888
|
+
set velocity(v: Pt) {
|
|
889
|
+
this._vel = v;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* This agent's speed, in units per second.
|
|
894
|
+
*/
|
|
895
|
+
get speed(): number {
|
|
896
|
+
return Math.sqrt(this._vel[0] * this._vel[0] + this._vel[1] * this._vel[1]);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* The angle this agent is heading toward, in radians. Note that a stationary agent has no
|
|
901
|
+
* heading and reports 0 (pointing along +x) rather than NaN. Set a [`Flock`](#link)'s
|
|
902
|
+
* `minSpeed` if you are drawing headings and want to avoid that.
|
|
903
|
+
*/
|
|
904
|
+
get heading(): number {
|
|
905
|
+
return Math.atan2(this._vel[1], this._vel[0]);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Flock is a subclass of [`Group`](#link) that holds [`Boid`](#link) agents and simulates
|
|
911
|
+
* flocking behavior (also known as "boids", after Craig Reynolds). Each agent steers by three
|
|
912
|
+
* local rules — separation, alignment, and cohesion — evaluated over the neighbors within its
|
|
913
|
+
* `perception` radius. Create one with [`Create.flock`](#link) and advance it with
|
|
914
|
+
* [`Flock.step`](#link). Since a Flock is a Group of Pts, it draws directly:
|
|
915
|
+
* `form.points( flock, 2, "circle" )`.
|
|
916
|
+
*
|
|
917
|
+
* Neighbors are found through a uniform spatial hash rather than by testing every pair, so the
|
|
918
|
+
* cost scales with the number of agents rather than with its square.
|
|
919
|
+
* See [a demo here](https://ptsjs.org/demo/?name=create.flock).
|
|
920
|
+
*/
|
|
921
|
+
export class Flock extends Group {
|
|
922
|
+
protected _perception: number = 40;
|
|
923
|
+
protected _separation: number = 20;
|
|
924
|
+
protected _cohesionWeight: number = 1;
|
|
925
|
+
protected _alignWeight: number = 1;
|
|
926
|
+
protected _separateWeight: number = 1.5;
|
|
927
|
+
protected _maxSpeed: number = 100;
|
|
928
|
+
protected _minSpeed: number = 0;
|
|
929
|
+
protected _maxForce: number = 200;
|
|
930
|
+
protected _boundary: FlockBoundary = "steer";
|
|
931
|
+
protected _margin: number = 50;
|
|
932
|
+
protected _maxTimeStep: number = 50;
|
|
933
|
+
protected _bound: GroupLike | null = null;
|
|
934
|
+
protected _initialSpeed: number | undefined = undefined;
|
|
935
|
+
|
|
936
|
+
// Flat per-agent state, gathered from the Boids at the start of each step and scattered back
|
|
937
|
+
// at the end. The neighbor pass runs entirely on these, so it reads sequential memory instead
|
|
938
|
+
// of chasing `this[i].velocity[0]` through two objects for every pair.
|
|
939
|
+
private _pos: Float32Array = new Float32Array(0);
|
|
940
|
+
private _vel: Float32Array = new Float32Array(0);
|
|
941
|
+
// Per-agent accumulators, interleaved 6-wide: cohesion x/y, alignment x/y, separation x/y.
|
|
942
|
+
private _sums: Float32Array = new Float32Array(0);
|
|
943
|
+
private _counts: Uint32Array = new Uint32Array(0);
|
|
944
|
+
|
|
945
|
+
// Spatial-hash scratch, grown geometrically and reused across steps.
|
|
946
|
+
private _hashKeys: Uint32Array = new Uint32Array(0);
|
|
947
|
+
private _cellX: Int32Array = new Int32Array(0);
|
|
948
|
+
private _cellY: Int32Array = new Int32Array(0);
|
|
949
|
+
private _cellStart: Uint32Array = new Uint32Array(0);
|
|
950
|
+
private _cellEntries: Uint32Array = new Uint32Array(0);
|
|
951
|
+
private _steerOut: Float32Array = new Float32Array(2);
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Set any number of options at once. Unspecified options keep their current value.
|
|
955
|
+
* @param options a [`FlockOptions`](#link) object
|
|
956
|
+
*/
|
|
957
|
+
setup(options: FlockOptions): this {
|
|
958
|
+
if (options.perception !== undefined) this.perception = options.perception;
|
|
959
|
+
if (options.separation !== undefined) this.separation = options.separation;
|
|
960
|
+
if (options.cohesionWeight !== undefined)
|
|
961
|
+
this._cohesionWeight = options.cohesionWeight;
|
|
962
|
+
if (options.alignWeight !== undefined)
|
|
963
|
+
this._alignWeight = options.alignWeight;
|
|
964
|
+
if (options.separateWeight !== undefined)
|
|
965
|
+
this._separateWeight = options.separateWeight;
|
|
966
|
+
if (options.maxSpeed !== undefined) this.maxSpeed = options.maxSpeed;
|
|
967
|
+
if (options.minSpeed !== undefined) this.minSpeed = options.minSpeed;
|
|
968
|
+
if (options.maxForce !== undefined) this._maxForce = options.maxForce;
|
|
969
|
+
if (options.bound !== undefined) this._bound = options.bound as GroupLike;
|
|
970
|
+
if (options.boundary !== undefined) this._boundary = options.boundary;
|
|
971
|
+
if (options.margin !== undefined) this.margin = options.margin;
|
|
972
|
+
if (options.maxTimeStep !== undefined)
|
|
973
|
+
this.maxTimeStep = options.maxTimeStep;
|
|
974
|
+
if (options.initialSpeed !== undefined)
|
|
975
|
+
this._initialSpeed = options.initialSpeed;
|
|
976
|
+
return this;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Radius within which an agent sees its neighbors. This is also the spatial hash's cell size.
|
|
981
|
+
*/
|
|
982
|
+
get perception(): number {
|
|
983
|
+
return this._perception;
|
|
984
|
+
}
|
|
985
|
+
set perception(r: number) {
|
|
986
|
+
this._perception = Math.max(0, r);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Radius within which an agent steers away from its neighbors. Values above `perception`
|
|
991
|
+
* have no additional effect, since an agent only considers neighbors it can see.
|
|
992
|
+
*/
|
|
993
|
+
get separation(): number {
|
|
994
|
+
return this._separation;
|
|
995
|
+
}
|
|
996
|
+
set separation(r: number) {
|
|
997
|
+
this._separation = Math.max(0, r);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Weight of the cohesion behavior, which steers an agent toward its neighbors' center.
|
|
1002
|
+
*/
|
|
1003
|
+
get cohesionWeight(): number {
|
|
1004
|
+
return this._cohesionWeight;
|
|
1005
|
+
}
|
|
1006
|
+
set cohesionWeight(w: number) {
|
|
1007
|
+
this._cohesionWeight = w;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Weight of the alignment behavior, which matches an agent's heading to its neighbors'.
|
|
1012
|
+
*/
|
|
1013
|
+
get alignWeight(): number {
|
|
1014
|
+
return this._alignWeight;
|
|
1015
|
+
}
|
|
1016
|
+
set alignWeight(w: number) {
|
|
1017
|
+
this._alignWeight = w;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Weight of the separation behavior, which steers an agent away from close neighbors.
|
|
1022
|
+
*/
|
|
1023
|
+
get separateWeight(): number {
|
|
1024
|
+
return this._separateWeight;
|
|
1025
|
+
}
|
|
1026
|
+
set separateWeight(w: number) {
|
|
1027
|
+
this._separateWeight = w;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Maximum speed, in units per second.
|
|
1032
|
+
*/
|
|
1033
|
+
get maxSpeed(): number {
|
|
1034
|
+
return this._maxSpeed;
|
|
1035
|
+
}
|
|
1036
|
+
set maxSpeed(s: number) {
|
|
1037
|
+
this._maxSpeed = Math.max(0, s);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Minimum speed, in units per second, so agents never stall. Default is 0.
|
|
1042
|
+
*/
|
|
1043
|
+
get minSpeed(): number {
|
|
1044
|
+
return this._minSpeed;
|
|
1045
|
+
}
|
|
1046
|
+
set minSpeed(s: number) {
|
|
1047
|
+
this._minSpeed = Math.max(0, s);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Maximum steering force, in units per second squared. This caps how sharply an agent can
|
|
1052
|
+
* turn toward the direction its three behaviors blend to. A boundary turn is added on top,
|
|
1053
|
+
* so an agent near an edge can accelerate up to twice this.
|
|
1054
|
+
*/
|
|
1055
|
+
get maxForce(): number {
|
|
1056
|
+
return this._maxForce;
|
|
1057
|
+
}
|
|
1058
|
+
set maxForce(f: number) {
|
|
1059
|
+
this._maxForce = f;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Boundary that keeps the flock in view, as a [`Bound`](#link) or a Group of 2 Pts.
|
|
1064
|
+
* When this is null, no boundary behavior is applied regardless of `boundary`.
|
|
1065
|
+
*/
|
|
1066
|
+
get bound(): GroupLike | null {
|
|
1067
|
+
return this._bound;
|
|
1068
|
+
}
|
|
1069
|
+
set bound(b: GroupLike | null) {
|
|
1070
|
+
this._bound = b;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* How the boundary is treated: `"steer"`, `"wrap"`, `"bounce"`, or `"none"`.
|
|
1075
|
+
*
|
|
1076
|
+
* Note that `"wrap"` teleports agents across the bound while the neighborhood search is not
|
|
1077
|
+
* toroidal, so a flock loses sight of itself at the seam. Prefer `"steer"` when that matters.
|
|
1078
|
+
*/
|
|
1079
|
+
get boundary(): FlockBoundary {
|
|
1080
|
+
return this._boundary;
|
|
1081
|
+
}
|
|
1082
|
+
set boundary(b: FlockBoundary) {
|
|
1083
|
+
this._boundary = b;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Distance from an edge at which `"steer"` starts turning agents back.
|
|
1088
|
+
*/
|
|
1089
|
+
get margin(): number {
|
|
1090
|
+
return this._margin;
|
|
1091
|
+
}
|
|
1092
|
+
set margin(m: number) {
|
|
1093
|
+
this._margin = Math.max(0, m);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Maximum simulated time in milliseconds per [`Flock.step`](#link) call. Longer elapsed times
|
|
1098
|
+
* are clamped to this, which keeps a stalled frame from teleporting the flock. Default is 50.
|
|
1099
|
+
*/
|
|
1100
|
+
get maxTimeStep(): number {
|
|
1101
|
+
return this._maxTimeStep;
|
|
1102
|
+
}
|
|
1103
|
+
set maxTimeStep(ms: number) {
|
|
1104
|
+
this._maxTimeStep = Math.max(0, ms);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Speed given to an agent added without an explicit velocity. Defaults to half of `maxSpeed`.
|
|
1109
|
+
*/
|
|
1110
|
+
get initialSpeed(): number {
|
|
1111
|
+
return this._initialSpeed === undefined
|
|
1112
|
+
? this._maxSpeed * 0.5
|
|
1113
|
+
: this._initialSpeed;
|
|
1114
|
+
}
|
|
1115
|
+
set initialSpeed(s: number) {
|
|
1116
|
+
this._initialSpeed = s;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Add an agent to this flock. Named `addBoid` rather than `add` because [`Group.add`](#link)
|
|
1121
|
+
* already means "translate every Pt in this group", and [`Group.moveBy`](#link) delegates to it.
|
|
1122
|
+
* @param pt a Pt, a Boid, or an array of numbers for the starting position
|
|
1123
|
+
* @param velocity optional starting velocity. When omitted, a new agent gets a random heading
|
|
1124
|
+
* at [`Flock.initialSpeed`](#link), drawn from [`Num.random`](#link).
|
|
1125
|
+
*/
|
|
1126
|
+
addBoid(pt: PtLike | Boid, velocity?: PtLike): this {
|
|
1127
|
+
const isBoid = pt instanceof Boid;
|
|
1128
|
+
const boid = isBoid ? (pt as Boid) : new Boid(pt);
|
|
1129
|
+
if (velocity !== undefined) {
|
|
1130
|
+
boid.velocity[0] = velocity[0];
|
|
1131
|
+
boid.velocity[1] = velocity[1];
|
|
1132
|
+
} else if (!isBoid) {
|
|
1133
|
+
const a = Num.random() * Const.two_pi;
|
|
1134
|
+
const s = this.initialSpeed;
|
|
1135
|
+
boid.velocity[0] = Math.cos(a) * s;
|
|
1136
|
+
boid.velocity[1] = Math.sin(a) * s;
|
|
1137
|
+
}
|
|
1138
|
+
this.push(boid);
|
|
1139
|
+
return this;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Advance the simulation. Call this once per frame with the frame time, eg
|
|
1144
|
+
* `space.add( (time, ftime) => flock.step( ftime ) )`.
|
|
1145
|
+
*
|
|
1146
|
+
* Elapsed times longer than [`Flock.maxTimeStep`](#link) are clamped, so a slow frame slows
|
|
1147
|
+
* the flock down instead of teleporting it. A non-positive or NaN time is a no-op.
|
|
1148
|
+
*
|
|
1149
|
+
* @param ms elapsed time in milliseconds
|
|
1150
|
+
*/
|
|
1151
|
+
step(ms: number): this {
|
|
1152
|
+
const n = this.length;
|
|
1153
|
+
if (n === 0 || !(ms > 0)) return this;
|
|
1154
|
+
|
|
1155
|
+
const dt = Math.min(ms, this._maxTimeStep) / 1000;
|
|
1156
|
+
if (dt <= 0) return this;
|
|
1157
|
+
|
|
1158
|
+
this._ensureBuffers(n);
|
|
1159
|
+
this._gather(n);
|
|
1160
|
+
if (n > 1 && this._perception > 0) {
|
|
1161
|
+
this._accumulate(n);
|
|
1162
|
+
} else {
|
|
1163
|
+
this._sums.fill(0, 0, n * 6);
|
|
1164
|
+
this._counts.fill(0, 0, n);
|
|
1165
|
+
}
|
|
1166
|
+
this._integrate(n, dt);
|
|
1167
|
+
this._scatter(n);
|
|
1168
|
+
return this;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Grow the flat state and hash scratch to fit `n` agents. Every buffer is fully written
|
|
1173
|
+
* before it is read within a step, so reallocating here never loses state.
|
|
1174
|
+
*/
|
|
1175
|
+
private _ensureBuffers(n: number) {
|
|
1176
|
+
if (this._pos.length < n * 2) {
|
|
1177
|
+
const size = n * 2 * 2; // geometric growth
|
|
1178
|
+
this._pos = new Float32Array(size);
|
|
1179
|
+
this._vel = new Float32Array(size);
|
|
1180
|
+
this._sums = new Float32Array(size * 3);
|
|
1181
|
+
this._counts = new Uint32Array(size);
|
|
1182
|
+
this._hashKeys = new Uint32Array(size);
|
|
1183
|
+
this._cellX = new Int32Array(size);
|
|
1184
|
+
this._cellY = new Int32Array(size);
|
|
1185
|
+
this._cellEntries = new Uint32Array(size);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Copy positions and velocities out of the Boids into the flat buffers. An element that
|
|
1191
|
+
* reached this Group without a velocity — pushed as a plain Pt, or produced by a Group method
|
|
1192
|
+
* that copies through the species constructor — is upgraded to a Boid in place here rather
|
|
1193
|
+
* than throwing mid-simulation.
|
|
1194
|
+
*/
|
|
1195
|
+
private _gather(n: number) {
|
|
1196
|
+
const pos = this._pos;
|
|
1197
|
+
const vel = this._vel;
|
|
1198
|
+
for (let i = 0; i < n; i++) {
|
|
1199
|
+
let b = this[i] as Boid;
|
|
1200
|
+
let v = b.velocity;
|
|
1201
|
+
if (v === undefined) {
|
|
1202
|
+
b = new Boid(b);
|
|
1203
|
+
v = b.velocity;
|
|
1204
|
+
this[i] = b;
|
|
1205
|
+
}
|
|
1206
|
+
const k = i * 2;
|
|
1207
|
+
pos[k] = b[0];
|
|
1208
|
+
pos[k + 1] = b[1];
|
|
1209
|
+
vel[k] = v[0];
|
|
1210
|
+
vel[k + 1] = v[1];
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Write the integrated positions and velocities back into the Boids.
|
|
1216
|
+
*/
|
|
1217
|
+
private _scatter(n: number) {
|
|
1218
|
+
const pos = this._pos;
|
|
1219
|
+
const vel = this._vel;
|
|
1220
|
+
for (let i = 0; i < n; i++) {
|
|
1221
|
+
const b = this[i] as Boid;
|
|
1222
|
+
const v = b.velocity;
|
|
1223
|
+
const k = i * 2;
|
|
1224
|
+
b[0] = pos[k];
|
|
1225
|
+
b[1] = pos[k + 1];
|
|
1226
|
+
v[0] = vel[k];
|
|
1227
|
+
v[1] = vel[k + 1];
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Accumulate the three behaviors' sums for every agent in a single pass over neighboring
|
|
1233
|
+
* pairs, using a uniform spatial hash (a counting-sort grid) built the same way as
|
|
1234
|
+
* [`World`](#link)'s collision broad phase — see `World._collideParticles`. The two are kept
|
|
1235
|
+
* separate on purpose: this one queries a fixed radius and accumulates into flat sums, and
|
|
1236
|
+
* sharing an abstraction between them would put an indirect call on the per-pair path.
|
|
1237
|
+
*
|
|
1238
|
+
* Cells are exactly `perception` wide, which is the smallest size for which every neighbor
|
|
1239
|
+
* within that radius is guaranteed to lie in the 3x3 block around an agent's cell. Each
|
|
1240
|
+
* agent's cell coordinates are kept, so an entry found through a hash bucket is checked
|
|
1241
|
+
* against the cell actually being visited: two cells that collide into one bucket cannot
|
|
1242
|
+
* count a neighbor twice or hide one. That exact check is also what allows visiting only
|
|
1243
|
+
* half of the neighborhood — the agent's own cell (pairing with higher indices) plus the four
|
|
1244
|
+
* cells east, south-west, south, and south-east — since every pair of adjacent cells is then
|
|
1245
|
+
* seen from exactly one side.
|
|
1246
|
+
*
|
|
1247
|
+
* Each pair is accumulated into both agents: "j is within r of i" is symmetric, so this
|
|
1248
|
+
* halves the distance computations. No `sqrt` is taken here — radius tests compare squared
|
|
1249
|
+
* distances, and separation falls off as the inverse square, which is what makes the
|
|
1250
|
+
* fallback in `__flock_tieBreak` necessary.
|
|
1251
|
+
*/
|
|
1252
|
+
private _accumulate(n: number) {
|
|
1253
|
+
const pos = this._pos;
|
|
1254
|
+
const vel = this._vel;
|
|
1255
|
+
const sums = this._sums;
|
|
1256
|
+
const counts = this._counts;
|
|
1257
|
+
|
|
1258
|
+
sums.fill(0, 0, n * 6);
|
|
1259
|
+
counts.fill(0, 0, n);
|
|
1260
|
+
|
|
1261
|
+
const r2 = this._perception * this._perception;
|
|
1262
|
+
const sep = Math.min(this._separation, this._perception);
|
|
1263
|
+
const sep2 = sep * sep;
|
|
1264
|
+
const minSep = sep * __flock_minSepFraction;
|
|
1265
|
+
const minSep2 = minSep * minSep;
|
|
1266
|
+
const inv = 1 / this._perception;
|
|
1267
|
+
|
|
1268
|
+
// Hash table size: the next power of two above 2n, so buckets stay sparse.
|
|
1269
|
+
let m = 16;
|
|
1270
|
+
while (m < n * 2) m <<= 1;
|
|
1271
|
+
const mask = m - 1;
|
|
1272
|
+
if (this._cellStart.length < m + 1)
|
|
1273
|
+
this._cellStart = new Uint32Array(m + 1);
|
|
1274
|
+
|
|
1275
|
+
const keys = this._hashKeys;
|
|
1276
|
+
const cellX = this._cellX;
|
|
1277
|
+
const cellY = this._cellY;
|
|
1278
|
+
const start = this._cellStart;
|
|
1279
|
+
const entries = this._cellEntries;
|
|
1280
|
+
|
|
1281
|
+
// Cell coordinates once per agent. A non-finite or absurd position gets a sentinel cell
|
|
1282
|
+
// that no neighborhood visit can match, so it neither sees nor is seen (see
|
|
1283
|
+
// `__flock_maxCell`). Then count per cell, exclusive prefix sum, and scatter; after the
|
|
1284
|
+
// scatter, bucket k spans [start[k-1], start[k]).
|
|
1285
|
+
start.fill(0, 0, m + 1);
|
|
1286
|
+
for (let i = 0; i < n; i++) {
|
|
1287
|
+
const cx = Math.floor(pos[i * 2] * inv);
|
|
1288
|
+
const cy = Math.floor(pos[i * 2 + 1] * inv);
|
|
1289
|
+
if (
|
|
1290
|
+
cx >= -__flock_maxCell &&
|
|
1291
|
+
cx <= __flock_maxCell &&
|
|
1292
|
+
cy >= -__flock_maxCell &&
|
|
1293
|
+
cy <= __flock_maxCell
|
|
1294
|
+
) {
|
|
1295
|
+
cellX[i] = cx;
|
|
1296
|
+
cellY[i] = cy;
|
|
1297
|
+
} else {
|
|
1298
|
+
cellX[i] = __flock_noCell;
|
|
1299
|
+
cellY[i] = __flock_noCell;
|
|
1300
|
+
}
|
|
1301
|
+
const key =
|
|
1302
|
+
((Math.imul(cellX[i], 0x9e3779b1) ^ Math.imul(cellY[i], 0x85ebca77)) >>>
|
|
1303
|
+
0) &
|
|
1304
|
+
mask;
|
|
1305
|
+
keys[i] = key;
|
|
1306
|
+
start[key]++;
|
|
1307
|
+
}
|
|
1308
|
+
let sum = 0;
|
|
1309
|
+
for (let k = 0; k < m; k++) {
|
|
1310
|
+
const c = start[k];
|
|
1311
|
+
start[k] = sum;
|
|
1312
|
+
sum += c;
|
|
1313
|
+
}
|
|
1314
|
+
start[m] = sum;
|
|
1315
|
+
for (let i = 0; i < n; i++) {
|
|
1316
|
+
entries[start[keys[i]]++] = i;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
for (let i = 0; i < n; i++) {
|
|
1320
|
+
const cx = cellX[i];
|
|
1321
|
+
if (cx === __flock_noCell) continue;
|
|
1322
|
+
const cy = cellY[i];
|
|
1323
|
+
const ki = i * 2;
|
|
1324
|
+
const ix = pos[ki];
|
|
1325
|
+
const iy = pos[ki + 1];
|
|
1326
|
+
const ivx = vel[ki];
|
|
1327
|
+
const ivy = vel[ki + 1];
|
|
1328
|
+
const si = i * 6;
|
|
1329
|
+
|
|
1330
|
+
// the five cells of the half neighborhood, own cell first
|
|
1331
|
+
for (let c = 0; c < 5; c++) {
|
|
1332
|
+
const gx = c === 2 ? cx - 1 : c === 0 || c === 3 ? cx : cx + 1;
|
|
1333
|
+
const gy = c < 2 ? cy : cy + 1;
|
|
1334
|
+
const key =
|
|
1335
|
+
((Math.imul(gx, 0x9e3779b1) ^ Math.imul(gy, 0x85ebca77)) >>> 0) &
|
|
1336
|
+
mask;
|
|
1337
|
+
const end = start[key];
|
|
1338
|
+
const begin = key > 0 ? start[key - 1] : 0;
|
|
1339
|
+
for (let e = begin; e < end; e++) {
|
|
1340
|
+
const j = entries[e];
|
|
1341
|
+
if (cellX[j] !== gx || cellY[j] !== gy) continue; // another cell in this bucket
|
|
1342
|
+
if (c === 0 && j <= i) continue; // own cell: each pair once
|
|
1343
|
+
|
|
1344
|
+
const kj = j * 2;
|
|
1345
|
+
const jx = pos[kj];
|
|
1346
|
+
const jy = pos[kj + 1];
|
|
1347
|
+
const dx = jx - ix;
|
|
1348
|
+
const dy = jy - iy;
|
|
1349
|
+
const d2 = dx * dx + dy * dy;
|
|
1350
|
+
if (!(d2 < r2)) continue;
|
|
1351
|
+
|
|
1352
|
+
const sj = j * 6;
|
|
1353
|
+
sums[si] += jx;
|
|
1354
|
+
sums[si + 1] += jy;
|
|
1355
|
+
sums[sj] += ix;
|
|
1356
|
+
sums[sj + 1] += iy;
|
|
1357
|
+
sums[si + 2] += vel[kj];
|
|
1358
|
+
sums[si + 3] += vel[kj + 1];
|
|
1359
|
+
sums[sj + 2] += ivx;
|
|
1360
|
+
sums[sj + 3] += ivy;
|
|
1361
|
+
counts[i]++;
|
|
1362
|
+
counts[j]++;
|
|
1363
|
+
|
|
1364
|
+
if (d2 < sep2) {
|
|
1365
|
+
let ox = dx;
|
|
1366
|
+
let oy = dy;
|
|
1367
|
+
let dd = d2;
|
|
1368
|
+
if (dd === 0) {
|
|
1369
|
+
// Coincident agents have no direction to separate along; split them along x.
|
|
1370
|
+
ox = __flock_tieBreak;
|
|
1371
|
+
oy = 0;
|
|
1372
|
+
dd = minSep2;
|
|
1373
|
+
} else if (dd < minSep2) {
|
|
1374
|
+
dd = minSep2;
|
|
1375
|
+
}
|
|
1376
|
+
const w = 1 / dd;
|
|
1377
|
+
const wx = ox * w;
|
|
1378
|
+
const wy = oy * w;
|
|
1379
|
+
sums[si + 4] -= wx;
|
|
1380
|
+
sums[si + 5] -= wy;
|
|
1381
|
+
sums[sj + 4] += wx;
|
|
1382
|
+
sums[sj + 5] += wy;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Turn the accumulated sums into a steering force per agent, then integrate velocity and
|
|
1391
|
+
* position. This is O(n), so it uses the classic "steer toward the desired velocity"
|
|
1392
|
+
* formulation and its handful of square roots, which behaves better than weighting raw
|
|
1393
|
+
* offsets and keeps `maxSpeed` and `maxForce` as the only scale-dependent knobs.
|
|
1394
|
+
* See `__flock_accumulateUnit` for why the behaviors are blended before that steer is taken.
|
|
1395
|
+
*/
|
|
1396
|
+
private _integrate(n: number, dt: number) {
|
|
1397
|
+
const pos = this._pos;
|
|
1398
|
+
const vel = this._vel;
|
|
1399
|
+
const sums = this._sums;
|
|
1400
|
+
const counts = this._counts;
|
|
1401
|
+
const out = this._steerOut;
|
|
1402
|
+
|
|
1403
|
+
const maxSpeed = this._maxSpeed;
|
|
1404
|
+
const maxForce = this._maxForce;
|
|
1405
|
+
const minSpeed = Math.min(this._minSpeed, maxSpeed);
|
|
1406
|
+
const cw = this._cohesionWeight;
|
|
1407
|
+
const aw = this._alignWeight;
|
|
1408
|
+
const sw = this._separateWeight;
|
|
1409
|
+
|
|
1410
|
+
const bound = this._bound;
|
|
1411
|
+
const boundary = bound ? this._boundary : "none";
|
|
1412
|
+
const steer = boundary === "steer" && this._margin > 0;
|
|
1413
|
+
const wrap = boundary === "wrap";
|
|
1414
|
+
const bounce = boundary === "bounce";
|
|
1415
|
+
let minX = 0;
|
|
1416
|
+
let minY = 0;
|
|
1417
|
+
let maxX = 0;
|
|
1418
|
+
let maxY = 0;
|
|
1419
|
+
if (bound && boundary !== "none") {
|
|
1420
|
+
const b0 = bound[0];
|
|
1421
|
+
const b1 = bound[1];
|
|
1422
|
+
minX = Math.min(b0[0], b1[0]);
|
|
1423
|
+
minY = Math.min(b0[1], b1[1]);
|
|
1424
|
+
maxX = Math.max(b0[0], b1[0]);
|
|
1425
|
+
maxY = Math.max(b0[1], b1[1]);
|
|
1426
|
+
}
|
|
1427
|
+
const margin = this._margin;
|
|
1428
|
+
|
|
1429
|
+
for (let i = 0; i < n; i++) {
|
|
1430
|
+
const k = i * 2;
|
|
1431
|
+
const s = i * 6;
|
|
1432
|
+
let px = pos[k];
|
|
1433
|
+
let py = pos[k + 1];
|
|
1434
|
+
let vx = vel[k];
|
|
1435
|
+
let vy = vel[k + 1];
|
|
1436
|
+
|
|
1437
|
+
// Blend the three behaviors into one desired direction...
|
|
1438
|
+
out[0] = 0;
|
|
1439
|
+
out[1] = 0;
|
|
1440
|
+
const c = counts[i];
|
|
1441
|
+
if (c > 0) {
|
|
1442
|
+
// steer toward the neighbors' center
|
|
1443
|
+
if (cw !== 0) {
|
|
1444
|
+
__flock_accumulateUnit(
|
|
1445
|
+
out,
|
|
1446
|
+
sums[s] / c - px,
|
|
1447
|
+
sums[s + 1] / c - py,
|
|
1448
|
+
cw,
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
// match the neighbors' heading. The mean and the sum point the same way, and the
|
|
1452
|
+
// direction is normalized anyway, so the neighbor count is not needed here.
|
|
1453
|
+
if (aw !== 0) __flock_accumulateUnit(out, sums[s + 2], sums[s + 3], aw);
|
|
1454
|
+
if (sw !== 0) __flock_accumulateUnit(out, sums[s + 4], sums[s + 5], sw);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// ...then take a single steer toward it, so the damping is applied once.
|
|
1458
|
+
let ax = 0;
|
|
1459
|
+
let ay = 0;
|
|
1460
|
+
const dm2 = out[0] * out[0] + out[1] * out[1];
|
|
1461
|
+
if (dm2 > 0) {
|
|
1462
|
+
const sc = maxSpeed / Math.sqrt(dm2);
|
|
1463
|
+
ax = out[0] * sc - vx;
|
|
1464
|
+
ay = out[1] * sc - vy;
|
|
1465
|
+
const f2 = ax * ax + ay * ay;
|
|
1466
|
+
if (f2 > maxForce * maxForce) {
|
|
1467
|
+
const fs = maxForce / Math.sqrt(f2);
|
|
1468
|
+
ax *= fs;
|
|
1469
|
+
ay *= fs;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
if (steer) {
|
|
1474
|
+
// Ramp the turn from zero at the margin's inner edge to full force at the wall, and
|
|
1475
|
+
// hold it at full force for anything that already escaped. This is added on top of the
|
|
1476
|
+
// blended steer rather than mixed into it, so the flock's own rules cannot outvote it
|
|
1477
|
+
// and pin a cluster against an edge.
|
|
1478
|
+
const dl = px - minX;
|
|
1479
|
+
const dr = maxX - px;
|
|
1480
|
+
const dtp = py - minY;
|
|
1481
|
+
const db = maxY - py;
|
|
1482
|
+
if (dl < margin) ax += maxForce * (1 - Math.max(dl, 0) / margin);
|
|
1483
|
+
else if (dr < margin) ax -= maxForce * (1 - Math.max(dr, 0) / margin);
|
|
1484
|
+
if (dtp < margin) ay += maxForce * (1 - Math.max(dtp, 0) / margin);
|
|
1485
|
+
else if (db < margin) ay -= maxForce * (1 - Math.max(db, 0) / margin);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
vx += ax * dt;
|
|
1489
|
+
vy += ay * dt;
|
|
1490
|
+
|
|
1491
|
+
const sp2 = vx * vx + vy * vy;
|
|
1492
|
+
if (sp2 > maxSpeed * maxSpeed) {
|
|
1493
|
+
const sc = maxSpeed / Math.sqrt(sp2);
|
|
1494
|
+
vx *= sc;
|
|
1495
|
+
vy *= sc;
|
|
1496
|
+
} else if (minSpeed > 0 && sp2 < minSpeed * minSpeed) {
|
|
1497
|
+
if (sp2 > 0) {
|
|
1498
|
+
const sc = minSpeed / Math.sqrt(sp2);
|
|
1499
|
+
vx *= sc;
|
|
1500
|
+
vy *= sc;
|
|
1501
|
+
} else {
|
|
1502
|
+
// A dead stop has no heading to preserve; pick one deterministically.
|
|
1503
|
+
vx = minSpeed;
|
|
1504
|
+
vy = 0;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
px += vx * dt;
|
|
1509
|
+
py += vy * dt;
|
|
1510
|
+
|
|
1511
|
+
if (wrap) {
|
|
1512
|
+
const w = maxX - minX;
|
|
1513
|
+
const h = maxY - minY;
|
|
1514
|
+
if (w > 0) {
|
|
1515
|
+
if (px < minX) px = maxX - ((minX - px) % w);
|
|
1516
|
+
else if (px > maxX) px = minX + ((px - maxX) % w);
|
|
1517
|
+
}
|
|
1518
|
+
if (h > 0) {
|
|
1519
|
+
if (py < minY) py = maxY - ((minY - py) % h);
|
|
1520
|
+
else if (py > maxY) py = minY + ((py - maxY) % h);
|
|
1521
|
+
}
|
|
1522
|
+
} else if (bounce) {
|
|
1523
|
+
// `<=` rather than `<`: an agent that lands exactly on the wall is still heading out,
|
|
1524
|
+
// and would otherwise leave on the next step without ever reflecting.
|
|
1525
|
+
if (px <= minX) {
|
|
1526
|
+
px = minX;
|
|
1527
|
+
if (vx < 0) vx = -vx;
|
|
1528
|
+
} else if (px >= maxX) {
|
|
1529
|
+
px = maxX;
|
|
1530
|
+
if (vx > 0) vx = -vx;
|
|
1531
|
+
}
|
|
1532
|
+
if (py <= minY) {
|
|
1533
|
+
py = minY;
|
|
1534
|
+
if (vy < 0) vy = -vy;
|
|
1535
|
+
} else if (py >= maxY) {
|
|
1536
|
+
py = maxY;
|
|
1537
|
+
if (vy > 0) vy = -vy;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
pos[k] = px;
|
|
1542
|
+
pos[k + 1] = py;
|
|
1543
|
+
vel[k] = vx;
|
|
1544
|
+
vel[k + 1] = vy;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|