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/Physics.ts
ADDED
|
@@ -0,0 +1,1233 @@
|
|
|
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, Bound } from "./Pt";
|
|
4
|
+
import { Polygon, Circle } from "./Op";
|
|
5
|
+
import { Geom } from "./Num";
|
|
6
|
+
import { type PtLike, type PtIterable } from "./Types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A `World` stores and manages [`Body`](#link) and [`Particle`](#link) for 2D physics simulation.
|
|
10
|
+
* It advances with a substepped position-based (XPBD-style) solver and a spatial-hash broad phase.
|
|
11
|
+
* See a [Particle demo](https://ptsjs.org/demo/?name=physics.particles) and a [Body demo](https://ptsjs.org/demo/?name=physics.shapes) on the demo page.
|
|
12
|
+
*/
|
|
13
|
+
// Velocities are expressed per 60 Hz frame: `hit`, drag deltas, `changed`, friction, and
|
|
14
|
+
// stiffness all use this unit, so a sketch behaves the same at any display refresh rate.
|
|
15
|
+
const FRAME = 1 / 60; // seconds
|
|
16
|
+
const FRAME_MS = 1000 / 60;
|
|
17
|
+
// Elapsed times below this are carried into the next update rather than solved as a
|
|
18
|
+
// tiny step (a duplicate RAF timestamp would otherwise produce a 10× step-size jump).
|
|
19
|
+
const MIN_STEP_MS = 2;
|
|
20
|
+
|
|
21
|
+
export class World {
|
|
22
|
+
protected _gravity: Pt = new Pt();
|
|
23
|
+
protected _friction: number = 1; // general friction
|
|
24
|
+
protected _damping: number = 0.75; // collision damping
|
|
25
|
+
protected _iterations: number = 1; // constraint iterations per substep
|
|
26
|
+
protected _substeps: number = 4; // solver substeps per update
|
|
27
|
+
protected _maxTimeStep: number = 50; // clamp on ms per update
|
|
28
|
+
protected _bound: Bound;
|
|
29
|
+
|
|
30
|
+
protected _particles: Particle[] = [];
|
|
31
|
+
protected _bodies: Body[] = [];
|
|
32
|
+
protected _pnames: string[] = []; // particle name index
|
|
33
|
+
protected _bnames: string[] = []; // body name index
|
|
34
|
+
|
|
35
|
+
protected _drawParticles!: (p: Particle, i: number) => void;
|
|
36
|
+
protected _drawBodies!: (p: Body, i: number) => void;
|
|
37
|
+
|
|
38
|
+
// substep-adjusted friction, computed once per update
|
|
39
|
+
private _frictionStep: number = 1;
|
|
40
|
+
// elapsed time too short to solve, carried into the next update
|
|
41
|
+
private _carry: number = 0;
|
|
42
|
+
|
|
43
|
+
// spatial-hash and AABB scratch buffers, grown geometrically and reused
|
|
44
|
+
private _hashKeys: Uint32Array = new Uint32Array(0);
|
|
45
|
+
private _cellStart: Uint32Array = new Uint32Array(0);
|
|
46
|
+
private _cellEntries: Uint32Array = new Uint32Array(0);
|
|
47
|
+
private _neighborKeys: Uint32Array = new Uint32Array(9);
|
|
48
|
+
private _bodyBounds: Float32Array = new Float32Array(0);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a `World` for 2D physics simulation.
|
|
52
|
+
* @param bound a Group or an Iterable<Pt> representing a rectangular bounding box
|
|
53
|
+
* @param friction a value between 0 to 1, where 1 means no friction. Default is 1
|
|
54
|
+
* @param gravity a number of a Pt to define gravitational force. A number is a shorthand to set `new Pt(0, n)`. Default is 0.
|
|
55
|
+
*/
|
|
56
|
+
constructor(
|
|
57
|
+
bound: PtIterable,
|
|
58
|
+
friction: number = 1,
|
|
59
|
+
gravity: PtLike | number = 0,
|
|
60
|
+
) {
|
|
61
|
+
this._bound = Bound.fromGroup(bound);
|
|
62
|
+
this._friction = friction;
|
|
63
|
+
this._gravity =
|
|
64
|
+
typeof gravity === "number" ? new Pt(0, gravity) : new Pt(gravity);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Current bound in this `World`.
|
|
70
|
+
*/
|
|
71
|
+
get bound(): Bound {
|
|
72
|
+
return this._bound;
|
|
73
|
+
}
|
|
74
|
+
set bound(bound: Bound) {
|
|
75
|
+
this._bound = bound;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Current gravity in this `World`.
|
|
80
|
+
*/
|
|
81
|
+
get gravity(): Pt {
|
|
82
|
+
return this._gravity;
|
|
83
|
+
}
|
|
84
|
+
set gravity(g: Pt) {
|
|
85
|
+
this._gravity = g;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Current friction in this `World`.
|
|
90
|
+
*/
|
|
91
|
+
get friction(): number {
|
|
92
|
+
return this._friction;
|
|
93
|
+
}
|
|
94
|
+
set friction(f: number) {
|
|
95
|
+
this._friction = f;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Current damping in this `World`.
|
|
100
|
+
*/
|
|
101
|
+
get damping(): number {
|
|
102
|
+
return this._damping;
|
|
103
|
+
}
|
|
104
|
+
set damping(f: number) {
|
|
105
|
+
this._damping = f;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Constraint solver iterations per substep.
|
|
110
|
+
*/
|
|
111
|
+
get iterations(): number {
|
|
112
|
+
return this._iterations;
|
|
113
|
+
}
|
|
114
|
+
set iterations(f: number) {
|
|
115
|
+
this._iterations = f;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Target number of solver substeps per 60 Hz frame (16.7 ms). Each [`World.update`](#link)
|
|
120
|
+
* runs enough substeps of about that size to cover its elapsed time, so a 30 Hz frame solves
|
|
121
|
+
* twice as many substeps as a 60 Hz frame rather than larger ones. More substeps produce a
|
|
122
|
+
* more stable and accurate simulation at a linear cost. Default is 4.
|
|
123
|
+
*/
|
|
124
|
+
get substeps(): number {
|
|
125
|
+
return this._substeps;
|
|
126
|
+
}
|
|
127
|
+
set substeps(n: number) {
|
|
128
|
+
this._substeps = Math.max(1, Math.round(n));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Maximum simulated time in milliseconds per [`World.update`](#link) call. Larger elapsed
|
|
133
|
+
* times are clamped so that a hitch (eg, a backgrounded tab) cannot destabilize the
|
|
134
|
+
* simulation. Default is 50.
|
|
135
|
+
*/
|
|
136
|
+
get maxTimeStep(): number {
|
|
137
|
+
return this._maxTimeStep;
|
|
138
|
+
}
|
|
139
|
+
set maxTimeStep(ms: number) {
|
|
140
|
+
this._maxTimeStep = Math.max(0, ms);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the number of bodies.
|
|
145
|
+
*/
|
|
146
|
+
get bodyCount(): number {
|
|
147
|
+
return this._bodies.length;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get the number of particles.
|
|
152
|
+
*/
|
|
153
|
+
get particleCount(): number {
|
|
154
|
+
return this._particles.length;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get a body in this world by index or string id.
|
|
159
|
+
* @param id numeric index of the body, or a string id that associates with it.
|
|
160
|
+
* @returns a Body, or undefined if not found
|
|
161
|
+
*/
|
|
162
|
+
body(id: number | string) {
|
|
163
|
+
if (typeof id === "string" && id.length > 0) {
|
|
164
|
+
return this._bodies[this._bnames.indexOf(id)];
|
|
165
|
+
}
|
|
166
|
+
return typeof id === "number" && id >= 0 ? this._bodies[id] : undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get a particle in this world by index or string id.
|
|
171
|
+
* @param id numeric index of the particle, or a string id that associates with it.
|
|
172
|
+
* @returns a Particle, or undefined if not found
|
|
173
|
+
*/
|
|
174
|
+
particle(id: number | string) {
|
|
175
|
+
if (typeof id === "string" && id.length > 0) {
|
|
176
|
+
return this._particles[this._pnames.indexOf(id)];
|
|
177
|
+
}
|
|
178
|
+
return typeof id === "number" && id >= 0 ? this._particles[id] : undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Given a body's name, return its index in the bodies array, or -1 if not found.
|
|
183
|
+
* @param name name of the body
|
|
184
|
+
* @returns index number, or -1 if not found
|
|
185
|
+
*/
|
|
186
|
+
bodyIndex(name: string): number {
|
|
187
|
+
return this._bnames.indexOf(name);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Given a particle's name, return its index in the particles array, or -1 if not found.
|
|
192
|
+
* @param name name of the particle
|
|
193
|
+
* @returns index number, or -1 if not found
|
|
194
|
+
*/
|
|
195
|
+
particleIndex(name: string): number {
|
|
196
|
+
return this._pnames.indexOf(name);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Advance this world by an amount of time, solved in substeps sized by
|
|
201
|
+
* [`World.substeps`](#link). The time is clamped to [`World.maxTimeStep`](#link), and an
|
|
202
|
+
* elapsed time under 2 ms is carried into the next call. Draw callbacks fire once per call,
|
|
203
|
+
* after the solve completes.
|
|
204
|
+
* @param ms change in time in milliseconds
|
|
205
|
+
*/
|
|
206
|
+
update(ms: number) {
|
|
207
|
+
const elapsed = Math.min(ms, this._maxTimeStep) + this._carry;
|
|
208
|
+
this._carry = 0;
|
|
209
|
+
if (elapsed >= MIN_STEP_MS) {
|
|
210
|
+
// The substep count follows the elapsed time, so the substep length stays
|
|
211
|
+
// near its target regardless of frame timing. A step-size jump between
|
|
212
|
+
// substeps is what the time-corrected integrator scales velocity by, and
|
|
213
|
+
// a large ratio also amplifies contact corrections — enough to launch a
|
|
214
|
+
// body that is merely resting on the floor.
|
|
215
|
+
const k = Math.max(1, Math.round((elapsed * this._substeps) / FRAME_MS));
|
|
216
|
+
const h = elapsed / 1000 / k;
|
|
217
|
+
// friction is a per-frame drag; compound it per substep
|
|
218
|
+
this._frictionStep = Math.pow(this._friction, h / FRAME);
|
|
219
|
+
for (let s = 0; s < k; s++) {
|
|
220
|
+
this._updateParticles(h);
|
|
221
|
+
// Body contacts resolve every substep: penetrations are detected while
|
|
222
|
+
// still shallow, and each positional push stays at the scale of one
|
|
223
|
+
// substep's motion — resolving once per update would inject the whole
|
|
224
|
+
// frame's correction at substep velocity, kicking bodies 4× harder than
|
|
225
|
+
// intended. The scalarized SAT makes the extra narrow-phase passes cheap.
|
|
226
|
+
this._updateBodies(h);
|
|
227
|
+
}
|
|
228
|
+
this._clearForces();
|
|
229
|
+
} else if (elapsed > 0) {
|
|
230
|
+
this._carry = elapsed;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (this._drawParticles) {
|
|
234
|
+
for (let i = 0, len = this._particles.length; i < len; i++) {
|
|
235
|
+
this._drawParticles(this._particles[i], i);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (this._drawBodies) {
|
|
239
|
+
for (let i = 0, len = this._bodies.length; i < len; i++) {
|
|
240
|
+
this._drawBodies(this._bodies[i], i);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Draw particles using the provided function.
|
|
247
|
+
* @param fn a function that draws a particle passed in the parameters `(particle, index)`.
|
|
248
|
+
*/
|
|
249
|
+
drawParticles(fn: (p: Particle, i: number) => void): void {
|
|
250
|
+
this._drawParticles = fn;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Draw bodies using the provided function.
|
|
255
|
+
* @param fn a function that draws a body passed in the parameters `(body, index)`.
|
|
256
|
+
*/
|
|
257
|
+
drawBodies(fn: (p: Body, i: number) => void): void {
|
|
258
|
+
this._drawBodies = fn;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Add a particle or body to this world.
|
|
263
|
+
* @param p `Particle` or `Body` instance
|
|
264
|
+
* @param name optional name, which can be referenced in `body()` or `particle()` function to retrieve this back.
|
|
265
|
+
*/
|
|
266
|
+
add(p: Particle | Body, name: string = ""): this {
|
|
267
|
+
if (p instanceof Body) {
|
|
268
|
+
this._bodies.push(<Body>p);
|
|
269
|
+
this._bnames.push(name);
|
|
270
|
+
} else {
|
|
271
|
+
this._particles.push(<Particle>p);
|
|
272
|
+
this._pnames.push(name);
|
|
273
|
+
}
|
|
274
|
+
return this;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private _index(fn: (name: string) => number, id: string | number): number {
|
|
278
|
+
let index = 0;
|
|
279
|
+
if (typeof id === "string") {
|
|
280
|
+
index = fn(id);
|
|
281
|
+
if (index < 0)
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Cannot find index of ${id}. You can use particleIndex() or bodyIndex() function to check existence by name.`,
|
|
284
|
+
);
|
|
285
|
+
} else {
|
|
286
|
+
index = id;
|
|
287
|
+
}
|
|
288
|
+
return index;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Remove bodies from this world. Support removing a range and negative index.
|
|
293
|
+
* @param from Start index, which can be negative (where -1 is at index 0, -2 at index 1, etc)
|
|
294
|
+
* @param count Number of items to remove. Default is 1.
|
|
295
|
+
*/
|
|
296
|
+
removeBody(from: number | string, count: number = 1): this {
|
|
297
|
+
const index = this._index(this.bodyIndex.bind(this), from);
|
|
298
|
+
const param = index < 0 ? [index * -1 - 1, count] : [index, count];
|
|
299
|
+
this._bodies.splice(param[0], param[1]);
|
|
300
|
+
this._bnames.splice(param[0], param[1]);
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Remove particles from this world. Support removing a range and negative index.
|
|
306
|
+
* @param from Start index, which can be negative (where -1 is at index 0, -2 at index 1, etc)
|
|
307
|
+
* @param count Number of items to remove. Default is 1.
|
|
308
|
+
*/
|
|
309
|
+
removeParticle(from: number | string, count: number = 1): this {
|
|
310
|
+
const index = this._index(this.particleIndex.bind(this), from);
|
|
311
|
+
const param = index < 0 ? [index * -1 - 1, count] : [index, count];
|
|
312
|
+
this._particles.splice(param[0], param[1]);
|
|
313
|
+
this._pnames.splice(param[0], param[1]);
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Static function to calculate edge constraints between 2 particles.
|
|
319
|
+
* @param p1 particle 1
|
|
320
|
+
* @param p2 particle 2
|
|
321
|
+
* @param dist distance between particles
|
|
322
|
+
* @param stiff stiffness between 0 to 1.
|
|
323
|
+
* @param precise use precise distance calculation. Default is `false`.
|
|
324
|
+
*/
|
|
325
|
+
static edgeConstraint(
|
|
326
|
+
p1: Particle,
|
|
327
|
+
p2: Particle,
|
|
328
|
+
dist: number,
|
|
329
|
+
stiff: number = 1,
|
|
330
|
+
precise: boolean = false,
|
|
331
|
+
) {
|
|
332
|
+
const m1 = 1 / (p1.mass || 1);
|
|
333
|
+
const m2 = 1 / (p2.mass || 1);
|
|
334
|
+
const mm = m1 + m2;
|
|
335
|
+
|
|
336
|
+
let delta = p2.$subtract(p1);
|
|
337
|
+
let distSq = dist * dist;
|
|
338
|
+
let d = precise
|
|
339
|
+
? dist / delta.magnitude() - 1
|
|
340
|
+
: distSq / (delta.dot(delta) + distSq) - 0.5; // approx square root
|
|
341
|
+
let f = delta.$multiply(d * stiff);
|
|
342
|
+
|
|
343
|
+
p1.subtract(f.$multiply(m1 / mm));
|
|
344
|
+
p2.add(f.$multiply(m2 / mm));
|
|
345
|
+
|
|
346
|
+
return p1;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Static function to calculate bounding box constraints.
|
|
351
|
+
* @param p particle
|
|
352
|
+
* @param rect a Group or an Iterable<Pt> representing a bounding box
|
|
353
|
+
* @param damping damping between 0 to 1, where 1 means no damping. Default is 0.75.
|
|
354
|
+
*/
|
|
355
|
+
static boundConstraint(
|
|
356
|
+
p: Particle,
|
|
357
|
+
rect: PtIterable,
|
|
358
|
+
damping: number = 0.75,
|
|
359
|
+
) {
|
|
360
|
+
const bound = Geom.boundingBox(rect);
|
|
361
|
+
World._boundParticle(
|
|
362
|
+
p,
|
|
363
|
+
bound[0][0],
|
|
364
|
+
bound[0][1],
|
|
365
|
+
bound[1][0],
|
|
366
|
+
bound[1][1],
|
|
367
|
+
damping,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Shared scalar core of the bound constraint: clamp to the rectangle inset by the
|
|
373
|
+
* particle's radius, and reflect the damped velocity on each axis that hit a wall
|
|
374
|
+
* (a corner hit reflects both).
|
|
375
|
+
*/
|
|
376
|
+
protected static _boundParticle(
|
|
377
|
+
p: Particle,
|
|
378
|
+
minX: number,
|
|
379
|
+
minY: number,
|
|
380
|
+
maxX: number,
|
|
381
|
+
maxY: number,
|
|
382
|
+
damping: number,
|
|
383
|
+
) {
|
|
384
|
+
const px = p[0];
|
|
385
|
+
const py = p[1];
|
|
386
|
+
const nx = Math.min(Math.max(px, minX + p.radius), maxX - p.radius);
|
|
387
|
+
const ny = Math.min(Math.max(py, minY + p.radius), maxY - p.radius);
|
|
388
|
+
|
|
389
|
+
if (nx !== px || ny !== py) {
|
|
390
|
+
const prev = p.previous;
|
|
391
|
+
const cx = (px - prev[0]) * damping;
|
|
392
|
+
const cy = (py - prev[1]) * damping;
|
|
393
|
+
prev[0] = nx !== px ? nx + cx : nx - cx;
|
|
394
|
+
prev[1] = ny !== py ? ny + cy : ny - cy;
|
|
395
|
+
p[0] = nx;
|
|
396
|
+
p[1] = ny;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Integrate a particle for one substep: Verlet with the accumulated force plus gravity as
|
|
402
|
+
* acceleration, and the substep-adjusted friction as drag. Forces are read but not cleared
|
|
403
|
+
* here — they persist across the substeps of one update and are cleared when it completes.
|
|
404
|
+
* @param p particle
|
|
405
|
+
* @param dt substep time in seconds
|
|
406
|
+
* @param prevDt time in seconds spanned by the particle's current displacement (see [`Particle.timeStep`](#link)); the velocity is rescaled to `dt` so that it is preserved when the step size changes.
|
|
407
|
+
*/
|
|
408
|
+
protected integrate(
|
|
409
|
+
p: Particle,
|
|
410
|
+
dt: number,
|
|
411
|
+
prevDt: number = p.timeStep || FRAME,
|
|
412
|
+
): Particle {
|
|
413
|
+
const prev = p.previous;
|
|
414
|
+
const ratio = prevDt > 0 ? dt / prevDt : 1;
|
|
415
|
+
|
|
416
|
+
if (p.lock) {
|
|
417
|
+
// A dragged lock stores its per-frame drag delta as velocity for collisions
|
|
418
|
+
// to read; express it per substep so it is applied once over the frame.
|
|
419
|
+
if (ratio !== 1) {
|
|
420
|
+
prev[0] = p[0] - (p[0] - prev[0]) * ratio;
|
|
421
|
+
prev[1] = p[1] - (p[1] - prev[1]) * ratio;
|
|
422
|
+
}
|
|
423
|
+
p.timeStep = dt;
|
|
424
|
+
p.verlet(dt, this._frictionStep, dt); // re-pins to the lock point
|
|
425
|
+
return p;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const force = p.force;
|
|
429
|
+
const f = this._frictionStep * ratio;
|
|
430
|
+
const dtSq = dt * dt;
|
|
431
|
+
const px = p[0];
|
|
432
|
+
const py = p[1];
|
|
433
|
+
const nx = px + (px - prev[0]) * f + (force[0] + this._gravity[0]) * dtSq;
|
|
434
|
+
const ny = py + (py - prev[1]) * f + (force[1] + this._gravity[1]) * dtSq;
|
|
435
|
+
prev[0] = px;
|
|
436
|
+
prev[1] = py;
|
|
437
|
+
p[0] = nx;
|
|
438
|
+
p[1] = ny;
|
|
439
|
+
p.timeStep = dt;
|
|
440
|
+
return p;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Internal function to update free particles for one substep: integrate, constrain to the
|
|
445
|
+
* bound, then resolve particle-particle collisions through the spatial hash.
|
|
446
|
+
*/
|
|
447
|
+
protected _updateParticles(dt: number) {
|
|
448
|
+
const ps = this._particles;
|
|
449
|
+
const len = ps.length;
|
|
450
|
+
if (len === 0) return;
|
|
451
|
+
|
|
452
|
+
const b0 = this._bound[0];
|
|
453
|
+
const b1 = this._bound[1];
|
|
454
|
+
const minX = Math.min(b0[0], b1[0]);
|
|
455
|
+
const minY = Math.min(b0[1], b1[1]);
|
|
456
|
+
const maxX = Math.max(b0[0], b1[0]);
|
|
457
|
+
const maxY = Math.max(b0[1], b1[1]);
|
|
458
|
+
|
|
459
|
+
for (let i = 0; i < len; i++) {
|
|
460
|
+
const p = ps[i];
|
|
461
|
+
this.integrate(p, dt);
|
|
462
|
+
World._boundParticle(p, minX, minY, maxX, maxY, this._damping);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
this._collideParticles();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Resolve particle-particle collisions using a uniform spatial hash (a counting-sort grid),
|
|
470
|
+
* visiting only neighboring cells instead of testing all pairs.
|
|
471
|
+
*/
|
|
472
|
+
private _collideParticles() {
|
|
473
|
+
const ps = this._particles;
|
|
474
|
+
const n = ps.length;
|
|
475
|
+
if (n < 2) return;
|
|
476
|
+
|
|
477
|
+
let rmax = 0;
|
|
478
|
+
for (let i = 0; i < n; i++) {
|
|
479
|
+
if (ps[i].radius > rmax) rmax = ps[i].radius;
|
|
480
|
+
}
|
|
481
|
+
if (rmax <= 0) return; // nothing can collide
|
|
482
|
+
|
|
483
|
+
// cells of 2×rmax mean any colliding pair is within the 3×3 neighborhood
|
|
484
|
+
const inv = 1 / (rmax * 2);
|
|
485
|
+
let m = 16;
|
|
486
|
+
while (m < n * 2) m <<= 1;
|
|
487
|
+
const mask = m - 1;
|
|
488
|
+
|
|
489
|
+
if (this._cellStart.length < m + 1)
|
|
490
|
+
this._cellStart = new Uint32Array(m + 1);
|
|
491
|
+
if (this._hashKeys.length < n) {
|
|
492
|
+
this._hashKeys = new Uint32Array(n * 2);
|
|
493
|
+
this._cellEntries = new Uint32Array(n * 2);
|
|
494
|
+
}
|
|
495
|
+
const keys = this._hashKeys;
|
|
496
|
+
const start = this._cellStart;
|
|
497
|
+
const entries = this._cellEntries;
|
|
498
|
+
|
|
499
|
+
// count per cell, exclusive prefix sum, then scatter; after the scatter,
|
|
500
|
+
// bucket k spans [start[k-1], start[k])
|
|
501
|
+
start.fill(0, 0, m + 1);
|
|
502
|
+
for (let i = 0; i < n; i++) {
|
|
503
|
+
const p = ps[i];
|
|
504
|
+
const key =
|
|
505
|
+
((Math.imul(Math.floor(p[0] * inv), 0x9e3779b1) ^
|
|
506
|
+
Math.imul(Math.floor(p[1] * inv), 0x85ebca77)) >>>
|
|
507
|
+
0) &
|
|
508
|
+
mask;
|
|
509
|
+
keys[i] = key;
|
|
510
|
+
start[key]++;
|
|
511
|
+
}
|
|
512
|
+
let sum = 0;
|
|
513
|
+
for (let k = 0; k < m; k++) {
|
|
514
|
+
const c = start[k];
|
|
515
|
+
start[k] = sum;
|
|
516
|
+
sum += c;
|
|
517
|
+
}
|
|
518
|
+
start[m] = sum;
|
|
519
|
+
for (let i = 0; i < n; i++) {
|
|
520
|
+
entries[start[keys[i]]++] = i;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const damping = this._damping;
|
|
524
|
+
const visited = this._neighborKeys;
|
|
525
|
+
for (let i = 0; i < n; i++) {
|
|
526
|
+
const p = ps[i];
|
|
527
|
+
const cx = Math.floor(p[0] * inv);
|
|
528
|
+
const cy = Math.floor(p[1] * inv);
|
|
529
|
+
// Visit each distinct hash key of the 3×3 neighborhood exactly once: two
|
|
530
|
+
// neighbor cells can collide to the same bucket, and visiting it twice
|
|
531
|
+
// would apply a pair's collision response twice.
|
|
532
|
+
let visitedCount = 0;
|
|
533
|
+
for (let gy = cy - 1; gy <= cy + 1; gy++) {
|
|
534
|
+
const hy = Math.imul(gy, 0x85ebca77);
|
|
535
|
+
for (let gx = cx - 1; gx <= cx + 1; gx++) {
|
|
536
|
+
const key = ((Math.imul(gx, 0x9e3779b1) ^ hy) >>> 0) & mask;
|
|
537
|
+
let seen = false;
|
|
538
|
+
for (let v = 0; v < visitedCount; v++) {
|
|
539
|
+
if (visited[v] === key) {
|
|
540
|
+
seen = true;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (seen) continue;
|
|
545
|
+
visited[visitedCount++] = key;
|
|
546
|
+
const end = start[key];
|
|
547
|
+
const begin = key > 0 ? start[key - 1] : 0;
|
|
548
|
+
for (let e = begin; e < end; e++) {
|
|
549
|
+
const j = entries[e];
|
|
550
|
+
if (j > i) p.collide(ps[j], damping);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Reset all accumulated forces after an update completes.
|
|
559
|
+
*/
|
|
560
|
+
private _clearForces() {
|
|
561
|
+
for (let i = 0, len = this._particles.length; i < len; i++) {
|
|
562
|
+
this._particles[i].force.fill(0);
|
|
563
|
+
}
|
|
564
|
+
for (let i = 0, len = this._bodies.length; i < len; i++) {
|
|
565
|
+
const bd = this._bodies[i];
|
|
566
|
+
for (let k = 0, klen = bd.length; k < klen; k++) {
|
|
567
|
+
(bd[k] as Particle).force.fill(0);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Internal function to update bodies for one substep: integrate and bound-constrain every
|
|
574
|
+
* body particle, resolve body-body and body-particle collisions behind an AABB broad
|
|
575
|
+
* phase, then restore shapes with the edge-constraint pass.
|
|
576
|
+
* @param dt substep time in seconds
|
|
577
|
+
*/
|
|
578
|
+
protected _updateBodies(dt: number) {
|
|
579
|
+
const bs = this._bodies;
|
|
580
|
+
const blen = bs.length;
|
|
581
|
+
if (blen === 0) return;
|
|
582
|
+
|
|
583
|
+
const b0 = this._bound[0];
|
|
584
|
+
const b1 = this._bound[1];
|
|
585
|
+
const minX = Math.min(b0[0], b1[0]);
|
|
586
|
+
const minY = Math.min(b0[1], b1[1]);
|
|
587
|
+
const maxX = Math.max(b0[0], b1[0]);
|
|
588
|
+
const maxY = Math.max(b0[1], b1[1]);
|
|
589
|
+
|
|
590
|
+
for (let i = 0; i < blen; i++) {
|
|
591
|
+
const bd = bs[i];
|
|
592
|
+
if (!bd) continue;
|
|
593
|
+
for (let k = 0, klen = bd.length; k < klen; k++) {
|
|
594
|
+
const bk = bd[k] as Particle;
|
|
595
|
+
this.integrate(bk, dt);
|
|
596
|
+
World._boundParticle(bk, minX, minY, maxX, maxY, this._damping);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
this._collideBodies(blen);
|
|
601
|
+
|
|
602
|
+
for (let i = 0; i < blen; i++) {
|
|
603
|
+
if (bs[i]) bs[i].solveEdges(dt, this._iterations, this._substeps);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Resolve body-body and body-particle collisions behind an AABB broad phase.
|
|
609
|
+
*/
|
|
610
|
+
private _collideBodies(blen: number) {
|
|
611
|
+
const bs = this._bodies;
|
|
612
|
+
|
|
613
|
+
// axis-aligned bounding boxes for the broad phase
|
|
614
|
+
if (this._bodyBounds.length < blen * 4) {
|
|
615
|
+
this._bodyBounds = new Float32Array(blen * 8);
|
|
616
|
+
}
|
|
617
|
+
const aabb = this._bodyBounds;
|
|
618
|
+
for (let i = 0; i < blen; i++) {
|
|
619
|
+
const bd = bs[i];
|
|
620
|
+
let bx0 = Infinity;
|
|
621
|
+
let by0 = Infinity;
|
|
622
|
+
let bx1 = -Infinity;
|
|
623
|
+
let by1 = -Infinity;
|
|
624
|
+
if (bd) {
|
|
625
|
+
for (let k = 0, klen = bd.length; k < klen; k++) {
|
|
626
|
+
const v = bd[k];
|
|
627
|
+
if (v[0] < bx0) bx0 = v[0];
|
|
628
|
+
if (v[0] > bx1) bx1 = v[0];
|
|
629
|
+
if (v[1] < by0) by0 = v[1];
|
|
630
|
+
if (v[1] > by1) by1 = v[1];
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
aabb[i * 4] = bx0;
|
|
634
|
+
aabb[i * 4 + 1] = by0;
|
|
635
|
+
aabb[i * 4 + 2] = bx1;
|
|
636
|
+
aabb[i * 4 + 3] = by1;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const ps = this._particles;
|
|
640
|
+
const plen = ps.length;
|
|
641
|
+
|
|
642
|
+
for (let i = 0; i < blen; i++) {
|
|
643
|
+
const bd = bs[i];
|
|
644
|
+
if (!bd) continue;
|
|
645
|
+
const ax0 = aabb[i * 4];
|
|
646
|
+
const ay0 = aabb[i * 4 + 1];
|
|
647
|
+
const ax1 = aabb[i * 4 + 2];
|
|
648
|
+
const ay1 = aabb[i * 4 + 3];
|
|
649
|
+
|
|
650
|
+
for (let k = i + 1; k < blen; k++) {
|
|
651
|
+
if (
|
|
652
|
+
bs[k] &&
|
|
653
|
+
ax0 <= aabb[k * 4 + 2] &&
|
|
654
|
+
ax1 >= aabb[k * 4] &&
|
|
655
|
+
ay0 <= aabb[k * 4 + 3] &&
|
|
656
|
+
ay1 >= aabb[k * 4 + 1]
|
|
657
|
+
) {
|
|
658
|
+
bd.processBody(bs[k]);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
for (let mIdx = 0; mIdx < plen; mIdx++) {
|
|
663
|
+
const p = ps[mIdx];
|
|
664
|
+
const r = p.radius;
|
|
665
|
+
if (
|
|
666
|
+
p[0] >= ax0 - r &&
|
|
667
|
+
p[0] <= ax1 + r &&
|
|
668
|
+
p[1] >= ay0 - r &&
|
|
669
|
+
p[1] <= ay1 + r
|
|
670
|
+
) {
|
|
671
|
+
bd.processParticle(p);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Particle is a subclass of [`Pt`](#link) that has radius and mass. It's usually added into [`World`](#link) to create physics simulations.
|
|
680
|
+
* See [a demo here](https://ptsjs.org/demo/?name=physics.particles).
|
|
681
|
+
*/
|
|
682
|
+
export class Particle extends Pt {
|
|
683
|
+
protected _mass: number = 1;
|
|
684
|
+
protected _radius: number = 0;
|
|
685
|
+
protected _force: Pt = new Pt();
|
|
686
|
+
protected _prev: Pt = new Pt();
|
|
687
|
+
protected _prevDt: number = 0; // seconds spanned by the `_prev` displacement; 0 = one frame
|
|
688
|
+
|
|
689
|
+
protected _body!: Body;
|
|
690
|
+
protected _lock: boolean = false;
|
|
691
|
+
protected _lockPt!: Pt;
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Create a particle. Once a particle is created, you can set its mass and radius via the corresponding accessors.
|
|
695
|
+
* @param args a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties
|
|
696
|
+
*/
|
|
697
|
+
constructor(...args: any[]) {
|
|
698
|
+
super(...args);
|
|
699
|
+
this._prev = this.clone();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Mass of this particle.
|
|
704
|
+
*/
|
|
705
|
+
get mass(): number {
|
|
706
|
+
return this._mass;
|
|
707
|
+
}
|
|
708
|
+
set mass(m: number) {
|
|
709
|
+
this._mass = m;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Radius of this particle.
|
|
714
|
+
*/
|
|
715
|
+
get radius(): number {
|
|
716
|
+
return this._radius;
|
|
717
|
+
}
|
|
718
|
+
set radius(f: number) {
|
|
719
|
+
this._radius = f;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Get this particle's previous position.
|
|
724
|
+
*/
|
|
725
|
+
get previous(): Pt {
|
|
726
|
+
return this._prev;
|
|
727
|
+
}
|
|
728
|
+
set previous(p: Pt) {
|
|
729
|
+
this._prev = p;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Get current accumulated force.
|
|
734
|
+
*/
|
|
735
|
+
get force(): Pt {
|
|
736
|
+
return this._force;
|
|
737
|
+
}
|
|
738
|
+
set force(g: Pt) {
|
|
739
|
+
this._force = g;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Get the body of this particle, if any.
|
|
744
|
+
*/
|
|
745
|
+
get body(): Body {
|
|
746
|
+
return this._body;
|
|
747
|
+
}
|
|
748
|
+
set body(b: Body) {
|
|
749
|
+
this._body = b;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Lock this particle in current position.
|
|
754
|
+
*/
|
|
755
|
+
get lock(): boolean {
|
|
756
|
+
return this._lock;
|
|
757
|
+
}
|
|
758
|
+
set lock(b: boolean) {
|
|
759
|
+
// On unlock, reset `previous` to the current position: while locked, collisions
|
|
760
|
+
// and dragging can leave it arbitrarily stale, and integrating that difference
|
|
761
|
+
// would launch the particle. (To throw a particle on release, use `hit`.)
|
|
762
|
+
if (this._lock && !b) this._prev.to(this);
|
|
763
|
+
this._lock = b;
|
|
764
|
+
this._lockPt = new Pt(this);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Get the change in position per 60 Hz frame, ie, the current velocity in the same unit
|
|
769
|
+
* as [`Particle.hit`](#link). The raw displacement since the last step is
|
|
770
|
+
* `particle.$subtract( particle.previous )`.
|
|
771
|
+
*/
|
|
772
|
+
get changed(): Pt {
|
|
773
|
+
const d = this.$subtract(this._prev);
|
|
774
|
+
return this._prevDt > 0 && this._prevDt !== FRAME
|
|
775
|
+
? d.multiply(FRAME / this._prevDt)
|
|
776
|
+
: d;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* The time in seconds spanned by the displacement from [`Particle.previous`](#link) to the
|
|
781
|
+
* current position. A [`World`](#link) sets it to the substep length on every step, and
|
|
782
|
+
* [`Particle.hit`](#link) and the `position` setter reset it to one 60 Hz frame (1/60),
|
|
783
|
+
* which is the unit of their velocities. 0 means unknown and is treated as one frame.
|
|
784
|
+
*/
|
|
785
|
+
get timeStep(): number {
|
|
786
|
+
return this._prevDt;
|
|
787
|
+
}
|
|
788
|
+
set timeStep(t: number) {
|
|
789
|
+
this._prevDt = t;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Set a new position, and update previous and lock states if needed. The move is stored as
|
|
794
|
+
* this particle's velocity per frame, so dragging a locked particle knocks others away.
|
|
795
|
+
*/
|
|
796
|
+
set position(p: Pt) {
|
|
797
|
+
this.previous.to(this);
|
|
798
|
+
this._prevDt = FRAME;
|
|
799
|
+
if (this._lock) this._lockPt = p;
|
|
800
|
+
this.to(p);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Set the size of this particle. This sets both the radius and the mass.
|
|
805
|
+
* @param r `radius` value, and also set `mass` to the same value.
|
|
806
|
+
*/
|
|
807
|
+
size(r: number): this {
|
|
808
|
+
this._mass = r;
|
|
809
|
+
this._radius = r;
|
|
810
|
+
return this;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Add to the accumulated force.
|
|
815
|
+
* @param args a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties
|
|
816
|
+
*/
|
|
817
|
+
addForce(...args: any[]): Pt {
|
|
818
|
+
this._force.add(...args);
|
|
819
|
+
return this._force;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Verlet integration.
|
|
824
|
+
* @param dt change in time in seconds
|
|
825
|
+
* @param friction friction from 0 to 1, where 1 means no friction
|
|
826
|
+
* @param lastDt optional last change in time in seconds. Default is [`Particle.timeStep`](#link), or `dt` if unknown.
|
|
827
|
+
*/
|
|
828
|
+
verlet(dt: number, friction: number, lastDt?: number): this {
|
|
829
|
+
// Positional verlet: curr + (curr - prev) + a * dt * dt
|
|
830
|
+
|
|
831
|
+
if (this._lock) {
|
|
832
|
+
// Pin the position only. `previous` is deliberately left alone: dragging a
|
|
833
|
+
// locked particle via the `position` setter stores the drag delta there, and
|
|
834
|
+
// collisions read it as the particle's velocity — this is what makes a
|
|
835
|
+
// pointer-dragged particle knock others away. Stale velocity is cleared at
|
|
836
|
+
// the moment of unlocking instead (see the `lock` setter).
|
|
837
|
+
this.to(this._lockPt);
|
|
838
|
+
} else {
|
|
839
|
+
// time corrected (https://en.wikipedia.org/wiki/Verlet_integration#Non-constant_time_differences)
|
|
840
|
+
const lt = lastDt ? lastDt : this._prevDt || dt;
|
|
841
|
+
const adt = (dt * (dt + lt)) / 2;
|
|
842
|
+
const f = (friction * dt) / lt;
|
|
843
|
+
const force = this._force;
|
|
844
|
+
const prev = this._prev;
|
|
845
|
+
for (let i = 0, len = this.length; i < len; i++) {
|
|
846
|
+
const cur = this[i];
|
|
847
|
+
const v = (cur - prev[i]) * f + (force[i] || 0) * adt;
|
|
848
|
+
prev[i] = cur;
|
|
849
|
+
this[i] = cur + v;
|
|
850
|
+
}
|
|
851
|
+
force.fill(0);
|
|
852
|
+
this._prevDt = dt;
|
|
853
|
+
}
|
|
854
|
+
return this;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Hit this particle with an impulse, in pixels per 60 Hz frame. The impulse is scaled by 1/√mass, so a heavier particle moves less from the same hit.
|
|
859
|
+
* The result is the same at any frame rate and any [`World.substeps`](#link) setting.
|
|
860
|
+
* @param args an impulse vector defined by either a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties
|
|
861
|
+
* @example `hit(10, 20)`, `hit( new Pt(5, 9) )`
|
|
862
|
+
*/
|
|
863
|
+
hit(...args: any[]): this {
|
|
864
|
+
// express the current velocity per frame, then add the impulse in the same unit
|
|
865
|
+
const prev = this._prev;
|
|
866
|
+
if (this._prevDt > 0 && this._prevDt !== FRAME) {
|
|
867
|
+
const r = FRAME / this._prevDt;
|
|
868
|
+
for (let i = 0, len = this.length; i < len; i++) {
|
|
869
|
+
prev[i] = this[i] - (this[i] - prev[i]) * r;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
this._prevDt = FRAME;
|
|
873
|
+
prev.subtract(new Pt(...args).$divide(Math.sqrt(this._mass)));
|
|
874
|
+
return this;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Check and respoond to collisions between this and another particle.
|
|
879
|
+
* @param p2 another particle
|
|
880
|
+
* @param damp damping value between 0 to 1, where 1 means no damping.
|
|
881
|
+
*/
|
|
882
|
+
collide(p2: Particle, damp: number = 1): void {
|
|
883
|
+
// reference: http://codeflow.org/entries/2010/nov/29/verlet-collision-with-impulse-preservation
|
|
884
|
+
// simultaneous collision not yet resolved. Possible solutions in this paper: https://www2.msm.ctw.utwente.nl/sluding/PAPERS/dem07.pdf
|
|
885
|
+
|
|
886
|
+
const p1 = this;
|
|
887
|
+
let dx = p1[0] - p2[0];
|
|
888
|
+
let dy = p1[1] - p2[1];
|
|
889
|
+
let distSq = dx * dx + dy * dy;
|
|
890
|
+
const dr = p1.radius + p2.radius;
|
|
891
|
+
if (distSq >= dr * dr) return;
|
|
892
|
+
|
|
893
|
+
const prev1 = p1.previous;
|
|
894
|
+
const prev2 = p2.previous;
|
|
895
|
+
let c1x = p1[0] - prev1[0];
|
|
896
|
+
let c1y = p1[1] - prev1[1];
|
|
897
|
+
let c2x = p2[0] - prev2[0];
|
|
898
|
+
let c2y = p2[1] - prev2[1];
|
|
899
|
+
|
|
900
|
+
// separation of (dist - dr) / 2 along the collision axis; for exactly
|
|
901
|
+
// coincident particles, separate deterministically along the x-axis
|
|
902
|
+
let dist = Math.sqrt(distSq);
|
|
903
|
+
let k: number;
|
|
904
|
+
if (dist < 0.000001) {
|
|
905
|
+
dx = 1;
|
|
906
|
+
dy = 0;
|
|
907
|
+
dist = 1;
|
|
908
|
+
distSq = 1;
|
|
909
|
+
k = -dr / 2;
|
|
910
|
+
} else {
|
|
911
|
+
k = (dist - dr) / dist / 2;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const np1x = p1[0] - dx * k;
|
|
915
|
+
const np1y = p1[1] - dy * k;
|
|
916
|
+
const np2x = p2[0] + dx * k;
|
|
917
|
+
const np2y = p2[1] + dy * k;
|
|
918
|
+
|
|
919
|
+
const f1 = (damp * (dx * c1x + dy * c1y)) / distSq;
|
|
920
|
+
const f2 = (damp * (dx * c2x + dy * c2y)) / distSq;
|
|
921
|
+
const dm1 = p1.mass / (p1.mass + p2.mass);
|
|
922
|
+
const dm2 = p2.mass / (p1.mass + p2.mass);
|
|
923
|
+
|
|
924
|
+
c1x += (f2 - f1) * dx * dm2;
|
|
925
|
+
c1y += (f2 - f1) * dy * dm2;
|
|
926
|
+
c2x += (f1 - f2) * dx * dm1;
|
|
927
|
+
c2y += (f1 - f2) * dy * dm1;
|
|
928
|
+
|
|
929
|
+
p1[0] = np1x;
|
|
930
|
+
p1[1] = np1y;
|
|
931
|
+
p2[0] = np2x;
|
|
932
|
+
p2[1] = np2y;
|
|
933
|
+
prev1[0] = np1x - c1x;
|
|
934
|
+
prev1[1] = np1y - c1y;
|
|
935
|
+
prev2[0] = np2x - c2x;
|
|
936
|
+
prev2[1] = np2y - c2y;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Get a string representation of this particle
|
|
941
|
+
*/
|
|
942
|
+
toString(): string {
|
|
943
|
+
return `Particle: ${this[0]} ${this[1]} | previous ${this._prev[0]} ${this._prev[1]} | mass ${this._mass}`;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Body is a subclass of [`Group`](#link) that stores a set of [`Particle`](#link)s and edge constraints. It is usually added into a [`World`](#link) to create physics simulations.
|
|
949
|
+
* See [a demo here](https://ptsjs.org/demo/?name=physics.shapes).
|
|
950
|
+
*/
|
|
951
|
+
export class Body extends Group {
|
|
952
|
+
protected _cs: Array<number[]> = [];
|
|
953
|
+
protected _stiff: number = 1;
|
|
954
|
+
protected _locks: { [index: string]: Particle } = {};
|
|
955
|
+
protected _mass: number = 1;
|
|
956
|
+
protected _lambdas: Float32Array = new Float32Array(0); // XPBD multipliers, one per link
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Create an empty Body, this is usually followed by [`Body.init`](#link) to populate the Body. Alternatively, use static function [`Body.fromGroup`](#link) to create and initate a body directly.
|
|
960
|
+
*/
|
|
961
|
+
constructor() {
|
|
962
|
+
super();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Create and populate a body.
|
|
967
|
+
* @param body a Group or an Iterable<Pt> to define the body
|
|
968
|
+
* @param stiff stiffness value from 0 to 1, where 1 is the most stiff. Default is 1.
|
|
969
|
+
* @param autoLink Automatically create links between the Pts. This usually works for regular convex polygons. Default is true.
|
|
970
|
+
* @param autoMass Automatically calculate the mass based on the area of the polygon. Default is true.
|
|
971
|
+
*/
|
|
972
|
+
static fromGroup(
|
|
973
|
+
body: PtIterable,
|
|
974
|
+
stiff: number = 1,
|
|
975
|
+
autoLink: boolean = true,
|
|
976
|
+
autoMass: boolean = true,
|
|
977
|
+
): Body {
|
|
978
|
+
let b = new Body().init(body);
|
|
979
|
+
if (autoLink) b.linkAll(stiff);
|
|
980
|
+
if (autoMass) b.autoMass();
|
|
981
|
+
return b;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Initiate a body.
|
|
986
|
+
* @param body a Group or an Iterable<Pt> to define a body
|
|
987
|
+
* @param stiff stiffness value from 0 to 1, where 1 is the most stiff. Default is 1.
|
|
988
|
+
*/
|
|
989
|
+
init(body: PtIterable, stiff: number = 1): this {
|
|
990
|
+
for (let li of body) {
|
|
991
|
+
let p = new Particle(li);
|
|
992
|
+
p.body = this;
|
|
993
|
+
this.push(p);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
this._stiff = stiff;
|
|
997
|
+
|
|
998
|
+
return this;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Get mass of this body.
|
|
1003
|
+
*/
|
|
1004
|
+
get mass(): number {
|
|
1005
|
+
return this._mass;
|
|
1006
|
+
}
|
|
1007
|
+
set mass(m: number) {
|
|
1008
|
+
this._mass = m;
|
|
1009
|
+
for (let i = 0, len = this.length; i < len; i++) {
|
|
1010
|
+
(this[i] as Particle).mass = this._mass;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Automatically calculate a body's `mass` based on the area of the polygon.
|
|
1016
|
+
*/
|
|
1017
|
+
autoMass(): this {
|
|
1018
|
+
this.mass = Math.sqrt(Polygon.area(this)) / 10;
|
|
1019
|
+
return this;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Create a linked edge between two points.
|
|
1024
|
+
* @param index1 first point by index
|
|
1025
|
+
* @param index2 first point by index
|
|
1026
|
+
* @param stiff optionally stiffness value between 0 to 1, where 1 is the most stiff.
|
|
1027
|
+
*/
|
|
1028
|
+
link(index1: number, index2: number, stiff?: number): this {
|
|
1029
|
+
if (index1 < 0 || index1 >= this.length)
|
|
1030
|
+
throw new Error("index1 is not in the Group's indices");
|
|
1031
|
+
if (index2 < 0 || index2 >= this.length)
|
|
1032
|
+
throw new Error("index1 is not in the Group's indices");
|
|
1033
|
+
|
|
1034
|
+
let d = this[index1].$subtract(this[index2]).magnitude();
|
|
1035
|
+
this._cs.push([index1, index2, d, stiff || this._stiff]);
|
|
1036
|
+
return this;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Automatically create links for all the points to preserve the initial body shape. This usually works for regular convex polygon.
|
|
1041
|
+
* @param stiff optionally stiffness value between 0 to 1, where 1 is the most stiff.
|
|
1042
|
+
*/
|
|
1043
|
+
linkAll(stiff: number): void {
|
|
1044
|
+
const half = this.length / 2;
|
|
1045
|
+
|
|
1046
|
+
// skip duplicate pairs and self-pairs, which the cross-link passes below
|
|
1047
|
+
// can produce for odd sizes (a duplicate would double-solve an edge);
|
|
1048
|
+
// a linear scan of the small link list beats allocating a Set here
|
|
1049
|
+
const tryLink = (a: number, b: number, s?: number) => {
|
|
1050
|
+
if (a === b) return;
|
|
1051
|
+
const cs = this._cs;
|
|
1052
|
+
for (let k = 0, klen = cs.length; k < klen; k++) {
|
|
1053
|
+
const c = cs[k];
|
|
1054
|
+
if ((c[0] === a && c[1] === b) || (c[0] === b && c[1] === a)) return;
|
|
1055
|
+
}
|
|
1056
|
+
this.link(a, b, s);
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
for (let i = 0, len = this.length; i < len; i++) {
|
|
1060
|
+
const n = i >= len - 1 ? 0 : i + 1;
|
|
1061
|
+
tryLink(i, n, stiff);
|
|
1062
|
+
|
|
1063
|
+
if (len > 4) {
|
|
1064
|
+
const nd = Math.floor(half / 2) + 1;
|
|
1065
|
+
const n2 = i >= len - nd ? i % len : i + nd;
|
|
1066
|
+
tryLink(i, n2, stiff);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (i <= half - 1) {
|
|
1070
|
+
tryLink(i, Math.min(this.length - 1, i + Math.floor(half)));
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Return a list of all the linked edges as line segments.
|
|
1077
|
+
* @returns an array of Groups, each of which represents an edge
|
|
1078
|
+
*/
|
|
1079
|
+
linksToLines(): Group[] {
|
|
1080
|
+
let gs = [];
|
|
1081
|
+
for (let i = 0, len = this._cs.length; i < len; i++) {
|
|
1082
|
+
let ln = this._cs[i];
|
|
1083
|
+
gs.push(new Group(this[ln[0]], this[ln[1]]));
|
|
1084
|
+
}
|
|
1085
|
+
return gs;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Recalculate all edge constraints.
|
|
1090
|
+
*/
|
|
1091
|
+
processEdges(): void {
|
|
1092
|
+
for (let i = 0, len = this._cs.length; i < len; i++) {
|
|
1093
|
+
let [m, n, d, s] = this._cs[i];
|
|
1094
|
+
World.edgeConstraint(this[m] as Particle, this[n] as Particle, d, s);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Solve all edge constraints for one substep in XPBD form. A link's `stiff` value is a
|
|
1100
|
+
* geometric knob: it is the fraction of the remaining constraint violation resolved per
|
|
1101
|
+
* update, independent of the particles' masses and of the substep/iteration counts
|
|
1102
|
+
* (per Müller et al. 2007, the per-pass fraction is `1-(1-stiff)^(1/passes)`), and
|
|
1103
|
+
* `stiff=1` is a rigid projection. Mapping the fraction to a mass-relative compliance
|
|
1104
|
+
* keeps a heavy body exactly as stiff as a light one at the same value.
|
|
1105
|
+
* This is the solver used internally by [`World.update`](#link);
|
|
1106
|
+
* [`Body.processEdges`](#link) remains the simpler relaxation for direct use.
|
|
1107
|
+
* @param dt substep time in seconds (reserved; the geometric stiffness does not depend on it)
|
|
1108
|
+
* @param iterations solver iterations for this substep. Default is 1.
|
|
1109
|
+
* @param substeps the caller's substeps per update, for pass-count-independent stiffness. Default is 1.
|
|
1110
|
+
*/
|
|
1111
|
+
solveEdges(dt: number, iterations: number = 1, substeps: number = 1): this {
|
|
1112
|
+
const cs = this._cs;
|
|
1113
|
+
const clen = cs.length;
|
|
1114
|
+
if (clen === 0) return this;
|
|
1115
|
+
|
|
1116
|
+
if (this._lambdas.length < clen) this._lambdas = new Float32Array(clen);
|
|
1117
|
+
const lambdas = this._lambdas;
|
|
1118
|
+
lambdas.fill(0, 0, clen);
|
|
1119
|
+
const invPasses = 1 / Math.max(1, iterations * substeps);
|
|
1120
|
+
|
|
1121
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
1122
|
+
for (let ci = 0; ci < clen; ci++) {
|
|
1123
|
+
const c = cs[ci];
|
|
1124
|
+
const p1 = this[c[0]] as Particle;
|
|
1125
|
+
const p2 = this[c[1]] as Particle;
|
|
1126
|
+
const stiff = c[3];
|
|
1127
|
+
|
|
1128
|
+
const w1 = p1.lock ? 0 : 1 / (p1.mass || 1);
|
|
1129
|
+
const w2 = p2.lock ? 0 : 1 / (p2.mass || 1);
|
|
1130
|
+
const w = w1 + w2;
|
|
1131
|
+
if (w === 0) continue;
|
|
1132
|
+
|
|
1133
|
+
const dx = p2[0] - p1[0];
|
|
1134
|
+
const dy = p2[1] - p1[1];
|
|
1135
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1136
|
+
if (dist < 0.000001) continue;
|
|
1137
|
+
|
|
1138
|
+
// mass-relative compliance: the per-pass correction fraction is exactly
|
|
1139
|
+
// `sEff` regardless of mass, since w / (w + alpha) = sEff
|
|
1140
|
+
let alpha = 0;
|
|
1141
|
+
if (stiff < 1) {
|
|
1142
|
+
const sEff = 1 - Math.pow(1 - stiff, invPasses);
|
|
1143
|
+
alpha = sEff > 0.000001 ? (w * (1 - sEff)) / sEff : w * 1000000;
|
|
1144
|
+
}
|
|
1145
|
+
const dl = (-(dist - c[2]) - alpha * lambdas[ci]) / (w + alpha);
|
|
1146
|
+
lambdas[ci] += dl;
|
|
1147
|
+
|
|
1148
|
+
const s = dl / dist;
|
|
1149
|
+
const fx = dx * s;
|
|
1150
|
+
const fy = dy * s;
|
|
1151
|
+
p1[0] -= fx * w1;
|
|
1152
|
+
p1[1] -= fy * w1;
|
|
1153
|
+
p2[0] += fx * w2;
|
|
1154
|
+
p2[1] += fy * w2;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return this;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Check and respond to collisions between two bodies.
|
|
1162
|
+
* @param b another body
|
|
1163
|
+
*/
|
|
1164
|
+
processBody(b: Body): void {
|
|
1165
|
+
let b1 = this;
|
|
1166
|
+
let b2 = b;
|
|
1167
|
+
|
|
1168
|
+
let hit = Polygon.hasIntersectPolygon(b1, b2);
|
|
1169
|
+
|
|
1170
|
+
if (hit) {
|
|
1171
|
+
let cv = hit.normal.$multiply(hit.dist);
|
|
1172
|
+
|
|
1173
|
+
let t;
|
|
1174
|
+
let eg = hit.edge;
|
|
1175
|
+
if (Math.abs(eg[0][0] - eg[1][0]) > Math.abs(eg[0][1] - eg[1][1])) {
|
|
1176
|
+
t = (hit.vertex[0] - cv[0] - eg[0][0]) / (eg[1][0] - eg[0][0]);
|
|
1177
|
+
} else {
|
|
1178
|
+
t = (hit.vertex[1] - cv[1] - eg[0][1]) / (eg[1][1] - eg[0][1]);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
let lambda = 1 / (t * t + (1 - t) * (1 - t));
|
|
1182
|
+
|
|
1183
|
+
let m0 = (hit.vertex as Particle).body.mass || 1;
|
|
1184
|
+
let m1 = (hit.edge[0] as Particle).body.mass || 1;
|
|
1185
|
+
let mr0 = m0 / (m0 + m1);
|
|
1186
|
+
let mr1 = m1 / (m0 + m1);
|
|
1187
|
+
|
|
1188
|
+
eg[0].subtract(cv.$multiply((mr0 * (1 - t) * lambda) / 2));
|
|
1189
|
+
eg[1].subtract(cv.$multiply((mr0 * t * lambda) / 2));
|
|
1190
|
+
|
|
1191
|
+
hit.vertex.add(cv.$multiply(mr1));
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Check and respond to collisions between this body and a particle.
|
|
1197
|
+
* @param b a particle
|
|
1198
|
+
*/
|
|
1199
|
+
processParticle(b: Particle): void {
|
|
1200
|
+
let b1 = this;
|
|
1201
|
+
let b2 = b;
|
|
1202
|
+
|
|
1203
|
+
let hit = Polygon.hasIntersectCircle(b1, Circle.fromCenter(b, b.radius));
|
|
1204
|
+
|
|
1205
|
+
if (hit) {
|
|
1206
|
+
let cv = hit.normal.$multiply(hit.dist);
|
|
1207
|
+
|
|
1208
|
+
let t;
|
|
1209
|
+
let eg = hit.edge;
|
|
1210
|
+
if (Math.abs(eg[0][0] - eg[1][0]) > Math.abs(eg[0][1] - eg[1][1])) {
|
|
1211
|
+
t = (hit.vertex[0] - cv[0] - eg[0][0]) / (eg[1][0] - eg[0][0]);
|
|
1212
|
+
} else {
|
|
1213
|
+
t = (hit.vertex[1] - cv[1] - eg[0][1]) / (eg[1][1] - eg[0][1]);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
let lambda = 1 / (t * t + (1 - t) * (1 - t));
|
|
1217
|
+
// hit.vertex is the circle's center Pt (not a Particle), so the
|
|
1218
|
+
// particle's own mass is the vertex-side mass here
|
|
1219
|
+
let m0 = b2.mass || 1;
|
|
1220
|
+
let m1 = (hit.edge[0] as Particle).body.mass || 1;
|
|
1221
|
+
|
|
1222
|
+
let mr0 = m0 / (m0 + m1);
|
|
1223
|
+
let mr1 = m1 / (m0 + m1);
|
|
1224
|
+
|
|
1225
|
+
eg[0].subtract(cv.$multiply((mr0 * (1 - t) * lambda) / 2));
|
|
1226
|
+
eg[1].subtract(cv.$multiply((mr0 * t * lambda) / 2));
|
|
1227
|
+
|
|
1228
|
+
// raw displacement (not `changed`, which is per frame)
|
|
1229
|
+
let c1 = b.$subtract(b.previous).add(cv.$multiply(mr1));
|
|
1230
|
+
b.previous = b.$subtract(c1);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|