├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build-figures.sh ├── canvaskit.wasm ├── cli-figure-to-apng.ts ├── collision-2d.js ├── docs ├── aabb-aabb-overlap.png ├── aabb-aabb-sweep1.png ├── aabb-aabb-sweep2.png ├── aabb-point-overlap.png ├── aabb-segment-overlap.png ├── aabb-segment-sweep1.png ├── aabb-segments-sweep1-indexed.png ├── cone-point-overlap.png ├── ray-plane-distance.png ├── ray-sphere-overlap.png ├── segment-point-overlap.png ├── segment-segment-overlap.png ├── segment-sphere-overlap.png ├── segments-segment-overlap.png ├── segments-sphere-sweep1.png ├── sphere-sphere-overlap.png └── triangle-point-overlap.png ├── figures.html ├── figures ├── aabb-aabb-overlap.js ├── aabb-aabb-sweep1.js ├── aabb-aabb-sweep2.js ├── aabb-point-overlap.js ├── aabb-segment-overlap.js ├── aabb-segment-sweep1.js ├── aabb-segments-sweep1-indexed.js ├── common.js ├── cone-point-overlap.js ├── ray-plane-distance.js ├── ray-sphere-overlap.js ├── segment-point-overlap.js ├── segment-segment-overlap.js ├── segment-sphere-overlap.js ├── segments-segment-overlap.js ├── segments-sphere-sweep1.js ├── sphere-sphere-overlap.js └── triangle-point-overlap.js ├── package-lock.json ├── package.json ├── src ├── AABB.js ├── Plane.js ├── Polygon.js ├── PolylinePath.js ├── TraceInfo.js ├── aabb-aabb-contain.js ├── aabb-aabb-overlap.js ├── aabb-aabb-sweep1.js ├── aabb-aabb-sweep2.js ├── aabb-point-overlap.js ├── aabb-segment-overlap.js ├── aabb-segment-sweep1.js ├── aabb-segments-sweep1-indexed.js ├── cone-point-overlap.js ├── contact-copy.js ├── contact.js ├── cpa-time.js ├── get-lowest-root.js ├── point-polygon-overlap.js ├── ray-sphere-overlap.js ├── segment-normal.js ├── segment-point-overlap.js ├── segment-segment-overlap.js ├── segment-sphere-overlap.js ├── segments-ellipsoid-sweep1-indexed.js ├── segments-segment-overlap-indexed.js ├── segments-segment-overlap.js ├── segments-sphere-sweep1-indexed.js ├── segments-sphere-sweep1.js ├── segseg-closest.js ├── sphere-point-overlap.js ├── sphere-sphere-collision-response.js ├── sphere-sphere-overlap.js ├── sphere-sphere-sweep2.js ├── toji-tris.js ├── triangle-area.js ├── triangle-get-center.js └── triangle-point-overlap.js └── test ├── _assert.js ├── aabb-point-overlap.js ├── aabb-segment-overlap.js ├── segment-segment-overlap.js ├── segments-ellipsoid-sweep1-indexed.js └── trace-sphere-triangle.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | deno.lock 4 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.0 2 | * add Polygon-point checks 3 | 4 | 5 | # 0.2.0 6 | * add PolylinePath 7 | 8 | 9 | # 0.1.0 10 | * published to npm 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Michael Reinstein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # collision-2d 2 | 3 | There are many javascript collision routines and libraries for 2d. None satisifed all of these criteria: 4 | 5 | * consistent vector/matrix/line representation 6 | * doesn't generate memory garbage 7 | * is data-oriented and functional 8 | * consistent API interface 9 | * collisions only - no gravity, rigid body handling, or complex solvers 10 | * pure es modules 11 | 12 | so here we are! 13 | 14 | 15 | Note: If you're looking for higher-level 2d collision handling routine for ellipsoids vs line segments, check out https://github.com/mreinstein/collide-and-slide-2d 16 | 17 | 18 | ## available collision checks 19 | 20 | 21 | ### aabb-aabb overlap 22 | 23 | ![alt text](docs/aabb-aabb-overlap.png "AABB-AABB overlap test") 24 | 25 | ```javascript 26 | import { aabbOverlap } from '@footgun/collision-2d' 27 | 28 | const collided = aabbOverlap(aabb, aabb2, contact) 29 | ``` 30 | 31 | ### aabb-aabb contain 32 | 33 | ```javascript 34 | import { aabbContain } from '@footgun/collision-2d' 35 | 36 | // true when aabb1 fully contains aabb2 (2 is fully inside the bounds of 1) 37 | const contains = aabbContain(aabb1, aabb2) 38 | ``` 39 | 40 | 41 | ### aabb-aabb sweep 1 42 | 43 | ![alt text](docs/aabb-aabb-sweep1.png "AABB-AABB sweep 1 test") 44 | 45 | ```javascript 46 | import { aabbSweep1 } from '@footgun/collision-2d' 47 | 48 | const collided = aabbSweep1(aabb, aabb2, delta, contact) 49 | ``` 50 | 51 | 52 | ### aabb-aabb sweep 2 53 | 54 | ![alt text](docs/aabb-aabb-sweep2.png "AABB-AABB sweep 2 test") 55 | 56 | ```javascript 57 | import { aabbSweep2 } from '@footgun/collision-2d' 58 | 59 | const collided = aabbSweep2(aabb, delta, aabb2, delta2, contact) 60 | ``` 61 | 62 | 63 | ### aabb-segment sweep 64 | 65 | ![alt text](docs/aabb-segment-sweep1.png "AABB-segment sweep test") 66 | 67 | ```javascript 68 | import { aabbSegSweep1 } from '@footgun/collision-2d' 69 | 70 | const collided = aabbSegSweep1(line, aabb, delta, contact) 71 | ``` 72 | 73 | 74 | ### aabb-segments sweep-indexed 75 | 76 | ![alt text](docs/aabb-segments-sweep1-indexed.png "AABB-segments indexed sweep test") 77 | 78 | ```javascript 79 | import { aabbSegsSweep1Indexed } from '@footgun/collision-2d' 80 | 81 | const collided = aabbSegsSweep1Indexed(segments, indices, segmentCount, aabb, delta, contact) 82 | ``` 83 | 84 | if there is a collision, `contact.collider` will be an integer indicating the index of which segment in the `segments` array collided. 85 | 86 | 87 | ### aabb-point overlap 88 | 89 | ![alt text](docs/aabb-point-overlap.png "AABB-point overlap test") 90 | 91 | ```javascript 92 | import { aabbPointOverlap } from '@footgun/collision-2d' 93 | 94 | const collided = aabbPointOverlap(aabb, point, contact) 95 | ``` 96 | 97 | 98 | ### aabb-segment overlap 99 | 100 | ![alt text](docs/aabb-segment-overlap.png "AABB-segment overlap test") 101 | 102 | ```javascript 103 | import { aabbSegOverlap } from '@footgun/collision-2d' 104 | 105 | const collided = aabbSegOverlap(aabb, pos, delta, paddingX, paddingY, contact) 106 | ``` 107 | 108 | 109 | 110 | ### ray-plane-distance 111 | 112 | ![alt text](docs/ray-plane-distance.png "ray-plane distance") 113 | 114 | ```javascript 115 | import { Plane } from '@footgun/collision-2d' 116 | 117 | const p = Plane.create() 118 | Plane.fromPlane(p, planeOrigin, planeNormal) 119 | const distance = Plane.rayDistance(p, rayOrigin, rayVector) 120 | 121 | ``` 122 | 123 | 124 | ### ray-sphere overlap 125 | 126 | ![alt text](docs/ray-sphere-overlap.png "ray-sphere overlap test") 127 | 128 | ```javascript 129 | import { raySphereOverlap } from '@footgun/collision-2d' 130 | 131 | 132 | // declare 2 points that lie on an infinite ray 133 | const p1 = [ 100, 100 ] 134 | const p2 = [ 200, 100 ] 135 | 136 | const sphereCenter: [ 250, 100 ] 137 | const sphereRadius: 50 138 | const contact = { mu1: NaN, mu2: NaN } 139 | const overlaps = raySphereOverlap(p1, p2, sphereCenter, sphereRadius, contact) 140 | 141 | // mu1 and mu2 are the points along the line segment from p1 to p2 where the sphere intersection occurs: 142 | // intersection1 = p1 + contact.mu1 * (p2 - p1) 143 | // intersection2 = p1 + contact.mu2 * (p2 - p1) 144 | if (overlaps) { 145 | console.log('sphere intersection time 1:', contact.mu1) 146 | console.log('sphere intersection time 2', contact.mu2) 147 | } 148 | ``` 149 | 150 | 151 | ### segment-sphere overlap 152 | 153 | ![alt text](docs/segment-sphere-overlap.png "segment-sphere overlap test") 154 | 155 | ```javascript 156 | import { segSphereOverlap } from '@footgun/collision-2d' 157 | 158 | 159 | // declare 2 points that lie on a line segment 160 | const p1 = [ 100, 100 ] 161 | const p2 = [ 200, 100 ] 162 | 163 | const sphereCenter: [ 250, 100 ] 164 | const sphereRadius: 50 165 | const contact = { intersectionCount: 0, mu1: NaN, mu2: NaN } 166 | const overlaps = segSphereOverlap(p1, p2, sphereCenter, sphereRadius, contact) 167 | 168 | // mu1 and mu2 are the points along the line segment from p1 to p2 where the sphere intersection occurs: 169 | // intersection1 = p1 + contact.mu1 * (p2 - p1) 170 | // intersection2 = p1 + contact.mu2 * (p2 - p1) 171 | if (overlaps) { 172 | // the segment interesects the sphere, intersectionCount is 1 or 2 173 | // either mu1 or mu2 will be NaN if there's not 2 intersections 174 | console.log('intersection count:', contact.intersectionCount) 175 | console.log('sphere intersection time 1:', contact.mu1) 176 | console.log('sphere intersection time 2', contact.mu2) 177 | } else { 178 | // no overlap, contact.intersectionCount is 0 179 | } 180 | ``` 181 | 182 | 183 | ### segment-normal 184 | 185 | ```javascript 186 | import { segNormal } from '@footgun/collision-2d' 187 | 188 | const normal = segNormal(vec2.create(), pos1, pos2) 189 | ``` 190 | 191 | 192 | ### segment-point-overlap 193 | 194 | ![alt text](docs/segment-point-overlap.png "segment-point overlap test") 195 | 196 | ```javascript 197 | import { segPointOverlap } from '@footgun/collision-2d' 198 | 199 | const collided = segPointOverlap(p, segPoint0, segPoint1) // true or false 200 | ``` 201 | 202 | 203 | ### segment-segment-overlap 204 | 205 | ![alt text](docs/segment-segment-overlap.png "segment-segment overlap test") 206 | 207 | ```javascript 208 | import { segOverlap } from '@footgun/collision-2d' 209 | 210 | const intersectionPoint = vec2.create() 211 | if (segOverlap(seg1Point1, seg1Point2, seg2Point1, seg2Point2, intersectionPoint)) { 212 | // if we get here, intersectionPoint is filled in with where the 2 segments overlap 213 | } 214 | ``` 215 | 216 | 217 | ### segments-segment-overlap 218 | 219 | ![alt text](docs/segments-segment-overlap.png "segments-segment overlap test") 220 | 221 | ```javascript 222 | import { segsSegOverlap } from '@footgun/collision-2d' 223 | 224 | const collided = segsSegOverlap(segments, start, delta, contact) 225 | ``` 226 | 227 | if there is a collision, `contact.collider` will be an integer indicating the index of which segment in the `segments` array collided. 228 | 229 | 230 | ### segments-segment-overlap-indexed 231 | 232 | ```javascript 233 | import { segsSegOverlapIndexed } from '@footgun/collision-2d' 234 | 235 | const segs = [ 236 | [ p0, p1 ], 237 | [ p2, p3 ], 238 | [ p4, p5 ] 239 | ] 240 | const indices = [ 0, 2 ] // indices into the segs array 241 | 242 | const segmentCount = 2 // numer of indices to include. only run the segmentsSegment intersection tests on [ p0, p1 ] and [ p4, p5] 243 | 244 | const collided = segsSegOverlapIndexed(segments, indices, segmentCount, start, delta, contact) 245 | ``` 246 | 247 | if there is a collision, `contact.collider` will be an integer indicating the index of which segment in the `segments` array collided. 248 | 249 | 250 | 251 | ### segments-sphere-sweep 1 252 | 253 | ![alt text](docs/segments-sphere-sweep1.png "segments-sphere sweep test") 254 | 255 | 256 | ```javascript 257 | import { segsSphereSweep1 } from '@footgun/collision-2d' 258 | 259 | const collided = segsSphereSweep1(segments, position, radius, delta, contact) 260 | ``` 261 | 262 | if there is a collision, `contact.collider` will be an integer indicating the index of which segment in the `segments` array collided. 263 | 264 | 265 | ### segments-sphere-sweep-1-indexed 266 | 267 | ```javascript 268 | import { segsSphereSweep1Indexed } from '@footgun/collision-2d' 269 | 270 | const segs = [ 271 | [ p0, p1 ], 272 | [ p2, p3 ], 273 | [ p4, p5 ] 274 | ] 275 | const indices = [ 0, 2 ] // indices into the segs array 276 | 277 | const segmentCount = 2 // only run the segmentsSphereSweep tests on [ p0, p1 ] and [ p4, p5 ] 278 | 279 | const collided = segsSphereSweep1Indexed(segments, indices, segmentCount, position, radius, delta, contact) 280 | ``` 281 | 282 | if there is a collision, `contact.collider` will be an integer indicating the index of which segment in the `segments` array collided. 283 | 284 | 285 | ### sphere-sphere-overlap 286 | 287 | ![alt text](docs/sphere-sphere-overlap.png "sphere-sphere overlap test") 288 | 289 | ```javascript 290 | import { sphereOverlap } from '@footgun/collision-2d' 291 | 292 | const collided = sphereOverlap(centerA, radiusA, centerB, radiusB, contact) // collided is true or false 293 | ``` 294 | 295 | if there is a collision, `contact.delta` is a vector that can be added to sphere A’s position to move them into a non-colliding state. 296 | `contact.position` is the point of contact of these 2 spheres 297 | 298 | Note: `contact` is an optional parameter. if you only want to determine if the 2 spheres overlap, omit `contact` which will be faster. 299 | 300 | 301 | ### sphere-sphere-sweep2 302 | 303 | ```javascript 304 | import { sphereSweep2 } from '@footgun/collision-2d' 305 | 306 | const collided = sphereSweep2(radiusA, A0, A1, radiusB, B0, B1, contact) 307 | ``` 308 | 309 | * `A0` is the previous position of sphere A 310 | * `A1` is the new position of sphere A 311 | * `B0` is the previous position of sphere B 312 | * `B1` is the new position of sphere B 313 | 314 | If there is a collision `contact.position` will contain the point where the collision occurred. `contact.time` has the normalized time 315 | where the collision happened. 316 | 317 | 318 | ### cone-point-overlap 319 | 320 | ![alt text](docs/cone-point-overlap.png "cone-point overlap test") 321 | 322 | ```javascript 323 | import { conePointOverlap } from '@footgun/collision-2d' 324 | 325 | const collided = conePointOverlap(conePosition, coneRotation, coneFieldOfView, coneMinDistance, coneMaxDistance, point) // collided is true or false 326 | ``` 327 | 328 | 329 | ### triangle-point-overlap 330 | 331 | ![alt text](docs/triangle-point-overlap.png "triangle-point overlap test") 332 | 333 | ```javascript 334 | import { trianglePointOverlap } from '@footgun/collision-2d' 335 | 336 | const collided = trianglePointOverlap(v0, v1, v2, point) // collided is true or false 337 | ``` 338 | 339 | 340 | ## entities 341 | 342 | The collision routines all use these entity definitions 343 | 344 | 345 | ### point 346 | 347 | a point is a 2d vector, which is represented as an array with 2 values: 348 | ```javascript 349 | 350 | const position = [ 200, 150 ] // x: 200, y: 150 351 | 352 | ``` 353 | 354 | We use the fantastic `gl-matrix` `vec2` for representing these. 355 | 356 | 357 | ### aabb 358 | 359 | an axially aligned bounding box 360 | ```javascript 361 | const aabb = { 362 | position: [ 200, 100 ], // center point of the AABB 363 | width: 50, 364 | height: 50 365 | } 366 | ``` 367 | 368 | ### segment 369 | 370 | a line segment consists of 2 `point`s 371 | ```javascript 372 | const segment = [ 373 | [ 0, 0 ], // starting point of line 374 | [ 100, 0 ] // ending point of line 375 | ] 376 | ``` 377 | 378 | 379 | ### plane 380 | 381 | a 2d plane 382 | 383 | ```javascript 384 | { 385 | origin: vec2.create(), 386 | normal: vec2.create(), 387 | D: 0, 388 | } 389 | ``` 390 | 391 | 392 | ### contact 393 | 394 | The data structure populated when a collision occurs 395 | 396 | ```javascript 397 | { 398 | // for segments-segment-overlap and segments-sphere-sweep1 this is set to the index 399 | // in the array of line segments passed into the collision routine 400 | // for all other routines, collider is a reference to the colliding object itself 401 | collider : null, 402 | 403 | position : [ 0, 0 ], // the exact position of the collision 404 | delta : [ 0, 0 ], // a vector that can be applied to get out of the colliding state 405 | normal : [ 0, 0 ], // the collision normal vector 406 | time : 0 // the time of the collision, from 0..1 407 | } 408 | ``` 409 | 410 | 411 | ## conventions 412 | 413 | All collision checking functions return a boolean indicating if there was a collision. They also accept an optional `contact` argument, which gets filled in if there is an actual collision. 414 | 415 | 416 | "sweep" tests indicate at least 1 of the objects is moving. the number indicates how many objects are moving. e.g., `aabb-aabb-sweep2` means we are comparing 2 aabbs, both of which are moving. 417 | 418 | "overlap" tests don't take movement into account, and this is a static check to see if the 2 entities overlap. 419 | 420 | plural forms imply a collection. e.g., `segments-segment-ovelap` checks one line segment against a set of line segments. If there is more than one collision, the closest collision is set in the `contact` argument. 421 | 422 | "indexed" tests are the same as their non-indexed forms, except they take in an array of segment indices to use. These are nice in that you can avoid having to build large arrays of line segments every frame, if you have things like dynamic line segments (platforms) or have a spatial culling algorithm that selects line segments to include. 423 | 424 | 425 | ## credits 426 | 427 | Most of these collision checks were adapted from existing open source modules: 428 | 429 | * https://github.com/noonat/intersect 430 | * The diagrams are modified from noonat: https://noonat.github.io/intersect/ 431 | * https://github.com/kevzettler/gl-swept-sphere-triangle 432 | * https://gist.github.com/toji/2802287 433 | * segment-point-overlap from https://gist.github.com/mattdesl/47412d930dcd8cd765c871a65532ffac 434 | * segment-segment overlap from https://github.com/tmpvar/segseg 435 | * http://www.gamasutra.com/view/feature/131790/simple_intersection_tests_for_games.php 436 | * http://geomalgorithms.com/a07-_distance.html#dist3D_Segment_to_Segment 437 | * https://observablehq.com/@kelleyvanevert/2d-point-in-triangle-test 438 | * aabb-segment sweep from https://gamedev.stackexchange.com/questions/29479/swept-aabb-vs-line-segment-2d 439 | * PolylinePath implementation from Craig Reynold's seminal work on autonomous steering https://opensteer.sourceforge.net/ 440 | -------------------------------------------------------------------------------- /build-figures.sh: -------------------------------------------------------------------------------- 1 | # generate animated pngs in docs/ from all of the examples in figures/ 2 | 3 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-segments-sweep1-indexed.js --count 245 --output docs/aabb-segments-sweep1-indexed.png 4 | 5 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-segment-sweep1.js --count 245 --output docs/aabb-segment-sweep1.png 6 | 7 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-aabb-overlap.js --count 3000 --output docs/aabb-aabb-overlap.png 8 | 9 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-aabb-sweep1.js --count 245 --output docs/aabb-aabb-sweep1.png 10 | 11 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-aabb-sweep2.js --count 245 --output docs/aabb-aabb-sweep2.png 12 | 13 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-point-overlap.js --count 1200 --output docs/aabb-point-overlap.png 14 | 15 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/aabb-segment-overlap.js --count 1600 --output docs/aabb-segment-overlap.png 16 | 17 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/ray-plane-distance.js --count 1200 --output docs/ray-plane-distance.png 18 | 19 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/segment-point-overlap.js --count 1200 --output docs/segment-point-overlap.png 20 | 21 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/segment-segment-overlap.js --count 1200 --output docs/segment-segment-overlap.png 22 | 23 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/segments-segment-overlap.js --count 1200 --output docs/segments-segment-overlap.png 24 | 25 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/segments-sphere-sweep1.js --count 122 --output docs/segments-sphere-sweep1.png 26 | 27 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/sphere-sphere-overlap.js --count 245 --output docs/sphere-sphere-overlap.png 28 | 29 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/triangle-point-overlap.js --count 1200 --output docs/triangle-point-overlap.png 30 | 31 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/cone-point-overlap.js --count 1200 --output docs/cone-point-overlap.png 32 | 33 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/ray-sphere-overlap.js --count 1200 --output docs/ray-sphere-overlap.png 34 | 35 | deno run --allow-read --allow-write --allow-run --allow-net --allow-import cli-figure-to-apng.ts --module figures/segment-sphere-overlap.js --count 1200 --output docs/segment-sphere-overlap.png 36 | -------------------------------------------------------------------------------- /canvaskit.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/canvaskit.wasm -------------------------------------------------------------------------------- /cli-figure-to-apng.ts: -------------------------------------------------------------------------------- 1 | import Canvas, { CanvasRenderingContext2D, dataURLtoFile } from 'https://deno.land/x/canvas@v1.4.2/mod.ts' 2 | 3 | 4 | let m, c, o; 5 | 6 | for (let i=0; i < Deno.args.length - 1; i++) { 7 | const a = Deno.args[i] 8 | if (a === '--module' || a === '-m') 9 | m = Deno.args[i+1] 10 | else if (a === '--count') 11 | c = parseInt(Deno.args[i+1], 10) 12 | else if (a === '--output') 13 | o = Deno.args[i+1] 14 | } 15 | 16 | if (!m) { 17 | console.error('module path not found, exiting') 18 | Deno.exit(1) 19 | } 20 | 21 | if (!c) { 22 | console.error('frame count not found, exiting') 23 | Deno.exit(1) 24 | } 25 | 26 | if (!o) { 27 | console.error('output not found, exiting') 28 | Deno.exit(1) 29 | } 30 | 31 | const CANVAS_WIDTH = 400 32 | const CANVAS_HEIGHT = 180 33 | 34 | const canvas = Canvas.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) 35 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D 36 | 37 | ctx.translate(0.5, 0.5) 38 | 39 | 40 | const figure = await import(`./${m}`) 41 | 42 | 43 | const ex = figure.default.init(ctx, CANVAS_WIDTH, CANVAS_HEIGHT) 44 | 45 | //const FPS = 60 46 | 47 | // setting this to 1/ 60 causes the sphere/segment collision test to not work 48 | // probably due to differences in rounding precision between deno and v8? 49 | // hardcoding this to a very close approximation seems to solve the problem 50 | const DT = 0.01666 //1 / FPS 51 | 52 | const padLength = ('' + c).length 53 | 54 | for (let i=0; i < c; i++) { 55 | figure.default.draw(ex, DT) 56 | const data = dataURLtoFile(canvas.toDataURL()) 57 | const padded = ('' + i).padStart(padLength, '0') 58 | Deno.writeFileSync(`out_${padded}.png`, data) 59 | } 60 | 61 | await Deno.run({ 62 | cmd: [ 'apngasm', '-o', o, 'out_*.png', '--delay=16', '--force' ] 63 | }).status() 64 | 65 | 66 | for (let i=0; i < c; i++) { 67 | const padded = ('' + i).padStart(padLength, '0') 68 | Deno.removeSync(`out_${padded}.png`) 69 | } 70 | -------------------------------------------------------------------------------- /collision-2d.js: -------------------------------------------------------------------------------- 1 | export { default as aabbContain } from './src/aabb-aabb-contain.js' 2 | export { default as aabbOverlap } from './src/aabb-aabb-overlap.js' 3 | export { default as aabbSweep1 } from './src/aabb-aabb-sweep1.js' 4 | export { default as aabbSweep2 } from './src/aabb-aabb-sweep2.js' 5 | export { default as aabbPointOverlap } from './src/aabb-point-overlap.js' 6 | export { default as aabbSegOverlap } from './src/aabb-segment-overlap.js' 7 | export { default as aabbSegSweep1 } from './src/aabb-segment-sweep1.js' 8 | export { default as aabbSegsSweep1Indexed } from './src/aabb-segments-sweep1-indexed.js' 9 | export * as AABB from './src/AABB.js' 10 | export { default as conePointOverlap } from './src/cone-point-overlap.js' 11 | export { default as contactCopy } from './src/contact-copy.js' 12 | export { default as contact } from './src/contact.js' 13 | export { default as Plane } from './src/Plane.js' 14 | export { default as pointPolygonOverlap } from './src/point-polygon-overlap.js' 15 | export * as Polygon from './src/Polygon.js' 16 | export * as PolylinePath from './src/PolylinePath.js' 17 | export { default as raySphereOverlap } from './src/ray-sphere-overlap.js' 18 | export { default as segNormal } from './src/segment-normal.js' 19 | export { default as segPointOverlap } from './src/segment-point-overlap.js' 20 | export { default as segOverlap } from './src/segment-segment-overlap.js' 21 | export { default as segSphereOverlap } from './src/segment-sphere-overlap.js' 22 | export { default as segsEllipsoidSweep1Indexed }from './src/segments-ellipsoid-sweep1-indexed.js' 23 | export { default as segsSegOverlapIndexed } from './src/segments-segment-overlap-indexed.js' 24 | export { default as segsSegOverlap } from './src/segments-segment-overlap.js' 25 | export { default as segsSphereSweep1Indexed} from './src/segments-sphere-sweep1-indexed.js' 26 | export { default as segsSphereSweep1 } from './src/segments-sphere-sweep1.js' 27 | export { default as segClosest } from './src/segseg-closest.js' 28 | export { default as spherePointOverlap } from './src/sphere-point-overlap.js' 29 | export { default as sphereOverlap } from './src/sphere-sphere-overlap.js' 30 | export { default as sphereSweep2 } from './src/sphere-sphere-sweep2.js' 31 | export { default as triangleArea } from './src/triangle-area.js' 32 | export { default as triangleGetCenter } from './src/triangle-get-center.js' 33 | export { default as trianglePointOverlap } from './src/triangle-point-overlap.js' 34 | -------------------------------------------------------------------------------- /docs/aabb-aabb-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-aabb-overlap.png -------------------------------------------------------------------------------- /docs/aabb-aabb-sweep1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-aabb-sweep1.png -------------------------------------------------------------------------------- /docs/aabb-aabb-sweep2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-aabb-sweep2.png -------------------------------------------------------------------------------- /docs/aabb-point-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-point-overlap.png -------------------------------------------------------------------------------- /docs/aabb-segment-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-segment-overlap.png -------------------------------------------------------------------------------- /docs/aabb-segment-sweep1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-segment-sweep1.png -------------------------------------------------------------------------------- /docs/aabb-segments-sweep1-indexed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/aabb-segments-sweep1-indexed.png -------------------------------------------------------------------------------- /docs/cone-point-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/cone-point-overlap.png -------------------------------------------------------------------------------- /docs/ray-plane-distance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/ray-plane-distance.png -------------------------------------------------------------------------------- /docs/ray-sphere-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/ray-sphere-overlap.png -------------------------------------------------------------------------------- /docs/segment-point-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/segment-point-overlap.png -------------------------------------------------------------------------------- /docs/segment-segment-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/segment-segment-overlap.png -------------------------------------------------------------------------------- /docs/segment-sphere-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/segment-sphere-overlap.png -------------------------------------------------------------------------------- /docs/segments-segment-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/segments-segment-overlap.png -------------------------------------------------------------------------------- /docs/segments-sphere-sweep1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/segments-sphere-sweep1.png -------------------------------------------------------------------------------- /docs/sphere-sphere-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/sphere-sphere-overlap.png -------------------------------------------------------------------------------- /docs/triangle-point-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mreinstein/collision-2d/0e80f58cf06bc80c6df665564d900a18e3e4435d/docs/triangle-point-overlap.png -------------------------------------------------------------------------------- /figures.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | collision-2d examples 5 | 6 | 28 | 29 | 30 | 31 | 32 | 43 | 44 | 45 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /figures/aabb-aabb-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import contact from '../src/contact.js' 3 | import aabbAabbOverlap from '../src/aabb-aabb-overlap.js' 4 | 5 | 6 | function init (context, width, height) { 7 | return { 8 | angle: 0, 9 | box1: { 10 | position: [ 0, 0 ], 11 | width: 128, 12 | height: 32 13 | }, 14 | 15 | box2: { 16 | position: [ 0, 0 ], 17 | width: 32, 18 | height: 32 19 | }, 20 | ...common.init(context, width, height) 21 | } 22 | } 23 | 24 | 25 | function draw (data, dt) { 26 | common.clear(data) 27 | 28 | data.angle += 0.2 * Math.PI * dt 29 | data.box2.position[0] = Math.cos(data.angle) * 96 30 | data.box2.position[1] = Math.sin(data.angle * 2.4) * 24 31 | 32 | 33 | const c = contact() 34 | const hit = aabbAabbOverlap(data.box1, data.box2, c) 35 | 36 | common.drawAABB(data, data.box1, '#666') 37 | 38 | if (hit) { 39 | common.drawAABB(data, data.box2, '#f00') 40 | data.box2.position[0] += c.delta[0] 41 | data.box2.position[1] += c.delta[1] 42 | common.drawAABB(data, data.box2, '#ff0') 43 | common.drawPoint(data, c.position, '#ff0') 44 | common.drawRay(data, c.position, c.normal, 4, '#ff0', false) 45 | } else { 46 | common.drawAABB(data, data.box2, '#0f0') 47 | } 48 | } 49 | 50 | 51 | export default { init, draw } 52 | -------------------------------------------------------------------------------- /figures/aabb-aabb-sweep1.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import contact from '../src/contact.js' 3 | import aabbAabbSweep1 from '../src/aabb-aabb-sweep1.js' 4 | import { vec2 } from 'gl-matrix' 5 | 6 | 7 | function init (context, width, height) { 8 | 9 | return { 10 | angle: 0, 11 | staticBox: { 12 | position: [ 0, 0 ], 13 | width: 224, 14 | height: 32 15 | }, 16 | 17 | sweepBoxes: [ 18 | { 19 | position: [ -152, 24 ], 20 | width: 32, 21 | height: 32 22 | }, 23 | { 24 | position: [ 128, -48 ], 25 | width: 32, 26 | height: 32 27 | } 28 | ], 29 | 30 | sweepDeltas: [ 31 | [ 64, -12 ], 32 | [ -32, 96 ] 33 | ], 34 | 35 | tempBox: { 36 | position: [ 0, 0 ], 37 | width: 32, 38 | height: 32 39 | }, 40 | ...common.init(context, width, height) 41 | } 42 | } 43 | 44 | 45 | function draw (data, dt) { 46 | common.clear(data) 47 | 48 | data.angle += 0.5 * Math.PI * dt 49 | 50 | common.drawAABB(data, data.staticBox, '#666') 51 | const factor = (Math.cos(data.angle) + 1) * 0.5 || 1e-8 52 | 53 | common.drawAABB(data, data.staticBox, '#666') 54 | 55 | data.sweepBoxes.forEach((box, i) => { 56 | const delta = vec2.scale([], data.sweepDeltas[i], factor) 57 | const c = contact() 58 | const sweep = aabbAabbSweep1(data.staticBox, box, delta, c) 59 | const length = vec2.length(delta) 60 | const dir = vec2.normalize([], delta) 61 | 62 | common.drawAABB(data, box, '#666') 63 | 64 | if (sweep) { 65 | // Draw a red box at the point where it was trying to move to 66 | common.drawRay(data, box.position, dir, length, '#f00') 67 | data.tempBox.position[0] = box.position[0] + delta[0] 68 | data.tempBox.position[1] = box.position[1] + delta[1] 69 | common.drawAABB(data, data.tempBox, '#f00') 70 | 71 | // Draw a yellow box at the point it actually got to 72 | data.tempBox.position[0] = box.position[0] + delta[0] * c.time 73 | data.tempBox.position[1] = box.position[1] + delta[1] * c.time 74 | common.drawAABB(data, data.tempBox, '#ff0') 75 | common.drawPoint(data, c.position, '#ff0') 76 | common.drawRay(data, c.position, c.normal, 4, '#ff0', false) 77 | 78 | } else { 79 | data.tempBox.position[0] = box.position[0] + delta[0] 80 | data.tempBox.position[1] = box.position[1] + delta[1] 81 | common.drawAABB(data, data.tempBox, '#0f0') 82 | common.drawRay(data, box.position, dir, length, '#0f0') 83 | } 84 | 85 | }) 86 | 87 | } 88 | 89 | 90 | export default { init, draw } 91 | -------------------------------------------------------------------------------- /figures/aabb-aabb-sweep2.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import contact from '../src/contact.js' 3 | import aabbAabbSweep2 from '../src/aabb-aabb-sweep2.js' 4 | import { vec2 } from 'gl-matrix' 5 | 6 | 7 | function init (context, width, height) { 8 | 9 | return { 10 | angle: 0, 11 | sweepBoxes: [ 12 | { 13 | position: [ -60, 24 ], 14 | width: 32, 15 | height: 32 16 | }, 17 | { 18 | position: [ 60, -48 ], 19 | width: 32, 20 | height: 32 21 | } 22 | ], 23 | 24 | sweepDeltas: [ 25 | [ 64, -20 ], 26 | [ -64,50 ] 27 | ], 28 | 29 | tempBox: { 30 | position: [ 0, 0 ], 31 | width: 32, 32 | height: 32 33 | }, 34 | ...common.init(context, width, height) 35 | } 36 | } 37 | 38 | 39 | function draw (data, dt) { 40 | common.clear(data) 41 | 42 | data.angle += 0.5 * Math.PI * dt 43 | 44 | const factor = (Math.cos(data.angle) + 1) * 0.5 || 1e-8 45 | 46 | const vA = vec2.scale([], data.sweepDeltas[0], factor) 47 | const vB = vec2.scale([], data.sweepDeltas[1], factor) 48 | 49 | const [ box1, box2 ] = data.sweepBoxes 50 | 51 | const c = contact() 52 | 53 | const sweep = aabbAabbSweep2(data.sweepBoxes[0], vA, data.sweepBoxes[1], vB, c) 54 | 55 | if (sweep) { 56 | // Draw a red box at the point where it was trying to move to 57 | let length = vec2.length(vA) 58 | let dir = vec2.normalize([], vA) 59 | common.drawRay(data, box1.position, dir, length, '#f00') 60 | data.tempBox.position[0] = box1.position[0] + vA[0] 61 | data.tempBox.position[1] = box1.position[1] + vA[1] 62 | common.drawAABB(data, data.tempBox, '#f00') 63 | 64 | length = vec2.length(vB) 65 | dir = vec2.normalize([], vB) 66 | common.drawRay(data, box2.position, dir, length, '#f00') 67 | data.tempBox.position[0] = box2.position[0] + vB[0] 68 | data.tempBox.position[1] = box2.position[1] + vB[1] 69 | common.drawAABB(data, data.tempBox, '#f00') 70 | 71 | 72 | // Draw a yellow box at the point it actually got to 73 | data.tempBox.position[0] = box1.position[0] + vA[0] * c.time 74 | data.tempBox.position[1] = box1.position[1] + vA[1] * c.time 75 | common.drawAABB(data, data.tempBox, '#ff0') 76 | 77 | data.tempBox.position[0] = box2.position[0] + vB[0] * c.time 78 | data.tempBox.position[1] = box2.position[1] + vB[1] * c.time 79 | common.drawAABB(data, data.tempBox, '#ff0') 80 | 81 | // draw a ray at the contact location 82 | common.drawRay(data, c.position, c.normal, 4, '#ff0', false) 83 | 84 | } else { 85 | 86 | const dir = [ 0, 0 ] 87 | let length 88 | 89 | length = vec2.length(vA) 90 | vec2.normalize(dir, vA) 91 | 92 | vec2.add(data.tempBox.position, box1.position, vA) 93 | common.drawAABB(data, data.tempBox, '#0f0') 94 | common.drawRay(data, box1.position, dir, length, '#0f0') 95 | 96 | 97 | length = vec2.length(vB) 98 | vec2.normalize(dir, vB) 99 | 100 | vec2.add(data.tempBox.position, box2.position, vB) 101 | common.drawAABB(data, data.tempBox, '#0f0') 102 | common.drawRay(data, box2.position, dir, length, '#0f0') 103 | } 104 | 105 | } 106 | 107 | 108 | export default { init, draw } 109 | -------------------------------------------------------------------------------- /figures/aabb-point-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import contact from '../src/contact.js' 3 | import aabbPointOverlap from '../src/aabb-point-overlap.js' 4 | 5 | 6 | function init (context, width, height) { 7 | return { 8 | angle: 0, 9 | pos: [ 0, 0 ], 10 | box: { 11 | position: [ 0, 0 ], 12 | width: 32, 13 | height: 32 14 | }, 15 | ...common.init(context, width, height) 16 | } 17 | } 18 | 19 | 20 | function draw (data, dt) { 21 | common.clear(data) 22 | 23 | data.angle += 0.5 * Math.PI * dt 24 | data.pos[0] = Math.cos(data.angle * 0.4) * 32 25 | data.pos[1] = Math.sin(data.angle) * 12 26 | 27 | const c = contact() 28 | const hit = aabbPointOverlap(data.box, data.pos, c) 29 | common.drawAABB(data, data.box, '#666') 30 | 31 | const pointWidth = 1 32 | 33 | if (hit) { 34 | common.drawPoint(data, data.pos, '#f00', '', pointWidth) 35 | common.drawPoint(data, c.position, '#ff0', '', pointWidth) 36 | } else { 37 | common.drawPoint(data, data.pos, '#0f0', '', pointWidth) 38 | } 39 | } 40 | 41 | 42 | export default { init, draw } 43 | -------------------------------------------------------------------------------- /figures/aabb-segment-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import contact from '../src/contact.js' 3 | import intersectSegment from '../src/aabb-segment-overlap.js' 4 | import { vec2 } from 'gl-matrix' 5 | 6 | 7 | function init (context, width, height) { 8 | return { 9 | angle: 0, 10 | box: { 11 | position: [ 0, 0 ], 12 | width: 32, 13 | height: 32 14 | }, 15 | ...common.init(context, width, height) 16 | } 17 | } 18 | 19 | 20 | function draw (data, dt) { 21 | common.clear(data) 22 | 23 | data.angle += 0.5 * Math.PI * dt 24 | 25 | const pos1 = [ 26 | Math.cos(data.angle) * 64, 27 | Math.sin(data.angle) * 64 28 | ] 29 | 30 | const pos2 = [ 31 | Math.sin(data.angle) * 32, 32 | Math.cos(data.angle) * 32 33 | ] 34 | 35 | const delta = [ pos2[0] - pos1[0], pos2[1] - pos1[1] ] 36 | 37 | const c = contact() 38 | const paddingX = 0 39 | const paddingY = 0 40 | const hit = intersectSegment(data.box, pos1, delta, paddingX, paddingY, c) 41 | 42 | const length = vec2.length(delta) 43 | const dir = vec2.normalize([], delta) //[ delta[0], delta[1] ] 44 | 45 | common.drawAABB(data, data.box, '#666') 46 | 47 | if (hit) { 48 | common.drawRay(data, pos1, dir, length, '#f00') 49 | common.drawSegment(data, pos1, c.position, '#ff0') 50 | common.drawPoint(data, c.position, '#ff0') 51 | common.drawRay(data, c.position, c.normal, 6, '#ff0', false) 52 | } else { 53 | common.drawRay(data, pos1, dir, length, '#0f0') 54 | } 55 | } 56 | 57 | 58 | export default { init, draw } 59 | -------------------------------------------------------------------------------- /figures/aabb-segment-sweep1.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import contact from '../src/contact.js' 3 | import aabbSegmentSweep from '../src/aabb-segment-sweep1.js' 4 | import { vec2 } from 'gl-matrix' 5 | 6 | 7 | function init (context, width, height) { 8 | 9 | return { 10 | angle: 0, 11 | 12 | staticLine: [ 13 | 14 | [ 50, -50 ], 15 | [ -20, 0 ] 16 | ], 17 | 18 | sweepAABB: { 19 | position: [ -32, 60 ], 20 | width: 32, 21 | height: 32 22 | }, 23 | 24 | sweepDelta: [ 25 | 32, -96 26 | ], 27 | 28 | tempBox: { 29 | position: [ 0, 0 ], 30 | width: 32, 31 | height: 32 32 | }, 33 | ...common.init(context, width, height) 34 | } 35 | } 36 | 37 | 38 | function draw (data, dt) { 39 | common.clear(data) 40 | 41 | common.drawSegment(data, data.staticLine[0], data.staticLine[1], '#666') 42 | 43 | 44 | data.angle += 0.5 * Math.PI * dt 45 | 46 | const factor = (Math.cos(data.angle) + 1) * 0.5 || 1e-8 47 | 48 | const delta = vec2.scale([], data.sweepDelta, factor) 49 | const c = contact() 50 | 51 | const box = data.sweepAABB 52 | 53 | 54 | const sweep = aabbSegmentSweep(data.staticLine, box, delta, c) 55 | const length = vec2.length(delta) 56 | const dir = vec2.normalize([], delta) 57 | 58 | 59 | if (sweep) { 60 | // Draw a red box at the point where it was trying to move to 61 | common.drawRay(data, box.position, dir, length, '#f00') 62 | data.tempBox.position[0] = box.position[0] + delta[0] 63 | data.tempBox.position[1] = box.position[1] + delta[1] 64 | common.drawAABB(data, data.tempBox, '#f00') 65 | 66 | // Draw a yellow box at the point it actually got to 67 | data.tempBox.position[0] = box.position[0] + delta[0] * c.time 68 | data.tempBox.position[1] = box.position[1] + delta[1] * c.time 69 | common.drawAABB(data, data.tempBox, '#ff0') 70 | common.drawPoint(data, c.position, '#ff0') 71 | common.drawRay(data, c.position, c.normal, 4, '#ff0', false) 72 | 73 | } else { 74 | data.tempBox.position[0] = box.position[0] + delta[0] 75 | data.tempBox.position[1] = box.position[1] + delta[1] 76 | common.drawAABB(data, data.tempBox, '#0f0') 77 | common.drawRay(data, box.position, dir, length, '#0f0') 78 | } 79 | 80 | } 81 | 82 | 83 | export default { init, draw } 84 | -------------------------------------------------------------------------------- /figures/aabb-segments-sweep1-indexed.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import contact from '../src/contact.js' 3 | import aabbSegmentsSweep from '../src/aabb-segments-sweep1-indexed.js' 4 | import { vec2 } from 'gl-matrix' 5 | 6 | 7 | function init (context, width, height) { 8 | 9 | return { 10 | angle: 0, 11 | 12 | staticLines: [ 13 | [ [96,28], [-22,-15] ], 14 | [ [100, 11], [-30,-40] ], 15 | [ [100, -15], [-30,-60] ] 16 | ], 17 | 18 | indices: [ 0, 1, 2 ], 19 | lineCount: 3, 20 | 21 | sweepAABB: { 22 | position: [ -32, 60 ], 23 | width: 32, 24 | height: 32 25 | }, 26 | 27 | sweepDelta: [ 28 | 32, -96 29 | ], 30 | 31 | tempBox: { 32 | position: [ 0, 0 ], 33 | width: 32, 34 | height: 32 35 | }, 36 | ...common.init(context, width, height) 37 | } 38 | } 39 | 40 | 41 | function draw (data, dt) { 42 | common.clear(data) 43 | 44 | for (const line of data.staticLines) { 45 | common.drawSegment(data, line[0], line[1], '#666') 46 | } 47 | 48 | data.angle += 0.5 * Math.PI * dt 49 | 50 | const factor = (Math.cos(data.angle) + 1) * 0.5 || 1e-8 51 | 52 | const delta = vec2.scale([], data.sweepDelta, factor) 53 | const c = contact() 54 | 55 | const box = data.sweepAABB 56 | 57 | 58 | const sweep = aabbSegmentsSweep(data.staticLines, data.indices, data.lineCount, box, delta, c) 59 | const length = vec2.length(delta) 60 | const dir = vec2.normalize([], delta) 61 | 62 | if (sweep) { 63 | // Draw a red box at the point where it was trying to move to 64 | common.drawRay(data, box.position, dir, length, '#f00') 65 | data.tempBox.position[0] = box.position[0] + delta[0] 66 | data.tempBox.position[1] = box.position[1] + delta[1] 67 | common.drawAABB(data, data.tempBox, '#f00') 68 | 69 | // Draw a yellow box at the point it actually got to 70 | data.tempBox.position[0] = box.position[0] + delta[0] * c.time 71 | data.tempBox.position[1] = box.position[1] + delta[1] * c.time 72 | common.drawAABB(data, data.tempBox, '#ff0') 73 | common.drawPoint(data, c.position, '#ff0') 74 | common.drawRay(data, c.position, c.normal, 4, '#ff0', false) 75 | 76 | } else { 77 | data.tempBox.position[0] = box.position[0] + delta[0] 78 | data.tempBox.position[1] = box.position[1] + delta[1] 79 | common.drawAABB(data, data.tempBox, '#0f0') 80 | common.drawRay(data, box.position, dir, length, '#0f0') 81 | } 82 | 83 | } 84 | 85 | 86 | export default { init, draw } 87 | -------------------------------------------------------------------------------- /figures/common.js: -------------------------------------------------------------------------------- 1 | import { toRadians } from '@footgun/math-gap' 2 | 3 | 4 | function drawAABB (data, box, color='#fff', thickness=1) { 5 | const { origin, context } = data 6 | 7 | const x1 = Math.floor(origin[0] + box.position[0] - box.width/2) 8 | const y1 = Math.floor(origin[1] + box.position[1] - box.height/2) 9 | const x2 = Math.floor(origin[0] + box.position[0] + box.width/2) 10 | const y2 = Math.floor(origin[1] + box.position[1] + box.height/2) 11 | context.beginPath() 12 | context.moveTo(x1, y1) 13 | context.lineTo(x2, y1) 14 | context.lineTo(x2, y2) 15 | context.lineTo(x1, y2) 16 | context.lineTo(x1, y1) 17 | context.closePath() 18 | context.lineWidth = thickness 19 | context.strokeStyle = color 20 | context.stroke() 21 | } 22 | 23 | 24 | function drawCircle (data, center, radius, color='#fff', thickness=1) { 25 | const { origin, context } = data 26 | 27 | const x = Math.floor(origin[0] + center[0]) 28 | const y = Math.floor(origin[1] + center[1]) 29 | context.beginPath() 30 | context.arc(x, y, radius, 0, 2 * Math.PI, true) 31 | context.closePath() 32 | context.lineWidth = thickness 33 | context.strokeStyle = color 34 | context.stroke() 35 | } 36 | 37 | 38 | function drawCone (data, conePosition, coneRotation, coneFieldOfView, minDistance, maxDistance) { 39 | const { origin, context } = data 40 | 41 | const x = Math.floor(origin[0] + conePosition[0]) 42 | const y = Math.floor(origin[1] + conePosition[1]) 43 | 44 | //console.log('max:', maxDistance, 'min:', minDistance) 45 | //console.log('fov:', coneFieldOfView, ' rot:', coneRotation) 46 | context.fillStyle = '#333' 47 | context.beginPath() 48 | context.moveTo(origin[0], origin[1]) 49 | context.arc(x, y, maxDistance, coneRotation-toRadians(coneFieldOfView/2), coneRotation+toRadians(coneFieldOfView/2), false) 50 | context.fill() 51 | 52 | if (minDistance > 0) { 53 | context.fillStyle = '#202020' 54 | context.beginPath() 55 | context.arc(x, y, minDistance, 0, Math.PI*2, false) 56 | context.fill() 57 | } 58 | } 59 | 60 | 61 | function drawPoint (data, point, color='#fff', text='', thickness=1) { 62 | const { origin, context } = data 63 | 64 | const x = Math.floor(origin[0] + point[0] - thickness / 2) 65 | const y = Math.floor(origin[1] + point[1] - thickness / 2) 66 | context.lineWidth = thickness 67 | context.fillStyle = color 68 | context.strokeStyle = color 69 | context.fillRect(x, y, thickness, thickness) 70 | context.strokeRect(x, y, thickness, thickness) 71 | if (text) 72 | context.fillText(text, x + thickness * 4, y + thickness * 2) 73 | } 74 | 75 | 76 | function drawRay (data, pos, dir, length, color='#fff', arrow=true, thickness=1) { 77 | const { context } = data 78 | 79 | const pos2 = [ 80 | pos[0] + dir[0] * length, 81 | pos[1] + dir[1] * length 82 | ] 83 | 84 | drawSegment(data, pos, pos2, color, thickness) 85 | 86 | if (arrow) { 87 | pos = [ pos2[0], pos2[1] ] 88 | pos2[0] = pos[0] - dir[0] * 4 + dir[1] * 4 89 | pos2[1] = pos[1] - dir[1] * 4 - dir[0] * 4 90 | drawSegment(data, pos, pos2, color, thickness) 91 | pos2[0] = pos[0] - dir[0] * 4 - dir[1] * 4 92 | pos2[1] = pos[1] - dir[1] * 4 + dir[0] * 4 93 | drawSegment(data, pos, pos2, color, thickness) 94 | } 95 | } 96 | 97 | 98 | function drawSegment (data, point1, point2, color='#fff', thickness=1, dashed=false) { 99 | const { origin, context } = data 100 | 101 | if (dashed) 102 | context.setLineDash([4, 6]) 103 | 104 | const x1 = Math.floor(origin[0] + point1[0]) 105 | const y1 = Math.floor(origin[1] + point1[1]) 106 | const x2 = Math.floor(origin[0] + point2[0]) 107 | const y2 = Math.floor(origin[1] + point2[1]) 108 | context.beginPath() 109 | context.moveTo(x1, y1) 110 | context.lineTo(x2, y2) 111 | 112 | context.closePath() 113 | context.lineWidth = thickness 114 | context.strokeStyle = color 115 | context.stroke() 116 | 117 | if (dashed) 118 | context.setLineDash([]) 119 | } 120 | 121 | 122 | function drawTriangle (data, point0, point1, point2, color='#fff', thickness=1, dashed=false) { 123 | drawSegment(data, point0, point1, color, thickness, dashed) 124 | drawSegment(data, point1, point2, color, thickness, dashed) 125 | drawSegment(data, point2, point0, color, thickness, dashed) 126 | } 127 | 128 | 129 | function init (context, width, height) { 130 | return { 131 | context, 132 | width, 133 | height, 134 | origin: [ width * 0.5, height * 0.5 ], 135 | infiniteLength: Math.sqrt(width * width + height * height) 136 | } 137 | } 138 | 139 | 140 | function clear (data) { 141 | const { context, width, height } = data 142 | context.fillStyle = '#202020' 143 | context.fillRect(0, 0, width, height) 144 | } 145 | 146 | 147 | export default { drawAABB, drawCircle, drawCone, drawPoint, drawRay, drawSegment, drawTriangle, init, clear } 148 | -------------------------------------------------------------------------------- /figures/cone-point-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import conePointOverlap from '../src/cone-point-overlap.js' 3 | 4 | 5 | function init (context, width, height) { 6 | return { 7 | angle: 0, 8 | pos: [ 0, 0 ], 9 | tri: [ 10 | [ 0, -20 ], 11 | [ -20, 20 ], 12 | [ 20, 20 ] 13 | ], 14 | cone: { 15 | position: [ 0, 0 ], 16 | rotation: 0, 17 | fieldOfView: 80, 18 | minDistance: 10, 19 | maxDistance: 40 20 | }, 21 | ...common.init(context, width, height) 22 | } 23 | } 24 | 25 | 26 | function draw (data, dt) { 27 | common.clear(data) 28 | 29 | data.angle += 0.5 * Math.PI * dt 30 | data.pos[0] = 30 + Math.cos(data.angle * 0.4) * 32 31 | data.pos[1] = Math.sin(data.angle) * 12 32 | 33 | common.drawCone(data, data.cone.position, data.cone.rotation, data.cone.fieldOfView, data.cone.minDistance, data.cone.maxDistance) 34 | 35 | const pointWidth = 1 36 | 37 | /* 38 | const overlapping = triPointOverlap(data.tri[0], data.tri[1], data.tri[2], data.pos) 39 | 40 | common.drawTriangle(data, data.tri[0], data.tri[1], data.tri[2], '#666') 41 | 42 | if (overlapping) 43 | common.drawPoint(data, data.pos, '#f00', '', pointWidth) 44 | else 45 | common.drawPoint(data, data.pos, '#0f0', '', pointWidth) 46 | */ 47 | 48 | const pc = conePointOverlap(data.cone.position, data.cone.rotation, data.cone.fieldOfView, data.cone.minDistance, data.cone.maxDistance, data.pos) ? '#0f0' : '#f00' 49 | 50 | common.drawPoint(data, data.pos, pc, '', pointWidth) 51 | 52 | //data.cone.rotation += 0.02 53 | } 54 | 55 | 56 | export default { init, draw } 57 | -------------------------------------------------------------------------------- /figures/ray-plane-distance.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import plane from '../src/plane.js' 3 | import { vec2 } from 'gl-matrix' 4 | import segmentNormal from '../src/segment-normal.js' 5 | import setLength from 'https://cdn.jsdelivr.net/gh/mreinstein/vec2-gap/set-length.js' 6 | 7 | 8 | function init (context, width, height) { 9 | const line = [ 10 | [ -200, 200 ], 11 | [ 200, -200 ] 12 | ] 13 | 14 | const planeNormal = segmentNormal([], line[0], line[1]) 15 | const planeOrigin = line[0] 16 | 17 | return { 18 | angle: 2.6, 19 | line, 20 | planeNormal, 21 | planeOrigin, 22 | ...common.init(context, width, height) 23 | } 24 | } 25 | 26 | 27 | 28 | function draw (data, dt) { 29 | common.clear(data) 30 | 31 | //window.d = data 32 | data.angle += 0.5 * Math.PI * dt 33 | //data.angle = 6.8 34 | 35 | const pos1 = [ 36 | Math.cos(data.angle) * 64, 37 | Math.sin(data.angle) * 64 38 | ] 39 | 40 | const pos2 = [ 41 | Math.sin(data.angle) * 32, 42 | Math.cos(data.angle) * 32 43 | ] 44 | 45 | const delta = vec2.subtract([], pos2, pos1) 46 | 47 | common.drawSegment(data, data.line[0], data.line[1], '#666', 1, true) 48 | 49 | const dir = vec2.normalize([], delta) 50 | 51 | const p = plane.create() 52 | plane.fromPlane(p, data.planeOrigin, data.planeNormal) 53 | 54 | const distance = plane.rayDistance(p, pos1, dir) 55 | 56 | 57 | //const len = vec2.length(delta) 58 | //common.drawRay(data, pos1, dir, len, '#0f0') 59 | 60 | //const rr = setLength([], dir, distance) 61 | common.drawRay(data, pos1, dir, distance, '#ff0') 62 | 63 | /* 64 | if (segmentSegmentOverlap(pos1, pos2, data.line[0], data.line[1], intersection)) { 65 | common.drawPoint(data, intersection, '#ff0', '', 2) 66 | common.drawSegment(data, pos1, intersection, '#ff0') 67 | common.drawSegment(data, pos2, intersection, '#f00') 68 | } 69 | else { 70 | common.drawSegment(data, pos1, pos2, '#0f0') 71 | } 72 | */ 73 | 74 | } 75 | 76 | 77 | export default { init, draw } 78 | -------------------------------------------------------------------------------- /figures/ray-sphere-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import raySphereOverlap from '../src/ray-sphere-overlap.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | function init (context, width, height) { 7 | return { 8 | angle: 0, 9 | box: { 10 | position: [ 0, 0 ], 11 | width: 32, 12 | height: 32 13 | }, 14 | ...common.init(context, width, height) 15 | } 16 | } 17 | 18 | 19 | function draw (data, dt) { 20 | common.clear(data) 21 | 22 | data.angle += 0.3 * Math.PI * dt 23 | 24 | const pos1 = [ 25 | Math.cos(data.angle) * 64, 26 | Math.sin(data.angle) * 64 27 | ] 28 | 29 | const pos2 = [ 30 | Math.sin(data.angle) * 32, 31 | Math.cos(data.angle) * 32 32 | ] 33 | 34 | 35 | const delta = vec2.subtract(vec2.create(), pos2, pos1) 36 | //const delta = [ pos2[0] - pos1[0], pos2[1] - pos1[1] ] 37 | 38 | const sphereCenter = [ 0, 0 ] 39 | const sphereRadius = 22 40 | const contact = { mu1: NaN, mu2: NaN } 41 | 42 | const hit = raySphereOverlap(pos1, pos2, sphereCenter, sphereRadius, contact) 43 | 44 | 45 | common.drawCircle(data, sphereCenter, sphereRadius, '#666') 46 | 47 | 48 | const normal = vec2.normalize(vec2.create(), delta) 49 | const startP = vec2.scaleAndAdd(vec2.create(), pos1, normal, -200) 50 | const endP = vec2.scaleAndAdd(vec2.create(), pos1, normal, 200) 51 | 52 | if (hit) { 53 | common.drawSegment(data, startP, endP, '#f00', 1, true) 54 | common.drawSegment(data, pos1, pos2, '#f00', 1) 55 | 56 | const p1 = vec2.scaleAndAdd(vec2.create(), pos1, delta, contact.mu1) 57 | const p2 = vec2.scaleAndAdd(vec2.create(), pos1, delta, contact.mu2) 58 | 59 | common.drawPoint(data, p1, 'yellow', 'μ1', 2) 60 | common.drawPoint(data, p2, 'yellow', 'μ2', 2) 61 | 62 | } else { 63 | common.drawSegment(data, startP, endP, '#0f0', 1, true) 64 | common.drawSegment(data, pos1, pos2, '#0f0', 1) 65 | } 66 | 67 | } 68 | 69 | 70 | export default { init, draw } 71 | -------------------------------------------------------------------------------- /figures/segment-point-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import contact from '../src/contact.js' 3 | import segmentPointOverlap from '../src/segment-point-overlap.js' 4 | import { vec2 } from 'gl-matrix' 5 | 6 | 7 | function init (context, width, height) { 8 | return { 9 | angle: 0, 10 | line: [ 11 | [ 50, -50], 12 | [ -50, 50 ] 13 | ], 14 | point: [ 0, 0 ], 15 | ...common.init(context, width, height) 16 | } 17 | } 18 | 19 | 20 | function draw (data, dt) { 21 | common.clear(data) 22 | 23 | data.angle += 0.5 * Math.PI * dt 24 | vec2.set(data.point, Math.cos(data.angle * 0.4) * 32, Math.sin(data.angle) * 12) 25 | 26 | common.drawSegment(data, data.line[0], data.line[1], '#666') 27 | 28 | const overlaps = segmentPointOverlap(data.point, data.line[0], data.line[1]) 29 | 30 | common.drawPoint(data, data.point, overlaps ? '#ff0' : '#0f0', '', 2) 31 | } 32 | 33 | 34 | export default { init, draw } 35 | -------------------------------------------------------------------------------- /figures/segment-segment-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import segmentSegmentOverlap from '../src/segment-segment-overlap.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | function init (context, width, height) { 7 | return { 8 | angle: 2.6, 9 | line: [ 10 | [ 50, -50 ], 11 | [ -50, 50 ] 12 | ], 13 | ...common.init(context, width, height) 14 | } 15 | } 16 | 17 | 18 | 19 | function draw (data, dt) { 20 | common.clear(data) 21 | 22 | data.angle += 0.5 * Math.PI * dt 23 | 24 | const pos1 = [ 25 | Math.cos(data.angle) * 64, 26 | Math.sin(data.angle) * 64 27 | ] 28 | 29 | const pos2 = [ 30 | Math.sin(data.angle) * 32, 31 | Math.cos(data.angle) * 32 32 | ] 33 | 34 | common.drawSegment(data, data.line[0], data.line[1], '#666') 35 | 36 | 37 | const intersection = [ 0, 0 ] 38 | if (segmentSegmentOverlap(pos1, pos2, data.line[0], data.line[1], intersection)) { 39 | common.drawPoint(data, intersection, '#ff0', '', 2) 40 | common.drawSegment(data, pos1, intersection, '#ff0') 41 | common.drawSegment(data, pos2, intersection, '#f00') 42 | } 43 | else { 44 | common.drawSegment(data, pos1, pos2, '#0f0') 45 | } 46 | 47 | } 48 | 49 | 50 | export default { init, draw } 51 | -------------------------------------------------------------------------------- /figures/segment-sphere-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import segmentSphereOverlap from '../src/segment-sphere-overlap.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | function init (context, width, height) { 7 | return { 8 | angle: 0, 9 | ...common.init(context, width, height) 10 | } 11 | } 12 | 13 | 14 | function draw (data, dt) { 15 | common.clear(data) 16 | 17 | data.angle += 0.3 * Math.PI * dt 18 | 19 | const pos1 = [ 20 | Math.cos(data.angle) * 124, 21 | Math.sin(data.angle) * 24 22 | ] 23 | 24 | const pos2 = [ 25 | Math.sin(data.angle) * 32, 26 | Math.cos(data.angle) * 32 27 | ] 28 | 29 | 30 | const delta = vec2.subtract(vec2.create(), pos2, pos1) 31 | 32 | const sphereCenter = [ 0, 0 ] 33 | const sphereRadius = 31 34 | const contact = { mu1: NaN, mu2: NaN } 35 | 36 | const hit = segmentSphereOverlap(pos1, pos2, sphereCenter, sphereRadius, contact) 37 | 38 | 39 | common.drawCircle(data, sphereCenter, sphereRadius, '#666') 40 | 41 | if (hit) { 42 | common.drawSegment(data, pos1, pos2, '#f00', 1) 43 | 44 | const p1 = vec2.scaleAndAdd(vec2.create(), pos1, delta, contact.mu1) 45 | const p2 = vec2.scaleAndAdd(vec2.create(), pos1, delta, contact.mu2) 46 | 47 | common.drawPoint(data, p1, 'yellow', 'μ1', 2) 48 | common.drawPoint(data, p2, 'yellow', 'μ2', 2) 49 | 50 | } else { 51 | common.drawSegment(data, pos1, pos2, '#0f0', 1) 52 | } 53 | 54 | } 55 | 56 | 57 | export default { init, draw } 58 | -------------------------------------------------------------------------------- /figures/segments-segment-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import contact from '../src/contact.js' 3 | import segmentsSegmentOverlap from '../src/segments-segment-overlap.js' 4 | import { vec2 } from 'gl-matrix' 5 | 6 | 7 | function init (context, width, height) { 8 | 9 | const lines = [[[4,23],[6,31]],[[-22,-15],[96,28]],[[-67,76],[5,-77]],[[-26,-23],[-69,-37]],[[-11,59],[12,38]]] 10 | 11 | return { 12 | angle: 2.6, 13 | lines, 14 | ...common.init(context, width, height) 15 | } 16 | } 17 | 18 | 19 | function draw (data, dt) { 20 | common.clear(data) 21 | 22 | data.angle += 0.4 * Math.PI * dt 23 | 24 | 25 | const pos1 = [ 26 | Math.cos(data.angle) * 64, 27 | Math.sin(data.angle) * 64 28 | ] 29 | 30 | const pos2 = [ 31 | Math.sin(data.angle) * 32, 32 | Math.cos(data.angle) * 32 33 | ] 34 | 35 | const delta = vec2.subtract([], pos2, pos1) 36 | let len = vec2.length(delta) 37 | const dir = vec2.normalize([], delta) 38 | 39 | 40 | for (const line of data.lines) 41 | common.drawSegment(data, line[0], line[1], '#666') 42 | 43 | const c = contact() 44 | 45 | if (segmentsSegmentOverlap(data.lines, pos1, delta, c)) { 46 | vec2.subtract(dir, c.position, pos1) 47 | len = vec2.length(dir) 48 | vec2.normalize(dir, dir) 49 | common.drawRay(data, pos1, dir, len, '#ff0') 50 | 51 | vec2.subtract(dir, pos2, c.position) 52 | len = vec2.length(dir) 53 | vec2.normalize(dir, dir) 54 | common.drawRay(data, c.position, dir, len, '#f00') 55 | 56 | common.drawPoint(data, c.position, '#ff0', '', 2) 57 | } else { 58 | common.drawRay(data, pos1, dir, len, '#0f0') 59 | } 60 | 61 | } 62 | 63 | 64 | export default { init, draw } 65 | -------------------------------------------------------------------------------- /figures/segments-sphere-sweep1.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import contact from '../src/contact.js' 3 | import segmentsSphereSweep1 from '../src/segments-sphere-sweep1.js' 4 | import { vec2 } from 'gl-matrix' 5 | import randomInt from 'https://cdn.jsdelivr.net/gh/mreinstein/random-gap/int.js' 6 | 7 | 8 | function init (context, width, height) { 9 | 10 | const lines = [ 11 | [ [-50, -50], [ -50, 50] ], 12 | [ [ 50, 50], [ 50, -50] ], 13 | //[ [ 50, -50 ], [ -50, -50 ] ], 14 | //[ [ -50, 50 ], [ 50, 50 ] ] 15 | ] 16 | 17 | const velocity = vec2.random([], 60) 18 | 19 | return { 20 | angle: 2.6, 21 | radius: 10, 22 | position: [ 0, 0 ], 23 | velocity: [ 80, 0], 24 | dx: 80, 25 | lines, 26 | ...common.init(context, width, height) 27 | } 28 | } 29 | 30 | 31 | function draw (data, dt) { 32 | common.clear(data) 33 | 34 | const delta = vec2.scale([], data.velocity, dt) 35 | 36 | for (const line of data.lines) 37 | common.drawSegment(data, line[0], line[1], '#666') 38 | 39 | const c = contact() 40 | 41 | if (segmentsSphereSweep1(data.lines, data.position, data.radius, delta, c)) { 42 | common.drawCircle(data, data.position, data.radius, '#ff0') 43 | common.drawPoint(data, c.position, '#ff0', '', 2) 44 | 45 | // bounce 46 | vec2.scale(data.velocity, c.normal, data.dx) 47 | 48 | } else { 49 | vec2.add(data.position, data.position, delta) 50 | common.drawCircle(data, data.position, data.radius, '#0f0') 51 | } 52 | 53 | } 54 | 55 | 56 | export default { init, draw } 57 | -------------------------------------------------------------------------------- /figures/sphere-sphere-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import sphereSphereOverlap from '../src/sphere-sphere-overlap.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | function init (context, width, height) { 7 | return { 8 | angle: 0, 9 | sweepBoxes: [ 10 | { 11 | position: [ -60, 24 ], 12 | radius: 32 13 | }, 14 | { 15 | position: [ 60, -48 ], 16 | radius: 32 17 | } 18 | ], 19 | 20 | sweepDeltas: [ 21 | [ 64, -20 ], 22 | [ -64,50 ] 23 | ], 24 | 25 | ...common.init(context, width, height) 26 | } 27 | } 28 | 29 | 30 | function draw (data, dt) { 31 | common.clear(data) 32 | 33 | data.angle += 0.5 * Math.PI * dt 34 | 35 | const factor = (Math.cos(data.angle) + 1) * 0.5 || 1e-8 36 | 37 | const vA = vec2.scale([], data.sweepDeltas[0], factor) 38 | const vB = vec2.scale([], data.sweepDeltas[1], factor) 39 | 40 | const centerA = data.sweepBoxes[0].position 41 | const radiusA = data.sweepBoxes[0].radius 42 | 43 | const centerB = data.sweepBoxes[1].position 44 | const radiusB = data.sweepBoxes[1].radius 45 | 46 | const tmp1 = vec2.add([], centerA, vA) 47 | const tmp2 = vec2.add([], centerB, vB) 48 | 49 | 50 | const overlapping = sphereSphereOverlap(tmp1, radiusA, tmp2, radiusB) 51 | 52 | if (overlapping) { 53 | common.drawCircle(data, tmp1, radiusA, '#f00') 54 | common.drawCircle(data, tmp2, radiusB, '#f00') 55 | } else { 56 | common.drawCircle(data, tmp1, radiusA, '#0f0') 57 | common.drawCircle(data, tmp2, radiusB, '#0f0') 58 | } 59 | 60 | } 61 | 62 | 63 | export default { init, draw } 64 | -------------------------------------------------------------------------------- /figures/triangle-point-overlap.js: -------------------------------------------------------------------------------- 1 | import common from './common.js' 2 | import triPointOverlap from '../src/triangle-point-overlap.js' 3 | 4 | 5 | function init (context, width, height) { 6 | return { 7 | angle: 0, 8 | pos: [ 0, 0 ], 9 | tri: [ 10 | [ 0, -20 ], 11 | [ -20, 20 ], 12 | [ 20, 20 ] 13 | ], 14 | ...common.init(context, width, height) 15 | } 16 | } 17 | 18 | 19 | function draw (data, dt) { 20 | common.clear(data) 21 | 22 | data.angle += 0.5 * Math.PI * dt 23 | data.pos[0] = Math.cos(data.angle * 0.4) * 32 24 | data.pos[1] = Math.sin(data.angle) * 12 25 | 26 | const overlapping = triPointOverlap(data.tri[0], data.tri[1], data.tri[2], data.pos) 27 | 28 | common.drawTriangle(data, data.tri[0], data.tri[1], data.tri[2], '#666') 29 | 30 | const pointWidth = 1 31 | 32 | if (overlapping) 33 | common.drawPoint(data, data.pos, '#f00', '', pointWidth) 34 | else 35 | common.drawPoint(data, data.pos, '#0f0', '', pointWidth) 36 | } 37 | 38 | 39 | export default { init, draw } 40 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@footgun/collision-2d", 3 | "version": "0.2.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@footgun/collision-2d", 9 | "version": "0.2.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@footgun/math-gap": "^0.2.0", 13 | "clamp": "^1.0.1", 14 | "gl-matrix": "^3.4.3", 15 | "point-to-segment-2d": "^1.0.0", 16 | "robust-point-in-polygon": "^1.0.3", 17 | "segseg": "^1.0.0", 18 | "wgpu-matrix": "^3.4.0" 19 | } 20 | }, 21 | "node_modules/@footgun/math-gap": { 22 | "version": "0.2.0", 23 | "resolved": "https://registry.npmjs.org/@footgun/math-gap/-/math-gap-0.2.0.tgz", 24 | "integrity": "sha512-sGhfGo5OOEGAv4j4eHvECDwgnX2IDdx1Xd2MJeiFAyP9b/htmopRd2PNFQzQETxEuMBj+I5M5Odeky+AA+CFjQ==", 25 | "license": "MIT" 26 | }, 27 | "node_modules/clamp": { 28 | "version": "1.0.1", 29 | "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", 30 | "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==", 31 | "license": "MIT" 32 | }, 33 | "node_modules/gl-matrix": { 34 | "version": "3.4.3", 35 | "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", 36 | "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", 37 | "license": "MIT" 38 | }, 39 | "node_modules/point-to-segment-2d": { 40 | "version": "1.0.0", 41 | "resolved": "https://registry.npmjs.org/point-to-segment-2d/-/point-to-segment-2d-1.0.0.tgz", 42 | "integrity": "sha512-qjMaW51jOWDjvNSEntnxiGCvfWKu64Iiu6FVRaCWB5hBlZG7adxhx1AG3sXEwgf7v7e+o0y7TpO65OgumpDhfg==", 43 | "license": "MIT", 44 | "engines": { 45 | "node": ">=12" 46 | } 47 | }, 48 | "node_modules/robust-orientation": { 49 | "version": "1.2.1", 50 | "resolved": "https://registry.npmjs.org/robust-orientation/-/robust-orientation-1.2.1.tgz", 51 | "integrity": "sha512-FuTptgKwY6iNuU15nrIJDLjXzCChWB+T4AvksRtwPS/WZ3HuP1CElCm1t+OBfgQKfWbtZIawip+61k7+buRKAg==", 52 | "license": "MIT", 53 | "dependencies": { 54 | "robust-scale": "^1.0.2", 55 | "robust-subtract": "^1.0.0", 56 | "robust-sum": "^1.0.0", 57 | "two-product": "^1.0.2" 58 | } 59 | }, 60 | "node_modules/robust-point-in-polygon": { 61 | "version": "1.0.3", 62 | "resolved": "https://registry.npmjs.org/robust-point-in-polygon/-/robust-point-in-polygon-1.0.3.tgz", 63 | "integrity": "sha512-pPzz7AevOOcPYnFv4Vs5L0C7BKOq6C/TfAw5EUE58CylbjGiPyMjAnPLzzSuPZ2zftUGwWbmLWPOjPOz61tAcA==", 64 | "license": "MIT", 65 | "dependencies": { 66 | "robust-orientation": "^1.0.2" 67 | } 68 | }, 69 | "node_modules/robust-scale": { 70 | "version": "1.0.2", 71 | "resolved": "https://registry.npmjs.org/robust-scale/-/robust-scale-1.0.2.tgz", 72 | "integrity": "sha512-jBR91a/vomMAzazwpsPTPeuTPPmWBacwA+WYGNKcRGSh6xweuQ2ZbjRZ4v792/bZOhRKXRiQH0F48AvuajY0tQ==", 73 | "license": "MIT", 74 | "dependencies": { 75 | "two-product": "^1.0.2", 76 | "two-sum": "^1.0.0" 77 | } 78 | }, 79 | "node_modules/robust-subtract": { 80 | "version": "1.0.0", 81 | "resolved": "https://registry.npmjs.org/robust-subtract/-/robust-subtract-1.0.0.tgz", 82 | "integrity": "sha512-xhKUno+Rl+trmxAIVwjQMiVdpF5llxytozXJOdoT4eTIqmqsndQqFb1A0oiW3sZGlhMRhOi6pAD4MF1YYW6o/A==", 83 | "license": "MIT" 84 | }, 85 | "node_modules/robust-sum": { 86 | "version": "1.0.0", 87 | "resolved": "https://registry.npmjs.org/robust-sum/-/robust-sum-1.0.0.tgz", 88 | "integrity": "sha512-AvLExwpaqUqD1uwLU6MwzzfRdaI6VEZsyvQ3IAQ0ZJ08v1H+DTyqskrf2ZJyh0BDduFVLN7H04Zmc+qTiahhAw==", 89 | "license": "MIT" 90 | }, 91 | "node_modules/segseg": { 92 | "version": "1.0.0", 93 | "resolved": "https://registry.npmjs.org/segseg/-/segseg-1.0.0.tgz", 94 | "integrity": "sha512-2yXmyV1AJN9F9CFlUaQ0XLo0zh8C/BgB91ae8oouDw2yvH6uCXkxOvRocbvgAJmrxssiUn3PgfzNv+0yA4OmYA==", 95 | "license": "MIT", 96 | "engines": { 97 | "node": ">=12" 98 | } 99 | }, 100 | "node_modules/two-product": { 101 | "version": "1.0.2", 102 | "resolved": "https://registry.npmjs.org/two-product/-/two-product-1.0.2.tgz", 103 | "integrity": "sha512-vOyrqmeYvzjToVM08iU52OFocWT6eB/I5LUWYnxeAPGXAhAxXYU/Yr/R2uY5/5n4bvJQL9AQulIuxpIsMoT8XQ==", 104 | "license": "MIT" 105 | }, 106 | "node_modules/two-sum": { 107 | "version": "1.0.0", 108 | "resolved": "https://registry.npmjs.org/two-sum/-/two-sum-1.0.0.tgz", 109 | "integrity": "sha512-phP48e8AawgsNUjEY2WvoIWqdie8PoiDZGxTDv70LDr01uX5wLEQbOgSP7Z/B6+SW5oLtbe8qaYX2fKJs3CGTw==", 110 | "license": "MIT" 111 | }, 112 | "node_modules/wgpu-matrix": { 113 | "version": "3.4.0", 114 | "resolved": "https://registry.npmjs.org/wgpu-matrix/-/wgpu-matrix-3.4.0.tgz", 115 | "integrity": "sha512-kXHrbAPKEn9A32Wf4wVldyx9MmnzwhuB5p8GCqoJP3ItU5+iDT4J3aTQwPZWkfb153hwGtqZtUwR2M+ipJKadg==", 116 | "license": "MIT" 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@footgun/collision-2d", 3 | "version": "0.3.0", 4 | "description": "A collection of collision detection and response functions for 2D", 5 | "main": "collision-2d.js", 6 | "type": "module", 7 | "directories": { 8 | "doc": "docs", 9 | "test": "test" 10 | }, 11 | "scripts": { 12 | "build:figures": "./build-figures.sh", 13 | "prepublishOnly": "npm test", 14 | "test": "node --test ./test/*.js" 15 | }, 16 | "keywords": [ 17 | "collision", 18 | "2d", 19 | "physics", 20 | "geometry" 21 | ], 22 | "author": "Michael Reinstein", 23 | "license": "MIT", 24 | "repository": { 25 | "type": "git", 26 | "url": "git@github.com:mreinstein/collision-2d.git" 27 | }, 28 | "dependencies": { 29 | "@footgun/math-gap": "^0.2.0", 30 | "clamp": "^1.0.1", 31 | "gl-matrix": "^3.4.3", 32 | "point-to-segment-2d": "^1.0.0", 33 | "robust-point-in-polygon": "^1.0.3", 34 | "segseg": "^1.0.0", 35 | "wgpu-matrix": "^3.4.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/AABB.js: -------------------------------------------------------------------------------- 1 | // generate a bounding AABB from another primitive 2 | 3 | 4 | // create an AABB containing all 2D points 5 | export function fromPoints (points) { 6 | if (!points.length) 7 | throw new Error('no points specified') 8 | 9 | let minX, maxX, minY, maxY 10 | 11 | minX = maxX = points[0][0] 12 | minY = maxY = points[0][1] 13 | 14 | for (let i=1; i < points.length; i++) { 15 | minX = Math.min(minX, points[i][0]) 16 | maxX = Math.max(maxX, points[i][0]) 17 | minY = Math.min(minY, points[i][1]) 18 | maxY = Math.max(maxY, points[i][1]) 19 | } 20 | 21 | const width = maxX - minX 22 | const height = maxY - minY 23 | 24 | return { 25 | position: [ minX + (width / 2), minY + (height / 2)], 26 | width, 27 | height 28 | } 29 | } 30 | 31 | 32 | // create an AABB containing all line segments 33 | export function fromSegments (segments, indices) { 34 | if (!segments.length) 35 | throw new Error('no segments specified') 36 | 37 | if (!indices.length) 38 | throw new Error('no indices specified') 39 | 40 | let minX, maxX, minY, maxY 41 | 42 | const seg = segments[indices[0]] 43 | minX = maxX = seg[0][0] 44 | minY = maxY = seg[0][1] 45 | 46 | for (const idx of indices) { 47 | const seg = segments[idx] 48 | minX = Math.min(minX, seg[0][0]) 49 | minX = Math.min(minX, seg[1][0]) 50 | 51 | maxX = Math.max(maxX, seg[0][0]) 52 | maxX = Math.max(maxX, seg[1][0]) 53 | 54 | minY = Math.min(minY, seg[0][1]) 55 | minY = Math.min(minY, seg[1][1]) 56 | 57 | maxY = Math.max(maxY, seg[0][1]) 58 | maxY = Math.max(maxY, seg[1][1]) 59 | } 60 | 61 | const width = maxX - minX 62 | const height = maxY - minY 63 | 64 | return { 65 | position: [ minX + (width / 2), minY + (height / 2)], 66 | width, 67 | height 68 | } 69 | } 70 | 71 | 72 | // create an AABB containing all triangle vertices 73 | export function fromTriangle (p0, p1, p2) { 74 | const minX = Math.min(p0[0], p1[0], p2[0]) 75 | const maxX = Math.max(p0[0], p1[0], p2[0]) 76 | 77 | const minY = Math.min(p0[1], p1[1], p2[1]) 78 | const maxY = Math.max(p0[1], p1[1], p2[1]) 79 | 80 | const width = maxX - minX 81 | const height = maxY - minY 82 | 83 | return { 84 | position: [ minX + (width / 2), minY + (height / 2)], 85 | width, 86 | height 87 | } 88 | } 89 | 90 | 91 | // create an AABB containing a sphere 92 | export function fromSphere (center, radius) { 93 | return { 94 | position: [ center[0], center[1] ], 95 | width: radius * 2, 96 | height: radius * 2 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Plane.js: -------------------------------------------------------------------------------- 1 | // plane implementation adapted from Appendix B of http://www.peroxide.dk/papers/collision/collision.pdf 2 | import segmentNormal from './segment-normal.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | function create () { 7 | return { 8 | origin: vec2.create(), 9 | normal: vec2.create(), 10 | D: 0, 11 | } 12 | } 13 | 14 | 15 | // create a new plane from a normal and origin (a point on the plane) 16 | function fromPlane (out, origin, normal) { 17 | vec2.copy(out.origin, origin) 18 | vec2.copy(out.normal, normal) 19 | out.D = -(normal[0] * origin[0] + normal[1] * origin[1]) 20 | return out 21 | } 22 | 23 | 24 | // create a new plane from a line segment 25 | function fromSegment (out, p0, p1) { 26 | vec2.copy(out.origin, p0) 27 | segmentNormal(out.normal, p0, p1) 28 | out.D = -(out.normal[0] * out.origin[0] + out.normal[1] * out.origin[1]) 29 | return out 30 | } 31 | 32 | 33 | function isFrontFacingTo (plane, dir) { 34 | const dot = vec2.dot(plane.normal, dir) 35 | return dot <= 0 36 | } 37 | 38 | 39 | // returns the distance from the ray origin to the plane along the ray 40 | // @param vec2 rOrigin starting point of ray 41 | // @param vec2 rVector must be normalized 42 | // @return Number the distance from the ray origin to the plane along the ray 43 | function rayDistance (plane, rOrigin, rVector) { 44 | const numer = vec2.dot(plane.normal, rOrigin) + plane.D 45 | const denom = vec2.dot(plane.normal, rVector) 46 | if (denom === 0) // normal is orthogonal to vector, cant intersect 47 | return -1 48 | 49 | return -(numer / denom) 50 | } 51 | 52 | 53 | // determine how far a point is from a plane 54 | // returns a signed number indicating plane distance (negative numbers means behind the plane) 55 | function signedDistanceTo (plane, point) { 56 | return vec2.dot(point, plane.normal) + plane.D 57 | } 58 | 59 | 60 | export default { create, fromPlane, fromSegment, isFrontFacingTo, rayDistance, signedDistanceTo } 61 | -------------------------------------------------------------------------------- /src/Polygon.js: -------------------------------------------------------------------------------- 1 | import pointInPoly from 'robust-point-in-polygon' 2 | import aabbPointOverlap from './aabb-point-overlap.js' 3 | import { vec2 } from 'wgpu-matrix' 4 | 5 | 6 | export function isPointInsidePolygon (point, polygon) { 7 | if (!aabbPointOverlap(polygon.aabb, point)) 8 | return false 9 | 10 | const OUTSIDE_POLYGON = 1 11 | if (pointInPoly(polygon.points, point) === OUTSIDE_POLYGON) 12 | return false 13 | 14 | return true 15 | } 16 | 17 | 18 | // find the closest point on the edge of a polygon to a given point. 19 | // assumes the point is outside of the polygon 20 | export function closestPointOnPolygon (point, polygon) { 21 | let closest 22 | 23 | for (let i=0; i < polygon.points.length-1; i++) { 24 | const c = _getClosestPoint(polygon.points[i], polygon.points[i+1], point) 25 | if (!closest || vec2.distance(c, point) < vec2.distance(closest, point)) 26 | closest = c 27 | } 28 | 29 | return closest 30 | } 31 | 32 | 33 | // given a line defined by [A, B] and a point P, find the closest point on that line to P 34 | // set 'segmentClamp' to true if you want the closest point on the segment, not just the line. 35 | // from https://www.gamedev.net/forums/topic/444154-closest-point-on-a-line/3941160/ 36 | // 37 | // This one is unused but maybe good inspiration if the prev link's logic doesn't work out: 38 | // //https://math.stackexchange.com/questions/2193720/find-a-point-on-a-line-segment-which-is-the-closest-to-other-point-not-on-the-li 39 | function _getClosestPoint (A, B, P, segmentClamp=true) { 40 | const AP = vec2.subtract(P, A) 41 | const AB = vec2.subtract(B, A) 42 | 43 | const ab2 = AB[0]*AB[0] + AB[1]*AB[1] 44 | const ap_ab = AP[0]*AB[0] + AP[1]*AB[1] 45 | let t = ap_ab / ab2 46 | 47 | if (segmentClamp) { 48 | if (t < 0.0) 49 | t = 0.0 50 | else if (t > 1.0) 51 | t = 1.0 52 | } 53 | 54 | return vec2.addScaled(A, AB, t) 55 | } 56 | 57 | /////////////////////////// -------------------------------------------------------------------------------- /src/PolylinePath.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix' 2 | 3 | 4 | // this module was ported from OpenSteer's Pathway.cpp implementation 5 | 6 | // static temp variables used to avoid mem allocations 7 | const tmpSegDistance = { 8 | segmentProjection: 0, 9 | distance: 0, 10 | chosen: vec2.create() 11 | } 12 | 13 | const segmentNormal = vec2.create() 14 | 15 | const local = vec2.create() 16 | 17 | 18 | // a simple implementation of the Pathway protocol. The path is a "polyline" a series of line 19 | // segments between specified points. A radius defines a volume for the path which is the union 20 | // of a sphere at each point and a cylinder along each segment. 21 | export function create (options={}) { 22 | // construct a PolylinePathway given the number of points (vertices), 23 | // an array of points, and a path radius. 24 | let { points, pointCount, radius, cyclic } = options 25 | 26 | if (cyclic) 27 | pointCount++ 28 | 29 | const normals = [ ] 30 | const lengths = [ ] 31 | let totalPathLength = 0 32 | 33 | // loop over all points 34 | for (let i = 0; i < pointCount; i++) { 35 | // copy in point locations, closing cycle when appropriate 36 | const closeCycle = cyclic && (i == pointCount-1) 37 | const j = closeCycle ? 0 : i 38 | points[i] = points[j] 39 | 40 | // for the end of each segment 41 | if (i > 0) { 42 | // compute the segment length 43 | normals[i] = vec2.subtract(vec2.create(), points[i], points[i-1]) 44 | lengths[i] = vec2.length(normals[i]) 45 | 46 | // find the normalized vector parallel to the segment 47 | vec2.normalize(normals[i], normals[i]) 48 | 49 | // keep running total of segment lengths 50 | totalPathLength += lengths[i] 51 | } 52 | } 53 | 54 | return { 55 | pointCount, 56 | points, 57 | radius, 58 | cyclic, 59 | 60 | // lengths of each line segment between the points 61 | lengths, 62 | 63 | // unit vectors of each line segment between the points 64 | normals, 65 | 66 | totalPathLength 67 | } 68 | } 69 | 70 | 71 | // Given an arbitrary point ("A"), returns the nearest point ("P") on 72 | // this path. Also returns, via output arguments, the path tangent at 73 | // P and a measure of how far A is outside the Pathway's "tube". Note 74 | // that a negative distance indicates A is inside the Pathway. 75 | export function mapPointToPath (out, path, point) { 76 | let d, minDistance 77 | 78 | // loop over all segments, find the one nearest to the given point 79 | for (let i = 1; i < path.pointCount; i++) { 80 | pointToSegmentDistance(tmpSegDistance, point, path.points[i-1], path.points[i]) 81 | d = tmpSegDistance.distance 82 | if ((minDistance === undefined) || d < minDistance) { 83 | minDistance = d 84 | vec2.copy(out.onPath, tmpSegDistance.chosen) // set the point on the path 85 | 86 | vec2.subtract(out.tangent, path.points[i], path.points[i-1]) 87 | vec2.normalize(out.tangent, out.tangent) 88 | } 89 | } 90 | 91 | // measure how far original point is outside the Pathway's "tube" 92 | out.outside = vec2.distance(out.onPath, point) - path.radius 93 | 94 | return out 95 | } 96 | 97 | 98 | // given an arbitrary point, convert it to a distance along the path 99 | export function mapPointToPathDistance (path, point) { 100 | let d, minDistance 101 | let segmentLengthTotal = 0 102 | let pathDistance = 0 103 | 104 | for (let i = 1; i < path.pointCount; i++) { 105 | pointToSegmentDistance(tmpSegDistance, point, path.points[i-1], path.points[i]) 106 | d = tmpSegDistance.distance 107 | const segmentLength = path.lengths[i] 108 | 109 | if ((minDistance === undefined) || d < minDistance) { 110 | minDistance = d 111 | pathDistance = segmentLengthTotal + tmpSegDistance.segmentProjection 112 | } 113 | segmentLengthTotal += segmentLength 114 | } 115 | 116 | // return distance along path of onPath point 117 | return pathDistance 118 | } 119 | 120 | 121 | // given a distance along the path, convert it to a point on the path 122 | export function mapPathDistanceToPoint (out, path, pathDistance) { 123 | // clip or wrap given path distance according to cyclic flag 124 | let remaining = pathDistance 125 | 126 | if (path.cyclic) { 127 | remaining = pathDistance % path.totalPathLength //fmod(pathDistance, totalPathLength) 128 | } else { 129 | if (pathDistance < 0) 130 | return vec2.copy(out, path.points[0]) 131 | if (pathDistance >= path.totalPathLength) 132 | return vec2.copy(out, path.points[path.pointCount-1]) 133 | } 134 | 135 | // step through segments, subtracting off segment lengths until 136 | // locating the segment that contains the original pathDistance. 137 | // Interpolate along that segment to find 3d point value to return. 138 | for (let i = 1; i < path.pointCount; i++) { 139 | const segmentLength = path.lengths[i] 140 | if (segmentLength < remaining) { 141 | remaining -= segmentLength 142 | } else { 143 | const ratio = remaining / segmentLength 144 | //result = interpolate(ratio, points[i-1], points[i]) 145 | vec2.lerp(out, path.points[i-1], path.points[i], ratio) 146 | break 147 | } 148 | } 149 | return out 150 | } 151 | 152 | 153 | // TODO: compare this against https://www.npmjs.com/package/point-to-segment-2d 154 | // They claim to do the same things. Is that true? Is one better than the other? 155 | // 156 | // computes distance from a point to a line segment 157 | function pointToSegmentDistance (out, point, ep0, ep1) { 158 | 159 | vec2.subtract(segmentNormal, ep1, ep0) 160 | vec2.normalize(segmentNormal, segmentNormal) 161 | 162 | const segmentLength = vec2.distance(ep0, ep1) 163 | 164 | // convert the test point to be "local" to ep0 165 | vec2.subtract(local, point, ep0) 166 | 167 | // find the projection of "local" onto "segmentNormal" 168 | const segmentProjection = vec2.dot(segmentNormal, local) 169 | 170 | // handle boundary cases: when projection is not on segment, the 171 | // nearest point is one of the endpoints of the segment 172 | if (segmentProjection < 0) { 173 | vec2.copy(out.chosen, ep0) 174 | out.segmentProjection = 0 175 | out.distance = vec2.distance(point, ep0) 176 | return out 177 | } 178 | 179 | if (segmentProjection > segmentLength) { 180 | vec2.copy(out.chosen, ep1) 181 | out.segmentProjection = segmentLength 182 | out.distance = vec2.distance(point, ep1) 183 | return out 184 | } 185 | 186 | // otherwise nearest point is projection point on segment 187 | vec2.scale(out.chosen, segmentNormal, segmentProjection) 188 | vec2.add(out.chosen, out.chosen, ep0) 189 | 190 | out.segmentProjection = segmentProjection 191 | out.distance = vec2.distance(point, out.chosen) 192 | return out 193 | } 194 | -------------------------------------------------------------------------------- /src/TraceInfo.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix' 2 | 3 | 4 | export default function TraceInfo() { 5 | this.start = vec2.create() 6 | this.end = vec2.create() 7 | this.scaledStart = vec2.create() 8 | this.radius = 0 9 | this.invRadius = 0 10 | this.vel = vec2.create() 11 | this.scaledVel = vec2.create() 12 | this.velLength = 0 13 | this.normVel = vec2.create() 14 | this.collision = false 15 | 16 | this.t = 0 // time of collision from 0..1 when collision is true 17 | this.intersectPoint = vec2.create() // point on the triangle where the sphere collided 18 | 19 | this.tmp = vec2.create() 20 | this.tmpTri = [ vec2.create(), vec2.create() ] 21 | this.intersectTri = [ vec2.create(), vec2.create() ] 22 | 23 | this.tmpTriNorm = vec2.create() 24 | this.intersectTriNorm = vec2.create() 25 | } 26 | 27 | 28 | TraceInfo.prototype.resetTrace = function(start, end, radius) { 29 | this.invRadius = 1/radius 30 | this.radius = radius 31 | 32 | vec2.copy(this.start, start) 33 | vec2.copy(this.end, end) 34 | vec2.subtract(this.vel, end, start) 35 | vec2.normalize(this.normVel, this.vel) 36 | 37 | vec2.scale(this.scaledStart, start, this.invRadius) 38 | vec2.scale(this.scaledVel, this.vel, this.invRadius) 39 | 40 | this.velLength = vec2.length(this.vel) 41 | 42 | this.collision = false 43 | this.t = 1.0 44 | } 45 | 46 | 47 | TraceInfo.prototype.setCollision = function(t, point) { 48 | this.collision = true 49 | vec2.copy(this.intersectTri[0], this.tmpTri[0]) 50 | vec2.copy(this.intersectTri[1], this.tmpTri[1]) 51 | vec2.copy(this.intersectTriNorm, this.tmpTriNorm) 52 | if (t < this.t) { 53 | this.t = t 54 | vec2.scale(this.intersectPoint, point, this.radius) 55 | } 56 | } 57 | 58 | 59 | // position of the sphere when it collided with the closest triangle 60 | TraceInfo.prototype.getTraceEndpoint = function(end) { 61 | vec2.scale(this.tmp, this.vel, this.t) 62 | vec2.add(end, this.start, this.tmp) 63 | return end 64 | } 65 | 66 | 67 | TraceInfo.prototype.getTraceDistance = function() { 68 | vec2.scale(this.tmp, this.vel, this.t) 69 | return vec2.length(this.tmp) 70 | } 71 | -------------------------------------------------------------------------------- /src/aabb-aabb-contain.js: -------------------------------------------------------------------------------- 1 | // returns true if A fully contains B (B is fully inside of the bounds of A) 2 | export default function aabbAABBContain (a, b) { 3 | // B extends to the left of A 4 | if (b.position[0] - (b.width/2) < a.position[0] - (a.width/2)) 5 | return false 6 | 7 | // B extends to the right of A 8 | if (b.position[0] + (b.width/2) > a.position[0] + (a.width/2)) 9 | return false 10 | 11 | // B extends above A 12 | if (b.position[1] - (b.height/2) < a.position[1] - (a.height/2)) 13 | return false 14 | 15 | // B extends below A 16 | if (b.position[1] + (b.height/2) > a.position[1] + (a.height/2)) 17 | return false 18 | 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /src/aabb-aabb-overlap.js: -------------------------------------------------------------------------------- 1 | import { sign } from '@footgun/math-gap' 2 | import { vec2 } from 'gl-matrix' 3 | 4 | 5 | /* 6 | https://noonat.github.io/intersect/#aabb-vs-aabb 7 | 8 | This test uses a separating axis test, which checks for overlaps between the 9 | two boxes on each axis. If either axis is not overlapping, the boxes aren’t 10 | colliding. 11 | 12 | The function returns a Hit object, or null if the two static boxes do not 13 | overlap, and gives the axis of least overlap as the contact point. That is, it 14 | sets hit.delta so that the colliding box will be pushed out of the nearest edge 15 | This can cause weird behavior for moving boxes, so you should use sweepAABB 16 | instead for moving boxes. 17 | 18 | @param Object contact if specified, filled with collision details 19 | */ 20 | export default function aabbAABBOverlap (rect, rect2, contact=null) { 21 | 22 | const dx = rect2.position[0] - rect.position[0] 23 | const px = (rect.width / 2 + rect2.width / 2) - Math.abs(dx) 24 | 25 | if (px <= 0) 26 | return false 27 | 28 | const dy = rect2.position[1] - rect.position[1] 29 | const py = (rect.height / 2 + rect2.height / 2) - Math.abs(dy) 30 | 31 | if (py <= 0) 32 | return false 33 | 34 | // if we don't have to provide details on the collision, it's sufficient to 35 | // return true, indicating the rectangles do intersect 36 | if (!contact) 37 | return true 38 | 39 | /* 40 | pos is the point of contact between the two objects (or an estimation of it, in some sweep tests). 41 | normal is the surface normal at the point of contact. 42 | delta is the overlap between the two objects, and is a vector that can be added to the colliding object’s position to move it back to a non-colliding state. 43 | */ 44 | contact.collider = rect 45 | vec2.set(contact.delta, 0, 0) 46 | vec2.set(contact.normal, 0, 0) 47 | contact.time = 0 // boxes overlap 48 | 49 | if (px < py) { 50 | const sx = sign(dx) 51 | contact.delta[0] = px * sx 52 | contact.normal[0] = sx 53 | contact.position[0] = rect.position[0] + (rect.width / 2 * sx) 54 | contact.position[1] = rect2.position[1] 55 | } else { 56 | const sy = sign(dy) 57 | contact.delta[1] = py * sy 58 | contact.normal[1] = sy 59 | contact.position[0] = rect2.position[0] 60 | contact.position[1] = rect.position[1] + (rect.height / 2 * sy) 61 | } 62 | 63 | return true 64 | } 65 | -------------------------------------------------------------------------------- /src/aabb-aabb-sweep1.js: -------------------------------------------------------------------------------- 1 | import clamp from 'clamp' 2 | import intersectAABB from './aabb-aabb-overlap.js' 3 | import intersectLine from './aabb-segment-overlap.js' 4 | import { vec2 } from 'gl-matrix' 5 | 6 | 7 | const EPSILON = 1e-8 8 | 9 | /* 10 | finds the intersection of this box and another moving box, where the delta 11 | argument is the displacement vector of the moving box 12 | https://noonat.github.io/intersect/#aabb-vs-swept-aabb 13 | 14 | @param aabb the static box 15 | @param aabb2 the moving box 16 | @param delta the displacement vector of aabb2 17 | @param contact the contact object. filled if collision occurs 18 | @return bool true if the two AABBs collide, false otherwise 19 | */ 20 | export default function aabbAABBSweep1 (aabb, aabb2, delta, contact) { 21 | if (delta[0] === 0 && delta[1] === 0) 22 | return intersectAABB(aabb, aabb2, contact) 23 | 24 | const result = intersectLine(aabb, aabb2.position, delta, aabb2.width/2, aabb2.height/2, contact) 25 | if (result) { 26 | contact.time = clamp(contact.time - EPSILON, 0, 1) 27 | 28 | //contact.position[0] = aabb2.position[0] + delta[0] * contact.time 29 | //contact.position[1] = aabb2.position[1] + delta[1] * contact.time 30 | 31 | const direction = vec2.normalize([], delta) 32 | 33 | const halfX2 = aabb2.width / 2 34 | const halfY2 = aabb2.height / 2 35 | 36 | const halfX = aabb.width / 2 37 | const halfY = aabb.height / 2 38 | 39 | contact.position[0] = clamp(contact.position[0] + direction[0] * halfX2, aabb.position[0] - halfX, aabb.position[0] + halfX); 40 | contact.position[1] = clamp(contact.position[1] + direction[1] * halfY2, aabb.position[1] - halfY, aabb.position[1] + halfY); 41 | } 42 | 43 | return result 44 | } 45 | -------------------------------------------------------------------------------- /src/aabb-aabb-sweep2.js: -------------------------------------------------------------------------------- 1 | import aabbAABBOverlap from './aabb-aabb-overlap.js' 2 | import aabbAabbSweep1 from './aabb-aabb-sweep1.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | const pos2 = vec2.create() 7 | const dir = vec2.create() 8 | const amt = vec2.create() 9 | 10 | 11 | // Sweep two moving AABBs to see if and when they first and last were overlapping 12 | // https://www.gamedeveloper.com/disciplines/simple-intersection-tests-for-games 13 | // 14 | // A previous state of AABB A 15 | // B previous state of AABB B 16 | // va displacment vector of A 17 | // vb displacement vector of B 18 | // contact 19 | export default function aabbAABBSweep2 (A, va, B, vb, contact) { 20 | const delta = vec2.subtract([], vb, va) 21 | const hit = aabbAabbSweep1(A, B, delta, contact) 22 | 23 | if (hit) { 24 | // tweak collision position 25 | 26 | contact.position[0] = A.position[0] + va[0] * contact.time 27 | contact.position[1] = A.position[1] + va[1] * contact.time 28 | 29 | pos2[0] = B.position[0] + vb[0] * contact.time 30 | pos2[1] = B.position[1] + vb[1] * contact.time 31 | 32 | vec2.subtract(dir, pos2, contact.position) 33 | vec2.scale(amt, dir, 0.5) 34 | 35 | contact.position[0] += amt[0] 36 | contact.position[1] += amt[1] 37 | } 38 | 39 | return hit 40 | } 41 | -------------------------------------------------------------------------------- /src/aabb-point-overlap.js: -------------------------------------------------------------------------------- 1 | import { sign } from '@footgun/math-gap' 2 | import { vec2 } from 'gl-matrix' 3 | 4 | 5 | /* 6 | https://noonat.github.io/intersect/#aabb-vs-point 7 | 8 | If a point is behind all of the edges of the box, it’s colliding. 9 | The function returns true if the point is in the aabb, false otherwise 10 | 11 | contact.position and contact.delta will be set to the nearest edge of the box. 12 | 13 | This code first finds the overlap on the X and Y axis. If the overlap is less than zero for either, 14 | a collision is not possible. Otherwise, we find the axis with the smallest overlap and use that to 15 | create an intersection point on the edge of the box. 16 | 17 | @param Object contact if specified, filled with collision details 18 | */ 19 | export default function aabbPointOverlap (aabb, point, contact=null) { 20 | const halfX = aabb.width / 2 21 | 22 | const dx = point[0] - aabb.position[0] 23 | const px = halfX - Math.abs(dx) 24 | if (px <= 0) 25 | return false 26 | 27 | const halfY = aabb.height / 2 28 | const dy = point[1] - aabb.position[1] 29 | const py = halfY - Math.abs(dy) 30 | if (py <= 0) 31 | return false 32 | 33 | if (contact) { 34 | if (px < py) { 35 | const sx = sign(dx) 36 | vec2.set(contact.delta, px * sx, 0) 37 | vec2.set(contact.normal, sx, 0) 38 | contact.position[0] = aabb.position[0] + (halfX * sx) 39 | contact.position[1] = point[1] 40 | } else { 41 | const sy = sign(dy) 42 | vec2.set(contact.delta, 0, py * sy) 43 | vec2.set(contact.normal, 0, sy) 44 | contact.position[0] = point[0] 45 | contact.position[1] = aabb.position[1] + (halfY * sy) 46 | } 47 | 48 | contact.time = 1 49 | contact.collider = point 50 | } 51 | 52 | return true 53 | } 54 | -------------------------------------------------------------------------------- /src/aabb-segment-overlap.js: -------------------------------------------------------------------------------- 1 | import clamp from 'clamp' 2 | import { sign } from '@footgun/math-gap' 3 | 4 | 5 | /* 6 | determine if a line segment intersects a bounding box 7 | https://noonat.github.io/intersect/#aabb-vs-segment 8 | 9 | @param object rect bounding box to check, with { position, width, height } 10 | @param vec2 pos line segment origin/start position 11 | @param vec2 delta line segment move/displacement vector 12 | @param number paddingX added to the radius of the bounding box 13 | @param number paddingY added to the radius of the bounding box 14 | @param object contact physics contact descriptor. filled when argument isn't null and a collision occurs 15 | @return bool true if they intersect, false otherwise 16 | */ 17 | export default function aabbSegmentOverlap (rect, pos, delta, paddingX, paddingY, contact=null) { 18 | // if x or y is 0, result will be javascript Infinity 19 | let scaleX = 1.0 / delta[0] 20 | let scaleY = 1.0 / delta[1] 21 | 22 | let signX = sign(scaleX) 23 | let signY = sign(scaleY) 24 | let halfx = rect.width / 2 25 | let halfy = rect.height / 2 26 | let posx = rect.position[0] 27 | let posy = rect.position[1] 28 | let nearTimeX = (posx - signX * (halfx + paddingX) - pos[0]) * scaleX 29 | let nearTimeY = (posy - signY * (halfy + paddingY) - pos[1]) * scaleY 30 | let farTimeX = (posx + signX * (halfx + paddingX) - pos[0]) * scaleX 31 | let farTimeY = (posy + signY * (halfy + paddingY) - pos[1]) * scaleY 32 | 33 | if (Number.isNaN(nearTimeY)) 34 | nearTimeY = Infinity 35 | 36 | if (Number.isNaN(farTimeY)) 37 | farTimeY = Infinity 38 | 39 | if (nearTimeX > farTimeY || nearTimeY > farTimeX) 40 | return false 41 | 42 | const nearTime = nearTimeX > nearTimeY ? nearTimeX : nearTimeY 43 | const farTime = farTimeX < farTimeY ? farTimeX : farTimeY 44 | 45 | if (nearTime >= 1 || farTime <= 0) 46 | return false 47 | 48 | 49 | // if we don't have to provide details on the collision, it's sufficient to 50 | // return true, indicating the rectangles do intersect 51 | if (!contact) 52 | return true 53 | 54 | /* 55 | position is the point of contact between the two objects (or an estimation of it, in some sweep tests). 56 | normal is the surface normal at the point of contact. 57 | delta is the overlap between the two objects, and is a vector that can be added to the colliding object’s position to move it back to a non-colliding state. 58 | time is a fraction from 0 to 1 indicating how far along the line the collision occurred. (This is the t value for the line equation L(t) = A + t * (B - A)) 59 | */ 60 | contact.collider = rect 61 | contact.time = clamp(nearTime, 0, 1) 62 | if (nearTimeX > nearTimeY) { 63 | contact.normal[0] = -signX 64 | contact.normal[1] = 0 65 | } else { 66 | contact.normal[0] = 0 67 | contact.normal[1] = -signY 68 | } 69 | 70 | // NEW 71 | contact.delta[0] = (1.0 - contact.time) * -delta[0]; 72 | contact.delta[1] = (1.0 - contact.time) * -delta[1]; 73 | contact.position[0] = pos[0] + delta[0] * contact.time; 74 | contact.position[1] = pos[1] + delta[1] * contact.time; 75 | 76 | return true 77 | } 78 | -------------------------------------------------------------------------------- /src/aabb-segment-sweep1.js: -------------------------------------------------------------------------------- 1 | // based on https://gamedev.stackexchange.com/questions/29479/swept-aabb-vs-line-segment-2d 2 | 3 | import segmentNormal from './segment-normal.js' 4 | import { sign } from '@footgun/math-gap' 5 | import { vec2 } from 'gl-matrix' 6 | 7 | 8 | const aabbCenter = vec2.create() 9 | const aabbMin = vec2.create() 10 | const aabbMax = vec2.create() 11 | const lineNormal = vec2.create() 12 | const lineDir = vec2.create() 13 | const lineMin = vec2.create() 14 | const lineMax = vec2.create() 15 | const lineAabbDist = vec2.create() 16 | const hitNormal = vec2.create() 17 | const normalizedDelta = vec2.create() 18 | 19 | const PADDING = 0.005 20 | 21 | // sweep a moving aabb against a line segment 22 | // 23 | //@param line non-moving line segment 24 | //@param aabb moving box 25 | //@param vec2 delta movement vector of the aabb 26 | //@param object contact optional contact data structure filled on hit 27 | //@return boolean true when the box hits the segment 28 | export default function aabbSegmentSweep1 (line, aabb, delta, contact) { 29 | 30 | vec2.copy(aabbCenter, aabb.position) 31 | vec2.set(aabbMin, aabb.position[0] - aabb.width/2, aabb.position[1] - aabb.height/2) 32 | vec2.set(aabbMax, aabb.position[0] + aabb.width/2, aabb.position[1] + aabb.height/2) 33 | 34 | vec2.normalize(normalizedDelta, delta) 35 | 36 | // calculate line bounds 37 | vec2.subtract(lineDir,line[1],line[0]) 38 | if (lineDir[0] > 0) { 39 | // right 40 | lineMin[0] = line[0][0] 41 | lineMax[0] = line[1][0] 42 | 43 | } else { 44 | // left 45 | lineMin[0] = line[1][0] 46 | lineMax[0] = line[0][0] 47 | } 48 | 49 | if (lineDir[1] > 0) { 50 | // down 51 | lineMin[1] = line[0][1] 52 | lineMax[1] = line[1][1] 53 | 54 | } else { 55 | // up 56 | lineMin[1] = line[1][1] 57 | lineMax[1] = line[0][1] 58 | } 59 | 60 | // get aabb's center to line[0] distance 61 | vec2.subtract(lineAabbDist, line[0], aabbCenter) 62 | 63 | // get the line's normal 64 | // if the dot product of it and the delta is larger than 0, 65 | // it means the line's normal is facing away from the sweep 66 | segmentNormal(lineNormal, line[0], line[1]) 67 | vec2.copy(hitNormal, lineNormal) 68 | 69 | let hitTime = 0 // first overlap time 70 | let outTime = 1 // last overlap time 71 | 72 | // calculate the radius of the box in respect to the line normal 73 | let r = (aabb.width/2) * Math.abs(lineNormal[0]) + (aabb.height/2) * Math.abs(lineNormal[1]) 74 | 75 | // distance from box to line in respect to the line normal 76 | const boxProj = vec2.dot(lineAabbDist, lineNormal) 77 | 78 | // velocity, projected on the line normal 79 | const velProj = vec2.dot(delta, lineNormal) 80 | 81 | // inverse the radius if required 82 | if (velProj < 0) 83 | r *= -1 84 | 85 | // calculate first and last overlap times, 86 | // as if we're dealing with a line rather than a segment 87 | hitTime = Math.max((boxProj - r) / velProj, hitTime) 88 | outTime = Math.min((boxProj + r) / velProj, outTime) 89 | 90 | // run standard AABBvsAABB sweep 91 | // against an AABB constructed from the extents of the line segment 92 | // X axis overlap 93 | if (delta[0] < 0) { 94 | // sweeping left 95 | if (aabbMax[0] < lineMin[0]) 96 | return false 97 | 98 | const hit = (lineMax[0] - aabbMin[0]) / delta[0] 99 | const out = (lineMin[0] - aabbMax[0]) / delta[0] 100 | outTime = Math.min(out, outTime) 101 | if (hit >= hitTime && hit <= outTime) { 102 | // box is hitting the line on its end: 103 | // adjust the normal accordingly 104 | vec2.set(hitNormal, 1, 0) 105 | } 106 | hitTime = Math.max(hit, hitTime) 107 | 108 | } else if (delta[0] > 0) { 109 | // sweeping right 110 | if (aabbMin[0] > lineMax[0]) 111 | return false 112 | 113 | const hit = (lineMin[0] - aabbMax[0]) / delta[0] 114 | const out = (lineMax[0] - aabbMin[0]) / delta[0] 115 | outTime = Math.min(out, outTime) 116 | if (hit >= hitTime && hit <= outTime) 117 | vec2.set(hitNormal, -1, 0) 118 | 119 | hitTime = Math.max(hit, hitTime) 120 | 121 | } else if (lineMin[0] > aabbMax[0] || lineMax[0] < aabbMin[0]) { 122 | return false 123 | } 124 | 125 | if ( hitTime > outTime ) 126 | return false 127 | 128 | // Y axis overlap 129 | if (delta[1] < 0) { 130 | // sweeping up 131 | if (aabbMax[1] < lineMin[1]) 132 | return false 133 | 134 | const hit = (lineMax[1] - aabbMin[1]) / delta[1] 135 | const out = (lineMin[1] - aabbMax[1]) / delta[1] 136 | outTime = Math.min(out, outTime) 137 | if (hit >= hitTime && hit <= outTime) 138 | vec2.set(hitNormal, 0, 1) 139 | 140 | hitTime = Math.max(hit, hitTime) 141 | 142 | } else if (delta[1] > 0) { 143 | // sweeping down 144 | if (aabbMin[1] > lineMax[1]) 145 | return false 146 | 147 | const hit = (lineMin[1] - aabbMax[1]) / delta[1] 148 | const out = (lineMax[1] - aabbMin[1]) / delta[1] 149 | outTime = Math.min(out, outTime) 150 | if (hit >= hitTime && hit <= outTime) 151 | vec2.set(hitNormal, 0, -1) 152 | 153 | hitTime = Math.max(hit, hitTime) 154 | 155 | } else if (lineMin[1] > aabbMax[1] || lineMax[1] < aabbMin[1]) { 156 | return false 157 | } 158 | 159 | if (hitTime > outTime) 160 | return false 161 | 162 | // ignore this line if its normal is facing away from the sweep delta 163 | // check for this only at this point to account for a possibly changed hitNormal 164 | // from a hit on the line's end 165 | // 166 | // also ignore this line its normal is facing away from the adjusted hitNormal 167 | if (vec2.dot(normalizedDelta, hitNormal) > 0 || vec2.dot(lineNormal, hitNormal) < 0) 168 | return false 169 | 170 | if (contact) { 171 | const deltaX = delta[0] * hitTime + (PADDING * hitNormal[0]) 172 | const deltaY = delta[1] * hitTime + (PADDING * hitNormal[1]) 173 | vec2.set(contact.delta, deltaX, deltaY) 174 | vec2.add(contact.position, aabb.position, contact.delta) 175 | vec2.copy(contact.normal, hitNormal) 176 | contact.collider = line 177 | contact.time = hitTime 178 | } 179 | 180 | return true 181 | } 182 | -------------------------------------------------------------------------------- /src/aabb-segments-sweep1-indexed.js: -------------------------------------------------------------------------------- 1 | // based on https://gamedev.stackexchange.com/questions/29479/swept-aabb-vs-line-segment-2d 2 | import aabbSegmentSweep1 from './aabb-segment-sweep1.js' 3 | import contact from './contact.js' 4 | import copyContact from './contact-copy.js' 5 | import { vec2 } from 'gl-matrix' 6 | 7 | 8 | const _tmpContact = contact() 9 | 10 | 11 | export default function aabbSegmentSweep1Indexed (lines, indices, lineCount, aabb, delta, contact) { 12 | 13 | let colliderIndex = -1 14 | let resHitTime 15 | 16 | for (let i=0; i= 0 32 | } 33 | -------------------------------------------------------------------------------- /src/cone-point-overlap.js: -------------------------------------------------------------------------------- 1 | import { toRadians } from '@footgun/math-gap' 2 | import { vec2 } from 'gl-matrix' 3 | 4 | 5 | // static temp variables to avoid creating new ones each invocation 6 | const v1 = vec2.create(), v2 = vec2.create() 7 | 8 | 9 | /* 10 | Determine if a point is within a cone 11 | 12 | @param Vec2 conePosition position of cone origin 13 | @param number coneRotation observer rotation in radians 14 | @param number coneFieldOfView angle of cone in degrees 15 | @param number coneMinDistance min distance from cone position 16 | @param number coneMaxDistance max distance from cone position 17 | @param Object point vec2 position of item being checked against cone 18 | @return Bool true if point is in cone, false otherwise 19 | */ 20 | export default function conePointOverlap (conePosition, coneRotation, coneFieldOfView, coneMinDistance, coneMaxDistance, point) { 21 | const dist = vec2.distance(conePosition, point) 22 | if (dist > coneMaxDistance) 23 | return false 24 | 25 | if (dist < coneMinDistance) 26 | return false 27 | 28 | if (coneFieldOfView >= 360) 29 | return true 30 | 31 | vec2.subtract(v1, point, conePosition) 32 | vec2.set(v2, Math.cos(coneRotation), Math.sin(coneRotation)) 33 | const angle = vec2.angle(v1, v2) 34 | return angle <= toRadians(coneFieldOfView/2) 35 | } 36 | -------------------------------------------------------------------------------- /src/contact-copy.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix' 2 | 3 | 4 | export default function copyContact (out, contact) { 5 | out.time = contact.time 6 | out.collider = contact.collider 7 | vec2.copy(out.position, contact.position) 8 | vec2.copy(out.delta, contact.delta) 9 | vec2.copy(out.normal, contact.normal) 10 | return out 11 | } 12 | -------------------------------------------------------------------------------- /src/contact.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix' 2 | 3 | 4 | /* 5 | information related to a physics system collision. 6 | position is the point of contact between the two objects (or an estimation of it, in some sweep tests). 7 | normal is the surface normal at the point of contact. 8 | delta is the overlap between the two objects, and is a vector that can be added to the colliding object’s position to move it back to a non-colliding state. 9 | time is a fraction from 0 to 1 indicating how far along the line the collision occurred. (This is the t value for the line equation L(t) = A + t * (B - A)) 10 | */ 11 | export default function contact () { 12 | return { 13 | // for segments-segment-overlap and segments-sphere-sweep1 this is set to the index in the array of line segments passed into the collision routine 14 | collider : null, 15 | position : vec2.create(), 16 | delta : vec2.create(), 17 | normal : vec2.create(), 18 | time : 0 19 | } 20 | } -------------------------------------------------------------------------------- /src/cpa-time.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix' 2 | 3 | 4 | const EPSILON = 0.00000001 5 | 6 | const v1 = vec2.create() 7 | const v2 = vec2.create() 8 | const dv = vec2.create() 9 | const w0 = vec2.create() 10 | 11 | // compute the time of closest point of approach (CPA) for two tracks 12 | // a track consists of a starting point and a delta vector 13 | // 14 | // Input: two tracks Tr1 and Tr2 15 | // Return: the time at which the two tracks are closest from 0..1 16 | export default function cpaTime (Tr1, Tr2) { 17 | vec2.subtract(v1, Tr1[1], Tr1[0]) 18 | vec2.subtract(v2, Tr2[1], Tr2[0]) 19 | 20 | vec2.subtract(dv, v1, v2) 21 | 22 | const dv2 = vec2.dot(dv, dv) 23 | 24 | if (dv2 < EPSILON) // the tracks are almost parallel 25 | return 0.0; // any time is ok. Use time 0. 26 | 27 | vec2.subtract(w0, Tr1[0], Tr2[0]) 28 | 29 | //Vector w0 = Tr1.P0 - Tr2.P0; 30 | 31 | const cpatime = -vec2.dot(w0, dv) / dv2 32 | 33 | //float cpatime = -dot(w0,dv) / dv2; 34 | 35 | return cpatime; // time of CPA 36 | } 37 | -------------------------------------------------------------------------------- /src/get-lowest-root.js: -------------------------------------------------------------------------------- 1 | // TODO: Don't like the duality of returning a null or float, probably doesn't optimize nicely 2 | export default function getLowestRoot (a, b, c, maxR) { 3 | // check if a solution exists 4 | const det = b * b - 4.0 * a * c 5 | 6 | // if determinant is negative it means no solutions. 7 | if (det < 0) 8 | return null 9 | 10 | // calculate the two roots: (if determinant == 0 then 11 | // x1==x2 but let’s disregard that slight optimization) 12 | const sqrtDet = Math.sqrt(det) 13 | let r1 = (-b - sqrtDet) / (2.0*a) 14 | let r2 = (-b + sqrtDet) / (2.0*a) 15 | 16 | // sort so x1 <= x2 17 | if (r1 > r2) { 18 | const tmp = r2 19 | r2 = r1 20 | r1 = tmp 21 | } 22 | 23 | // get lowest root: 24 | if (r1 > 0 && r1 < maxR) 25 | return r1 26 | 27 | // it is possible that we want x2 - this can happen if x1 < 0 28 | if (r2 > 0 && r2 < maxR) 29 | return r2 30 | 31 | // No (valid) solutions 32 | return null 33 | } 34 | -------------------------------------------------------------------------------- /src/point-polygon-overlap.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix' 2 | 3 | 4 | const DEFAULT_OFFSET = vec2.create() 5 | 6 | 7 | // determine if a point is inside of a polygon 8 | export default function pointPolygonOverlap (point, segments, polygonOffset=DEFAULT_OFFSET) { 9 | 10 | for (const segment of segments) { 11 | const dx = segment[1][0] - segment[0][0] 12 | const dy = segment[1][1] - segment[0][1] 13 | const startX = segment[0][0] + polygonOffset[0] 14 | const startY = segment[0][1] + polygonOffset[1] 15 | 16 | if (dx === 0) { 17 | const dir = Math.sign(dy) 18 | if ((point[0] - startX) * dir < 0) 19 | return false 20 | } else { 21 | const dir = Math.sign(dx) 22 | const slope = dy / dx 23 | if ((point[1] - (startY + slope * (point[0] - startX))) * dir > 0) 24 | return false 25 | } 26 | } 27 | 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /src/ray-sphere-overlap.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix' 2 | 3 | 4 | // from http://paulbourke.net/geometry/circlesphere/raysphere.c 5 | 6 | const EPSILON = 1e-8 7 | 8 | const dp = [ 0, 0 ] 9 | 10 | 11 | // Calculate the intersection of an infinite ray and a sphere 12 | // When collision occurs, there are potentially two points of intersection given by 13 | // p = p1 + contact.mu1 (p2 - p1) 14 | // p = p1 + contact.mu2 (p2 - p1) 15 | // 16 | // @param Object p1 vec2 1 point of the segment on the infinite ray 17 | // @param Object p2 vec2 1 point of the segment on the infinite ray 18 | // @param Object sc vec2 sphere center point 19 | // @param Number r sphere radius 20 | // @param Object contact filled in when a collision occurs. e.g., { mu1: 0.34885, mu2: -0.233891 } 21 | // @returns Boolean true if there's an intersection, false otherwise 22 | export default function raySphereOverlap (p1, p2, sc, r, contact) { 23 | 24 | vec2.subtract(dp, p2, p1) 25 | 26 | const a = dp[0] * dp[0] + dp[1] * dp[1] 27 | const b = 2 * (dp[0] * (p1[0] - sc[0]) + dp[1] * (p1[1] - sc[1])) 28 | let c = sc[0] * sc[0] + sc[1] * sc[1] 29 | c += p1[0] * p1[0] + p1[1] * p1[1] 30 | c -= 2 * (sc[0] * p1[0] + sc[1] * p1[1]) 31 | c -= r * r; 32 | 33 | const bb4ac = b * b - 4 * a * c 34 | 35 | if (Math.abs(a) < EPSILON || bb4ac < 0) { 36 | contact.mu1 = 0 37 | contact.mu2 = 0 38 | return false 39 | } 40 | 41 | contact.mu1 = (-b + Math.sqrt(bb4ac)) / (2 * a) 42 | contact.mu2 = (-b - Math.sqrt(bb4ac)) / (2 * a) 43 | 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /src/segment-normal.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix' 2 | 3 | 4 | export default function segmentNormal (out, pos1, pos2) { 5 | let dx = pos2[0] - pos1[0] 6 | let dy = pos2[1] - pos1[1] 7 | 8 | if (dx !== 0) 9 | dx = -dx 10 | 11 | vec2.set(out, dy, dx) // normals: [ -dy, dx ] [ dy, -dx ] 12 | vec2.normalize(out, out) 13 | 14 | return out 15 | } 16 | -------------------------------------------------------------------------------- /src/segment-point-overlap.js: -------------------------------------------------------------------------------- 1 | import dist from 'point-to-segment-2d' 2 | 3 | 4 | // p - point 5 | // t0 - start point of segment 6 | // t1 - end point of segment 7 | // return boolean indicating if p is on the segment 8 | export default function segmentPointOverlap (p, t0, t1) { 9 | return dist(p, t0, t1) <= 0 10 | } 11 | -------------------------------------------------------------------------------- /src/segment-segment-overlap.js: -------------------------------------------------------------------------------- 1 | import segseg from 'segseg' 2 | 3 | 4 | export default function segmentSegmentOverlap (pos1, pos2, pos3, pos4, intersection) { 5 | return segseg(intersection, pos1, pos2, pos3, pos4) === 1 6 | } 7 | -------------------------------------------------------------------------------- /src/segment-sphere-overlap.js: -------------------------------------------------------------------------------- 1 | import raySphereOverlap from './ray-sphere-overlap.js' 2 | import { vec2 } from 'gl-matrix' 3 | 4 | 5 | // Calculate the intersection of a line segment and a sphere 6 | // When collision occurs, there are potentially zero to two points of intersection given by 7 | // p = p1 + contact.mu1 (p2 - p1) 8 | // p = p1 + contact.mu2 (p2 - p1) 9 | // 10 | // @param Object p1 vec2 1 point of the segment 11 | // @param Object p2 vec2 1 point of the segment 12 | // @param Object sc vec2 sphere center point 13 | // @param Number r sphere radius 14 | // @param Object contact filled in when a collision occurs. e.g., { intersectionCount: 2, mu1: 0.34885, mu2: -0.233891 } 15 | // @returns Boolean true if there's an intersection, false otherwise 16 | export default function segmentSphereOverlap (p1, p2, sc, r, contact) { 17 | 18 | const hit = raySphereOverlap(p1, p2, sc, r, contact) 19 | 20 | contact.intersectionCount = 0 21 | 22 | if (hit) { 23 | if (contact.mu1 >= 0 && contact.mu1 <= 1) 24 | contact.intersectionCount++ 25 | else 26 | contact.mu1 = NaN 27 | 28 | if (contact.mu2 >= 0 && contact.mu2 <= 1) 29 | contact.intersectionCount++ 30 | else 31 | contact.mu2 = NaN 32 | } 33 | 34 | return contact.intersectionCount > 0 35 | } 36 | -------------------------------------------------------------------------------- /src/segments-ellipsoid-sweep1-indexed.js: -------------------------------------------------------------------------------- 1 | import TraceInfo from './TraceInfo.js' 2 | import toji from './toji-tris.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | const traceInfo = new TraceInfo() 7 | const p0 = vec2.create() 8 | const p1 = vec2.create() 9 | const endPoint = vec2.create() 10 | 11 | 12 | export default function segmentsEllipsoid1Indexed (lines, indices, lineCount, position, ellipsoid, delta, contact) { 13 | 14 | vec2.add(endPoint, position, delta) 15 | 16 | const radius = 1 17 | traceInfo.resetTrace(position, endPoint, radius) 18 | 19 | let collider = -1 20 | for (let i=0; i < lineCount; i++) { 21 | const idx = indices[i] 22 | const line = lines[idx] 23 | const oldT = traceInfo.t 24 | 25 | vec2.divide(p0, line[0], ellipsoid) 26 | vec2.divide(p1, line[1], ellipsoid) 27 | 28 | toji.traceSphereTriangle(p0, p1, traceInfo) 29 | if (traceInfo.collision && oldT !== traceInfo.t) 30 | collider = idx 31 | } 32 | 33 | if (traceInfo.collision) { 34 | contact.time = traceInfo.t 35 | 36 | vec2.copy(contact.position, traceInfo.intersectPoint) 37 | vec2.copy(contact.normal, traceInfo.intersectTriNorm) 38 | 39 | vec2.negate(contact.delta, delta) 40 | vec2.scale(contact.delta, contact.delta, 1-contact.time) 41 | 42 | contact.collider = collider 43 | } 44 | 45 | return traceInfo.collision 46 | } 47 | -------------------------------------------------------------------------------- /src/segments-segment-overlap-indexed.js: -------------------------------------------------------------------------------- 1 | import lineNormal from './segment-normal.js' 2 | import segseg from './segment-segment-overlap.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | const EPSILON = 1e-8 7 | 8 | const isect = vec2.create() // the intersection if there is one 9 | const end = vec2.create() 10 | 11 | 12 | export default function segmentsSegmentOverlapIndexed (lines, indices, lineCount, start, delta, contact) { 13 | let nearest, nearestTime = 0, nearestIdx = -1 14 | 15 | vec2.set(end, start[0] + delta[0], start[1] + delta[1]) 16 | 17 | for (let i=0; i < lineCount; i++) { 18 | const idx = indices[i] 19 | const line = lines[idx] 20 | 21 | if (segseg(start, end, line[0], line[1], isect)) { 22 | const dist = vec2.distance(start, isect) 23 | if (!nearest || dist < nearestTime) { 24 | nearestTime = dist 25 | nearest = line 26 | nearestIdx = idx 27 | } 28 | } 29 | } 30 | 31 | let nearTime = nearestTime / vec2.length(delta) 32 | if (nearTime > 1) 33 | return false 34 | 35 | if (nearTime <= EPSILON) 36 | nearTime = 0 37 | 38 | if (nearest) { 39 | vec2.scaleAndAdd(contact.position, start, delta, nearTime) 40 | contact.collider = nearestIdx 41 | 42 | // determine which normal is on the right side of the plane for the intersection 43 | lineNormal(contact.normal, nearest[0], nearest[1]) 44 | 45 | // if dot product is less than 0, flip the normal 180 degrees 46 | const dot = vec2.dot(delta, contact.normal) 47 | if (dot > 0) 48 | vec2.negate(contact.normal, contact.normal) 49 | 50 | contact.time = nearTime 51 | } 52 | 53 | return !!nearest 54 | } 55 | -------------------------------------------------------------------------------- /src/segments-segment-overlap.js: -------------------------------------------------------------------------------- 1 | import lineNormal from './segment-normal.js' 2 | import segseg from './segment-segment-overlap.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | const EPSILON = 1e-8 7 | const isect = vec2.create() // the intersection if there is one 8 | const end = vec2.create() 9 | 10 | 11 | export default function segmentsSegmentOverlap (lines, start, delta, contact) { 12 | let nearest, nearestTime = 0, nearestIdx = -1 13 | 14 | vec2.set(end, start[0] + delta[0], start[1] + delta[1]) 15 | 16 | for (let i=0; i < lines.length; i++) { 17 | const line = lines[i]; 18 | if (segseg(start, end, line[0], line[1], isect)) { 19 | const dist = vec2.distance(start, isect) 20 | if (!nearest || dist < nearestTime) { 21 | nearestTime = dist 22 | nearest = line 23 | nearestIdx = i 24 | } 25 | } 26 | } 27 | 28 | let nearTime = nearestTime / vec2.length(delta) 29 | if (nearTime > 1) 30 | return false 31 | 32 | if (nearTime <= EPSILON) 33 | nearTime = 0 34 | 35 | if (nearest) { 36 | vec2.scaleAndAdd(contact.position, start, delta, nearTime) 37 | contact.collider = nearestIdx 38 | 39 | // determine which normal is on the right side of the plane for the intersection 40 | lineNormal(contact.normal, nearest[0], nearest[1]) 41 | 42 | // if dot product is less than 0, flip the normal 180 degrees 43 | const dot = vec2.dot(delta, contact.normal) 44 | if (dot > 0) 45 | vec2.negate(contact.normal, contact.normal) 46 | 47 | contact.time = nearTime 48 | } 49 | 50 | return !!nearest 51 | } 52 | -------------------------------------------------------------------------------- /src/segments-sphere-sweep1-indexed.js: -------------------------------------------------------------------------------- 1 | import TraceInfo from './TraceInfo.js' 2 | import toji from './toji-tris.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | const traceInfo = new TraceInfo() 7 | const endPoint = vec2.create() 8 | 9 | 10 | export default function segmentsSphereSweep1Indexed (lines, indices, lineCount, position, radius, delta, contact) { 11 | 12 | vec2.add(endPoint, position, delta) 13 | 14 | traceInfo.resetTrace(position, endPoint, radius) 15 | 16 | let collider = -1 17 | for (let i=0; i < lineCount; i++) { 18 | const idx = indices[i] 19 | const line = lines[idx] 20 | const oldT = traceInfo.t 21 | toji.traceSphereTriangle(line[0], line[1], traceInfo) 22 | if (traceInfo.collision && oldT !== traceInfo.t) 23 | collider = idx 24 | } 25 | 26 | if (traceInfo.collision) { 27 | contact.time = traceInfo.t 28 | 29 | vec2.copy(contact.position, traceInfo.intersectPoint) 30 | vec2.copy(contact.normal, traceInfo.intersectTriNorm) 31 | 32 | vec2.negate(contact.delta, delta) 33 | vec2.scale(contact.delta, contact.delta, 1-contact.time) 34 | 35 | contact.collider = collider 36 | } 37 | 38 | return traceInfo.collision 39 | } 40 | -------------------------------------------------------------------------------- /src/segments-sphere-sweep1.js: -------------------------------------------------------------------------------- 1 | import TraceInfo from './TraceInfo.js' 2 | import toji from './toji-tris.js' 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | const traceInfo = new TraceInfo() 7 | const endPoint = vec2.create() 8 | 9 | 10 | export default function segmentsSphereSweep1 (lines, position, radius, delta, contact) { 11 | 12 | vec2.add(endPoint, position, delta) 13 | 14 | traceInfo.resetTrace(position, endPoint, radius) 15 | 16 | let collider = -1 17 | for (let i=0; i < lines.length; i++) { 18 | const line = lines[i] 19 | const oldT = traceInfo.t 20 | toji.traceSphereTriangle(line[0], line[1], traceInfo) 21 | if (traceInfo.collision && oldT !== traceInfo.t) 22 | collider = i 23 | } 24 | 25 | if (traceInfo.collision) { 26 | contact.time = traceInfo.t 27 | 28 | vec2.copy(contact.position, traceInfo.intersectPoint) 29 | vec2.copy(contact.normal, traceInfo.intersectTriNorm) 30 | 31 | vec2.negate(contact.delta, delta) 32 | vec2.scale(contact.delta, contact.delta, 1-contact.time) 33 | 34 | contact.collider = collider 35 | } 36 | 37 | return traceInfo.collision 38 | } 39 | -------------------------------------------------------------------------------- /src/segseg-closest.js: -------------------------------------------------------------------------------- 1 | // determine the closest point between 2 line segments. 2 | // from http://geomalgorithms.com/a07-_distance.html#dist3D_Segment_to_Segment 3 | import { vec2 } from 'gl-matrix' 4 | 5 | 6 | const SMALL_NUM = 0.00000001 // anything that avoids division overflow 7 | 8 | const u = vec2.create() 9 | const v = vec2.create() 10 | const w = vec2.create() 11 | const s1tmp = vec2.create() 12 | const s2tmp = vec2.create() 13 | const diff = vec2.create() 14 | const dP = vec2.create() 15 | 16 | 17 | // get the 2D minimum distance between 2 segments 18 | // Input: two 2D line segments S1 and S2 19 | // Return: the shortest distance between S1 and S2 20 | 21 | export default function segSegClosest (S1, S2, detail) { 22 | vec2.subtract(u, S1[1], S1[0]) 23 | vec2.subtract(v, S2[1], S2[0]) 24 | vec2.subtract(w, S1[0], S2[0]) 25 | 26 | //Vector u = S1[1] - S1[0]; 27 | //Vector v = S2[1] - S2[0]; 28 | //Vector w = S1[0] - S2[0]; 29 | 30 | 31 | const a = vec2.dot(u, u) 32 | const b = vec2.dot(u, v) 33 | const c = vec2.dot(v, v) 34 | const d = vec2.dot(u, w) 35 | const e = vec2.dot(v, w) 36 | const D = a*c - b*b 37 | 38 | //float a = dot(u,u); // always >= 0 39 | //float b = dot(u,v); 40 | //float c = dot(v,v); // always >= 0 41 | //float d = dot(u,w); 42 | //float e = dot(v,w); 43 | //float D = a*c - b*b; // always >= 0 44 | 45 | 46 | let sN, sD = D 47 | let tN, tD = D 48 | 49 | //float sc, sN, sD = D; // sc = sN / sD, default sD = D >= 0 50 | //float tc, tN, tD = D; // tc = tN / tD, default tD = D >= 0 51 | 52 | 53 | // compute the line parameters of the two closest points 54 | if (D < SMALL_NUM) { // the lines are almost parallel 55 | sN = 0.0; // force using point P0 on segment S1 56 | sD = 1.0; // to prevent possible division by 0.0 later 57 | tN = e; 58 | tD = c; 59 | } 60 | else { // get the closest points on the infinite lines 61 | sN = (b*e - c*d); 62 | tN = (a*e - b*d); 63 | if (sN < 0.0) { // sc < 0 => the s=0 edge is visible 64 | sN = 0.0; 65 | tN = e; 66 | tD = c; 67 | } 68 | else if (sN > sD) { // sc > 1 => the s=1 edge is visible 69 | sN = sD; 70 | tN = e + b; 71 | tD = c; 72 | } 73 | } 74 | 75 | if (tN < 0.0) { // tc < 0 => the t=0 edge is visible 76 | tN = 0.0; 77 | // recompute sc for this edge 78 | if (-d < 0.0) 79 | sN = 0.0; 80 | else if (-d > a) 81 | sN = sD; 82 | else { 83 | sN = -d; 84 | sD = a; 85 | } 86 | } 87 | else if (tN > tD) { // tc > 1 => the t=1 edge is visible 88 | tN = tD; 89 | // recompute sc for this edge 90 | if ((-d + b) < 0.0) 91 | sN = 0; 92 | else if ((-d + b) > a) 93 | sN = sD; 94 | else { 95 | sN = (-d + b); 96 | sD = a; 97 | } 98 | } 99 | 100 | // do the division to get sc and tc 101 | // these are the distances on the lines from 0..1 where the lines are closest 102 | // sc is for line 1 (S1) and tc is for line 2 (S2) 103 | const sc = (Math.abs(sN) < SMALL_NUM ? 0.0 : sN / sD) 104 | const tc = (Math.abs(tN) < SMALL_NUM ? 0.0 : tN / tD) 105 | 106 | if (sc > 1) 107 | console.warn('WARNING: sc > 1:', sc) 108 | 109 | if (tc > 1) 110 | console.warn('WARNING: tc > 1:', tc) 111 | 112 | vec2.scale(s1tmp, u, sc) 113 | 114 | vec2.scale(s2tmp, v, tc) 115 | 116 | vec2.subtract(diff, s1tmp, s2tmp) 117 | 118 | vec2.add(dP, w, diff) 119 | 120 | const closestDistance = vec2.length(dP) 121 | 122 | if (detail) { 123 | detail.sc = sc 124 | detail.tc = tc 125 | detail.closestDistance = closestDistance 126 | } 127 | 128 | // get the difference of the two closest points 129 | //Vector dP = w + (sc * u) - (tc * v); // = S1(sc) - S2(tc) 130 | 131 | return closestDistance 132 | 133 | //return norm(dP); // return the closest distance sqrt(dot(v,v)) 134 | } 135 | -------------------------------------------------------------------------------- /src/sphere-point-overlap.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix' 2 | 3 | 4 | // determine if a point is inside a sphere 5 | export default function spherePointOverlap (point, centerA, radiusA) { 6 | return vec2.distance(point, centerA) <= radiusA 7 | } 8 | -------------------------------------------------------------------------------- /src/sphere-sphere-collision-response.js: -------------------------------------------------------------------------------- 1 | import contact from './contact.js' 2 | import sphereOverlap from './sphere-sphere-overlap.js' 3 | import { vec2 } from 'wgpu-matrix' 4 | 5 | 6 | // TODO: make this function actually work. Right now the data structure is hardcoded to 7 | // use crossroads entity data structures that have no analog in collision-2d (rigidBody, transform components) 8 | /* 9 | const mtd = vec2.create() 10 | const scaled = vec2.create() 11 | const v = vec2.create() 12 | const normalized = vec2.create() 13 | const impulse = vec2.create() 14 | const tmpContact = contact() 15 | 16 | 17 | // collide 2 spherical rigid bodies, updating their positions and velocities 18 | export default function sphereSphereCollisionResponse (sphere1, sphere2, restitution=0.85) { 19 | const body1 = sphere1.rigidBody 20 | const body2 = sphere2.rigidBody 21 | 22 | const overlap = sphereOverlap(sphere1.transform.position, 23 | body1.radius, 24 | sphere2.transform.position, 25 | body2.radius, 26 | tmpContact) 27 | 28 | if (!overlap) 29 | return 30 | 31 | vec2.copy(tmpContact.delta, mtd) 32 | 33 | // resolve intersection 34 | const im1 = (body1.mass !== 0) ? 1 / body1.mass : 1 / 500 35 | const im2 = (body2.mass !== 0) ? 1 / body2.mass : 1 / 500 36 | 37 | // push-pull them apart 38 | // 0 mass means static body unmoved by collisions (doors, machines, etc.) 39 | if (body1.mass !== 0) 40 | vec2.addScaled(sphere1.transform.position, mtd, im1 / (im1 + im2), sphere1.transform.position) 41 | 42 | if (body2.mass !== 0) { 43 | vec2.scale(mtd, im2 / (im1 + im2), scaled) 44 | vec2.subtract(sphere2.transform.position, scaled, sphere2.transform.position) 45 | } 46 | 47 | // impact speed 48 | vec2.subtract(body1.velocity, body2.velocity, v) 49 | vec2.normalize(mtd, normalized) 50 | const vn = vec2.dot(v, normalized) 51 | 52 | // sphere intersecting but moving away from each other already 53 | if (vn > 0.0) 54 | return 55 | 56 | // collision impulse 57 | const i = (-(1.0 + restitution) * vn) / (im1 + im2) 58 | vec2.scale(mtd, i, impulse) 59 | 60 | // change in momentum 61 | if (body1.mass !== 0) 62 | vec2.addScaled(body1.velocity, impulse, im1, body1.velocity) 63 | 64 | if (body2.mass !== 0) 65 | vec2.addScaled(body2.velocity, impulse, -im2, body2.velocity) 66 | } 67 | */ 68 | -------------------------------------------------------------------------------- /src/sphere-sphere-overlap.js: -------------------------------------------------------------------------------- 1 | import contact from './contact.js' 2 | import { vec2 } from 'gl-matrix' 3 | 4 | 5 | const _delta = vec2.create() 6 | const _mtd = vec2.create() 7 | 8 | 9 | // determine if 2 spheres overlap, and provide contact details 10 | export default function sphereSphereOverlap (centerA, radiusA, centerB, radiusB, contact) { 11 | if (!contact) 12 | return vec2.squaredDistance(centerA, centerB) <= ((radiusA + radiusB) ** 2) 13 | 14 | vec2.subtract(_delta, centerA, centerB) 15 | 16 | const r = radiusA + radiusB 17 | 18 | const dist2 = vec2.dot(_delta, _delta) 19 | 20 | if (dist2 > r*r) 21 | return false // they aren't colliding 22 | 23 | let d = vec2.length(_delta) 24 | 25 | if (d !== 0.0) { 26 | // minimum translation distance to push balls apart after intersecting 27 | vec2.scale(_mtd, _delta, ((radiusA + radiusB)-d)/d) 28 | } else { 29 | // Special case. spheres are exactly on top of each other. Prevent divide by zero. 30 | d = r - 1.0 31 | vec2.set(_delta, r, 0.0) 32 | vec2.scale(_mtd, _delta, (r-d)/d) 33 | } 34 | 35 | // delta is the overlap between the two spheres, and is a vector that can be added to 36 | // sphere A’s position to move them into a non-colliding state. 37 | vec2.copy(contact.delta, _mtd) 38 | 39 | // position is the point of contact of these 2 spheres (assuming they no longer penetrate) 40 | vec2.normalize(contact.position, _delta) 41 | vec2.scaleAndAdd(contact.position, centerA, contact.position, -radiusA) 42 | 43 | return true 44 | } 45 | -------------------------------------------------------------------------------- /src/sphere-sphere-sweep2.js: -------------------------------------------------------------------------------- 1 | import getLowestRoot from './get-lowest-root.js' 2 | import { vec2 } from 'gl-matrix' 3 | 4 | 5 | // from https://web.archive.org/web/20100629145557/http://www.gamasutra.com/view/feature/3383/simple_intersection_tests_for_games.php?page=2 6 | 7 | // tmp vars to avoid garbage collection 8 | const va = vec2.create() 9 | const vb = vec2.create() 10 | const AB = vec2.create() 11 | const vab = vec2.create() 12 | 13 | 14 | /* 15 | const SCALAR ra, //radius of sphere A 16 | const VECTOR& A0, //previous position of sphere A 17 | const VECTOR& A1, //current position of sphere A 18 | const SCALAR rb, //radius of sphere B 19 | const VECTOR& B0, //previous position of sphere B 20 | const VECTOR& B1, //current position of sphere B 21 | SCALAR& u0, //normalized time of first collision 22 | SCALAR& u1 //normalized time of second collision 23 | */ 24 | export default function sphereSphereSweep2 (ra, A0, A1, rb, B0, B1, contact) { 25 | 26 | vec2.subtract(va, A1, A0) 27 | 28 | vec2.subtract(vb, B1, B0) 29 | 30 | vec2.subtract(AB, B0, A0) 31 | 32 | vec2.subtract(vab, vb, va) // relative velocity (in normalized time) 33 | 34 | 35 | const rab = ra + rb 36 | 37 | const a = vec2.dot(vab, vab) // u*u coefficient 38 | 39 | 40 | const b = 2 * vec2.dot(vab, AB) // u coefficient 41 | 42 | const c = vec2.dot(AB, AB) - rab*rab // constant term 43 | 44 | // check if they're currently overlapping 45 | if (vec2.dot(AB, AB) <= rab*rab) { 46 | const t = 0 47 | fillContactDeets(ra, A0, A1, rb, B0, B1, t, contact) 48 | return true 49 | } 50 | 51 | // check if they hit each other during the frame 52 | const maxVal = 1 53 | const t = getLowestRoot(a, b, c, maxVal) 54 | 55 | if (t !== null) { 56 | fillContactDeets(ra, A0, A1, rb, B0, B1, t, contact) 57 | return true 58 | } 59 | 60 | return false 61 | } 62 | 63 | 64 | const _delta = vec2.create() 65 | const _pos1 = vec2.create() 66 | const _pos2 = vec2.create() 67 | 68 | function fillContactDeets (ra, A0, A1, rb, B0, B1, t, contact) { 69 | contact.time = t 70 | 71 | // final sphereA position 72 | vec2.subtract(_delta, A1, A0) 73 | vec2.scaleAndAdd(_pos1, A0, _delta, contact.time) 74 | 75 | // final sphereB position 76 | vec2.subtract(_delta, B1, B0) 77 | vec2.scaleAndAdd(_pos2, B0, _delta, contact.time) 78 | 79 | vec2.subtract(contact.position, _pos1, _pos2) 80 | vec2.normalize(contact.position, contact.position) 81 | vec2.scaleAndAdd(contact.position, _pos2, contact.position, rb) 82 | 83 | vec2.subtract(contact.normal, contact.position, _pos2) 84 | vec2.normalize(contact.normal, contact.normal) 85 | } 86 | 87 | -------------------------------------------------------------------------------- /src/toji-tris.js: -------------------------------------------------------------------------------- 1 | // from https://github.com/kevzettler/gl-swept-sphere-triangle 2 | import clamp from 'clamp' 3 | import TraceInfo from './TraceInfo.js' 4 | import getLowestRoot from './get-lowest-root.js' 5 | import lineNormal from './segment-normal.js' 6 | import segmentPointOverlap from './segment-point-overlap.js' 7 | import { vec2 } from 'gl-matrix' 8 | 9 | 10 | const ta = vec2.create() 11 | const tb = vec2.create() 12 | 13 | const norm = vec2.create() 14 | 15 | const v = vec2.create() 16 | const edge = vec2.create() 17 | 18 | const planeIntersect = vec2.create() 19 | 20 | 21 | function testVertex (p, velSqrLen, t, start, vel, trace) { 22 | vec2.subtract(v, start, p) 23 | const b = 2.0 * vec2.dot(vel, v) 24 | const c = vec2.squaredLength(v) - 1.0 25 | const newT = getLowestRoot(velSqrLen, b, c, t) 26 | if (newT !== null) { 27 | trace.setCollision(newT, p) 28 | return newT 29 | } 30 | return t 31 | } 32 | 33 | 34 | function testEdge (pa, pb, velSqrLen, t, start, vel, trace) { 35 | vec2.subtract(edge, pb, pa) 36 | vec2.subtract(v, pa, start) 37 | 38 | const edgeSqrLen = vec2.squaredLength(edge) 39 | const edgeDotVel = vec2.dot(edge, vel) 40 | const edgeDotSphereVert = vec2.dot(edge, v) 41 | 42 | const a = edgeSqrLen*-velSqrLen + edgeDotVel*edgeDotVel 43 | const b = edgeSqrLen*(2.0*vec2.dot(vel, v))-2.0*edgeDotVel*edgeDotSphereVert 44 | const c = edgeSqrLen*(1.0-vec2.squaredLength(v))+edgeDotSphereVert*edgeDotSphereVert 45 | 46 | // Check for intersection against infinite line 47 | const newT = getLowestRoot(a, b, c, t) 48 | if (newT !== null && newT < trace.t) { 49 | // Check if intersection against the line segment: 50 | const f = (edgeDotVel*newT-edgeDotSphereVert)/edgeSqrLen 51 | if (f >= 0.0 && f <= 1.0) { 52 | vec2.scale(v, edge, f) 53 | vec2.add(v, pa, v) 54 | trace.setCollision(newT, v) 55 | return newT 56 | } 57 | } 58 | return t 59 | } 60 | 61 | 62 | /** 63 | * @param {vec2} a First line vertex 64 | * @param {vec2} b Second line vertex 65 | * @param {TraceInfo} trace TraceInfo containing the sphere path to trace 66 | */ 67 | function traceSphereTriangle (a, b, trace) { 68 | vec2.copy(trace.tmpTri[0], a) 69 | vec2.copy(trace.tmpTri[0], b) 70 | 71 | const invRadius = trace.invRadius 72 | const vel = trace.scaledVel 73 | const start = trace.scaledStart 74 | 75 | // Scale the triangle points so that we're colliding against a unit-radius sphere. 76 | vec2.scale(ta, a, invRadius) 77 | vec2.scale(tb, b, invRadius) 78 | 79 | lineNormal(norm, ta, tb) 80 | 81 | vec2.copy(trace.tmpTriNorm, norm) 82 | const planeD = -(norm[0]*ta[0]+norm[1] *ta[1]) 83 | 84 | // Colliding against the backface of the triangle 85 | if (vec2.dot(norm, trace.normVel) >= 0) { 86 | // Two choices at this point: 87 | 88 | // 1) Negate the normal so that it always points towards the start point 89 | // This feels kludgy, but I'm not sure if there's a better alternative 90 | /*vec2.negate(norm, norm) 91 | planeD = -planeD*/ 92 | 93 | // 2) Or allow it to pass through 94 | return 95 | } 96 | 97 | // Get interval of plane intersection: 98 | let t0, t1 99 | let embedded = false 100 | 101 | // Calculate the signed distance from sphere 102 | // position to triangle plane 103 | const distToPlane = vec2.dot(start, norm) + planeD 104 | 105 | // cache this as we’re going to use it a few times below: 106 | const normDotVel = vec2.dot(norm, vel) 107 | 108 | if (normDotVel === 0.0) { 109 | // Sphere is travelling parrallel to the plane: 110 | if (Math.abs(distToPlane) >= 1.0) { 111 | // Sphere is not embedded in plane, No collision possible 112 | return 113 | } else { 114 | // Sphere is completely embedded in plane. 115 | // It intersects in the whole range [0..1] 116 | embedded = true 117 | t0 = 0.0 118 | t1 = 1.0 119 | } 120 | } else { 121 | 122 | // Calculate intersection interval: 123 | t0 = (-1.0-distToPlane) / normDotVel 124 | t1 = ( 1.0-distToPlane) / normDotVel 125 | // Swap so t0 < t1 126 | if (t0 > t1) { 127 | const temp = t1 128 | t1 = t0 129 | t0 = temp 130 | } 131 | // Check that at least one result is within range: 132 | if (t0 > 1.0 || t1 < 0.0) { 133 | // No collision possible 134 | return 135 | } 136 | 137 | t0 = clamp(t0, 0.0, 1.0) 138 | t1 = clamp(t1, 0.0, 1.0) 139 | } 140 | 141 | // If the closest possible collision point is further away 142 | // than an already detected collision then there's no point 143 | // in testing further. 144 | // 145 | // this is a cheaper way to sort time of intersections, by only 146 | // keeping the closest one. 147 | if (t0 >= trace.t) 148 | return 149 | 150 | // t0 and t1 now represent the range of the sphere movement 151 | // during which it intersects with the triangle plane. 152 | // Collisions cannot happen outside that range. 153 | 154 | // Check for collision againt the triangle face: 155 | if (!embedded) { 156 | // Calculate the intersection point with the plane 157 | vec2.subtract(planeIntersect, start, norm) 158 | vec2.scale(v, vel, t0) 159 | vec2.add(planeIntersect, v, planeIntersect) 160 | 161 | // Is that point inside the triangle? 162 | if (segmentPointOverlap(planeIntersect, ta, tb)) { 163 | trace.setCollision(t0, planeIntersect) 164 | // Collisions against the face will always be closer than vertex or edge collisions 165 | // so we can stop checking now. 166 | return 167 | } 168 | } 169 | 170 | const velSqrLen = vec2.squaredLength(vel) 171 | let t = trace.t 172 | 173 | // Check for collision againt the triangle vertices: 174 | t = testVertex(ta, velSqrLen, t, start, vel, trace) 175 | t = testVertex(tb, velSqrLen, t, start, vel, trace) 176 | 177 | // Check for collision against the triangle edges: 178 | t = testEdge(ta, tb, velSqrLen, t, start, vel, trace) 179 | } 180 | 181 | 182 | export default { TraceInfo, traceSphereTriangle } 183 | -------------------------------------------------------------------------------- /src/triangle-area.js: -------------------------------------------------------------------------------- 1 | // compute the area of a triangle given it's 3 vertices 2 | export default function triangleArea (a, b, c) { 3 | const ax = b[0] - a[0] 4 | const ay = b[1] - a[1] 5 | const bx = c[0] - a[0] 6 | const by = c[1] - a[1] 7 | return bx*ay - ax*by 8 | } 9 | -------------------------------------------------------------------------------- /src/triangle-get-center.js: -------------------------------------------------------------------------------- 1 | export default function getTriangleCenter (out, v0, v1, v2) { 2 | out[0] = (v0[0] + v1[0] + v2[0]) / 3 3 | out[1] = (v0[1] + v1[1] + v2[1]) / 3 4 | return out 5 | } 6 | -------------------------------------------------------------------------------- /src/triangle-point-overlap.js: -------------------------------------------------------------------------------- 1 | import { vec2 } from 'gl-matrix' 2 | 3 | 4 | // static temp variables to avoid creating new ones each invocation 5 | const c = vec2.create() 6 | const b = vec2.create() 7 | const p = vec2.create() 8 | 9 | 10 | /* 11 | Determine if a point is inside a triangle 12 | 13 | https://observablehq.com/@kelleyvanevert/2d-point-in-triangle-test 14 | 15 | @param Array v0, v1, v2 3 points of the triangle expressed as vec2 16 | @param Array point to check for containment within the triangle 17 | @returns bool true if the point is in the triangle, false otherwise 18 | */ 19 | export default function trianglePointOverlap (v0, v1, v2, point) { 20 | // compute vectors 21 | vec2.sub(c, v2, v0) 22 | vec2.sub(b, v1, v0) 23 | vec2.sub(p, point, v0) 24 | 25 | // compute dot products 26 | const cc = vec2.dot(c, c) 27 | const bc = vec2.dot(b, c) 28 | const pc = vec2.dot(c, p) 29 | const bb = vec2.dot(b, b) 30 | const pb = vec2.dot(b, p) 31 | 32 | // compute barycentric coordinates 33 | const denom = cc * bb - bc * bc 34 | const u = (bb*pc - bc*pb) / denom 35 | const v = (cc*pb - bc*pc) / denom 36 | 37 | return (u >= 0) && (v >= 0) && (u + v < 1) 38 | } 39 | -------------------------------------------------------------------------------- /test/_assert.js: -------------------------------------------------------------------------------- 1 | 2 | function equal (a, b) { 3 | if (a !== b) 4 | throw new Error(`${a} is not equal to ${b}`) 5 | } 6 | 7 | 8 | function deepEqual (a, b) { 9 | if (a.length !== b.length) 10 | throw new Error(`${a} is not equal to ${b}`) 11 | 12 | for (let i=0; i < a.length; i++) 13 | if (a[i] !== b[i]) 14 | throw new Error(`${a} is not equal to ${b}`) 15 | } 16 | 17 | 18 | function almostEqual (actual, expected, epsilon=1e-8) { 19 | if (Math.abs(actual - expected) > epsilon) 20 | equal(actual, expected) 21 | } 22 | 23 | 24 | function notNull (value) { 25 | if (value === null) 26 | throw new Error('value is unexpectedly null') 27 | 28 | return value 29 | } 30 | 31 | 32 | export default { almostEqual, notNull, equal, deepEqual } 33 | -------------------------------------------------------------------------------- /test/aabb-point-overlap.js: -------------------------------------------------------------------------------- 1 | import assert from './_assert.js' 2 | import contact from '../src/contact.js' 3 | import intersectPoint from '../src/aabb-point-overlap.js' 4 | 5 | 6 | // should return null when not colliding 7 | let aabb = { 8 | position: [ -8, -8 ], 9 | width: 16, 10 | height: 16 11 | } 12 | 13 | const points = [ 14 | [ -16, -16 ], 15 | [ 0, -16 ], 16 | [ 16, -16 ], 17 | [ 16, 0 ], 18 | [ 16, 16 ], 19 | [ 0, 16 ], 20 | [ -16, 16 ], 21 | [ -16, 0 ] 22 | ] 23 | 24 | const hit = contact() 25 | 26 | points.forEach(point => { 27 | assert.equal(intersectPoint(aabb, point), false); 28 | }) 29 | 30 | 31 | // should return hit when colliding 32 | aabb.position = [ 0, 0 ] 33 | assert.equal(intersectPoint(aabb, [ 4, 4 ]), true) 34 | 35 | 36 | /* 37 | // should set hit pos and normal to nearest edge of box 38 | const aabb = new AABB(new Point(0, 0), new Point(8, 8)); 39 | let hit = assert.notNull(aabb.intersectPoint(new Point(-4, -2))); 40 | assert.almostEqual(hit.pos.x, -8); 41 | assert.almostEqual(hit.pos.y, -2); 42 | assert.almostEqual(hit.delta.x, -4); 43 | assert.almostEqual(hit.delta.y, 0); 44 | assert.almostEqual(hit.normal.x, -1); 45 | assert.almostEqual(hit.normal.y, 0); 46 | 47 | hit = assert.notNull(aabb.intersectPoint(new Point(4, -2))); 48 | assert.almostEqual(hit.pos.x, 8); 49 | assert.almostEqual(hit.pos.y, -2); 50 | assert.almostEqual(hit.delta.x, 4); 51 | assert.almostEqual(hit.delta.y, 0); 52 | assert.almostEqual(hit.normal.x, 1); 53 | assert.almostEqual(hit.normal.y, 0); 54 | 55 | hit = assert.notNull(aabb.intersectPoint(new Point(2, -4))); 56 | assert.almostEqual(hit.pos.x, 2); 57 | assert.almostEqual(hit.pos.y, -8); 58 | assert.almostEqual(hit.delta.x, 0); 59 | assert.almostEqual(hit.delta.y, -4); 60 | assert.almostEqual(hit.normal.x, 0); 61 | assert.almostEqual(hit.normal.y, -1); 62 | 63 | hit = assert.notNull(aabb.intersectPoint(new Point(2, 4))); 64 | assert.almostEqual(hit.pos.x, 2); 65 | assert.almostEqual(hit.pos.y, 8); 66 | assert.almostEqual(hit.delta.x, 0); 67 | assert.almostEqual(hit.delta.y, 4); 68 | assert.almostEqual(hit.normal.x, 0); 69 | assert.almostEqual(hit.normal.y, 1); 70 | */ 71 | -------------------------------------------------------------------------------- /test/aabb-segment-overlap.js: -------------------------------------------------------------------------------- 1 | import assert from './_assert.js' 2 | import aabbSegOverlap from '../src/aabb-segment-overlap.js' 3 | 4 | 5 | const aabb = { 6 | position: [ 2624.20556640625, 1062 ], 7 | width: 10, 8 | height: 24 9 | } 10 | 11 | const seg = [ 12 | [3152, 1072], 13 | [3248, 1072] 14 | ] 15 | 16 | const delta = [ 96, 0 ] 17 | 18 | 19 | { 20 | let paddingX = 0 21 | let paddingY = -2 22 | 23 | assert.equal(aabbSegOverlap(aabb, seg[0], delta, paddingX, paddingY), false) 24 | } 25 | 26 | 27 | { 28 | aabb.position = [ 907.5, 318.5 ] 29 | aabb.width = 15 30 | aabb.height = 35 31 | 32 | 33 | seg[0] = [ 531, 301 ] 34 | delta[0] = 16 35 | 36 | assert.equal(aabbSegOverlap(aabb, seg[0], delta), false) 37 | } 38 | -------------------------------------------------------------------------------- /test/segment-segment-overlap.js: -------------------------------------------------------------------------------- 1 | import t from './_assert.js' 2 | import segseg from '../src/segment-segment-overlap.js' 3 | 4 | 5 | const result = [ 0, 0 ] 6 | 7 | 8 | /* 9 | Basic intersection 10 | 11 | (0, 5) 12 | o 13 | | 14 | (-10, 0) o--------+-------o (10, 0) 15 | | 16 | o 17 | (0, -5) 18 | 19 | */ 20 | 21 | t.equal(segseg([-10, 0], [10, 0], [0, 5], [0, -5], result), true) 22 | t.deepEqual(result, [0, 0]) 23 | 24 | 25 | 26 | /* 27 | Basic intersection 28 | 29 | (5, 5) 30 | o------o (10, 5) 31 | | 32 | | 33 | o 34 | (5, 0) 35 | 36 | */ 37 | t.equal(segseg([ 5, 5], [5, 0], [5, 5], [10, 5], result), true) 38 | t.deepEqual(result, [5,5]) 39 | 40 | 41 | /* 42 | Colinear 43 | (-2, 0) (2, 0) 44 | (-10, 0) o----o--------o-----o (10, 0) 45 | 46 | */ 47 | t.equal(segseg([-10, 0], [10, 0], [-2, 0], [2, 0], result), false) 48 | 49 | 50 | /* 51 | No intersection (parallel) 52 | 53 | (-10, 5) o-------------o (10, 5) 54 | 55 | (-10, 0) o-------------o (10, 0) 56 | 57 | */ 58 | t.equal(segseg([-10, 0], [10, 0], [-10, 5], [10, 5], result), false) 59 | 60 | 61 | /* 62 | No intersection 63 | 64 | (-2, 5) o 65 | \ 66 | (-10, 0) o----o o (2, 0) 67 | (0, 0) 68 | 69 | */ 70 | t.equal(segseg([-10, 0], [0, 0], [-2, 5], [2, 0], result), false) 71 | 72 | 73 | /* 74 | No intersection 75 | 76 | (-2, 5) o 77 | | 78 | o (-2, 1) 79 | (-10, 0) o----o 80 | (0, 0) 81 | 82 | */ 83 | t.equal(segseg([ -10, 0], [0, 0], [-2, 5], [-2, 1], result), false) 84 | 85 | 86 | /* 87 | No intersection 88 | 89 | (-5, 5) o 90 | / 91 | / (-10, 0) 92 | /o-----------o 93 | o (0, 0) 94 | (-25, -5) 95 | 96 | */ 97 | t.equal(segseg([-10, 0], [0, 0], [-5, 5], [-25, -5], result), false) 98 | -------------------------------------------------------------------------------- /test/segments-ellipsoid-sweep1-indexed.js: -------------------------------------------------------------------------------- 1 | import Contact from '../src/contact.js' 2 | import assert from './_assert.js' 3 | import sweep from '../src/segments-ellipsoid-sweep1-indexed.js' 4 | import { vec2 } from 'gl-matrix' 5 | 6 | 7 | const lines = [ 8 | [ [64.0, 128.0], [ 64.0, 0.0 ] ], 9 | [ [0,0], [0, 128] ] 10 | ] 11 | const indices = [ 0, 1 ] 12 | 13 | const ellipsoid = [ 5, 10 ] 14 | const position = [ 32, 64 ] 15 | const delta = [ 120, 0 ] 16 | const lineCount = 2 17 | const contact = Contact() 18 | 19 | 20 | // convert the start and end positions into R3 ellipsoid space 21 | vec2.divide(position, position, ellipsoid) 22 | vec2.divide(delta, delta, ellipsoid) 23 | 24 | const collision = sweep(lines, indices, lineCount, position, ellipsoid, delta, contact) 25 | 26 | // convert the intersection point back to R3 (non-ellipsoid) space 27 | vec2.multiply(contact.position, contact.position, ellipsoid) 28 | 29 | assert.equal(collision, true) 30 | assert.equal(contact.collider, 0) 31 | assert.almostEqual(contact.time, 0.225) 32 | 33 | const EPSILON = 0.000001 34 | assert.almostEqual(contact.position[0], 64, EPSILON) 35 | assert.almostEqual(contact.position[1], 64, EPSILON) 36 | 37 | assert.deepEqual(contact.normal, [ -1, 0 ]) 38 | -------------------------------------------------------------------------------- /test/trace-sphere-triangle.js: -------------------------------------------------------------------------------- 1 | import assert from './_assert.js' 2 | import toji from '../src/toji-tris.js' 3 | 4 | 5 | const start = [ 0, 32 ] 6 | const end = [ 128, 32 ] 7 | const radius = 16 8 | 9 | const ti = new toji.TraceInfo() 10 | ti.resetTrace(start, end, radius) 11 | 12 | const b = [ 128, 0 ] 13 | const a = [ 128, 128 ] 14 | 15 | toji.traceSphereTriangle(a, b, ti); 16 | 17 | assert.equal(ti.collision, true) 18 | assert.equal(ti.t, 0.875) 19 | assert.deepEqual(ti.intersectTriNorm, [ -1, 0 ]) 20 | assert.deepEqual(ti.intersectPoint, [ 128, 32 ]) 21 | 22 | 23 | // when the sphere is 0 distance from a line segment, collide with it 24 | { 25 | const start = [ 112, 64 ] 26 | const end = [ 140, 64 ] 27 | const radius = 16 28 | 29 | const ti = new toji.TraceInfo() 30 | ti.resetTrace(start, end, radius) 31 | 32 | toji.traceSphereTriangle(a, b, ti); 33 | assert.equal(ti.collision, true) 34 | assert.equal(ti.t, 0.0) 35 | } 36 | --------------------------------------------------------------------------------