├── .esdoc.json ├── .gitignore ├── LICENSE ├── README.md ├── collisions.d.ts ├── demo ├── examples │ ├── Stress.mjs │ └── Tank.mjs └── index.mjs ├── docs ├── ast │ └── source │ │ ├── .external-ecmascript.js.json │ │ ├── Collisions.mjs.json │ │ └── modules │ │ ├── BVH.mjs.json │ │ ├── BVHBranch.mjs.json │ │ ├── Body.mjs.json │ │ ├── Circle.mjs.json │ │ ├── Point.mjs.json │ │ ├── Polygon.mjs.json │ │ ├── Result.mjs.json │ │ └── SAT.mjs.json ├── badge.svg ├── class │ └── src │ │ ├── Collisions.mjs~Collisions.html │ │ └── modules │ │ ├── Body.mjs~Body.html │ │ ├── Circle.mjs~Circle.html │ │ ├── Point.mjs~Point.html │ │ ├── Polygon.mjs~Polygon.html │ │ └── Result.mjs~Result.html ├── coverage.json ├── css │ ├── github.css │ ├── identifiers.css │ ├── manual.css │ ├── prettify-tomorrow.css │ ├── search.css │ ├── source.css │ ├── style.css │ └── test.css ├── demo │ ├── index.html │ └── index.js ├── file │ └── src │ │ ├── Collisions.mjs.html │ │ └── modules │ │ ├── BVH.mjs.html │ │ ├── BVHBranch.mjs.html │ │ ├── Body.mjs.html │ │ ├── Circle.mjs.html │ │ ├── Point.mjs.html │ │ ├── Polygon.mjs.html │ │ ├── Result.mjs.html │ │ └── SAT.mjs.html ├── identifiers.html ├── image │ ├── badge.svg │ ├── esdoc-logo-mini-black.png │ ├── esdoc-logo-mini.png │ ├── github.png │ ├── manual-badge.svg │ └── search.png ├── index.html ├── index.json ├── script │ ├── inherited-summary.js │ ├── inner-link.js │ ├── manual.js │ ├── patch-for-local.js │ ├── prettify │ │ ├── Apache-License-2.0.txt │ │ └── prettify.js │ ├── pretty-print.js │ ├── search.js │ ├── search_index.js │ └── test-summary.js └── source.html ├── package-lock.json ├── package.json ├── src ├── Collisions.mjs └── modules │ ├── BVH.mjs │ ├── BVHBranch.mjs │ ├── Body.mjs │ ├── Circle.mjs │ ├── Point.mjs │ ├── Polygon.mjs │ ├── Result.mjs │ └── SAT.mjs └── webpack.config.js /.esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source" : "./src", 3 | "destination" : "./docs", 4 | "includes" : ["\\.js$", "\\.mjs$"], 5 | 6 | "plugins" : [ 7 | { 8 | "name" : "esdoc-standard-plugin", 9 | 10 | "option" : { 11 | "accessor": { 12 | "access" : ["public", "protected"], 13 | "autoPrivate" : true 14 | }, 15 | 16 | "lint": {"enable": false}, 17 | "typeInference": {"enable": false} 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sinova 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 | -------------------------------------------------------------------------------- /collisions.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The base class for bodies used to detect collisions 3 | * @export 4 | * @abstract 5 | * @class Body 6 | */ 7 | export abstract class Body { 8 | x: number; 9 | y: number; 10 | padding: number; 11 | 12 | /** 13 | * Determines if the body is colliding with another body 14 | * @param {Circle|Polygon|Point} target The target body to test against 15 | * @param {Result} [result = null] A Result object on which to store information about the collision 16 | * @param {boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own potential collision heuristic) 17 | * @returns {boolean} 18 | */ 19 | collides(target: Body, result?: Result, aabb?: boolean): boolean; 20 | 21 | /** 22 | * Returns a list of potential collisions 23 | * @returns {Body[]} 24 | */ 25 | potentials(): Body[]; 26 | 27 | /** 28 | * Removes the body from its current collision system 29 | */ 30 | remove(): void; 31 | 32 | /** 33 | * Draws the bodies within the system to a CanvasRenderingContext2D's current path 34 | * @param {CanvasRenderingContext2D} context 35 | */ 36 | draw(context: CanvasRenderingContext2D): void; 37 | } 38 | 39 | /** 40 | * A circle used to detect collisions 41 | * @export 42 | * @class Circle 43 | * @extends {Body} 44 | */ 45 | export class Circle extends Body { 46 | /** 47 | * @constructor 48 | * @param {number} [x = 0] The starting X coordinate 49 | * @param {number} [y = 0] The starting Y coordinate 50 | * @param {number} [radius = 0] The radius 51 | * @param {number} [scale = 1] The scale 52 | * @param {number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 53 | */ 54 | constructor(x?: number, y?: number, radius?: number, scale?: number, padding?: number); 55 | radius: number; 56 | scale: number; 57 | } 58 | 59 | /** 60 | * A polygon used to detect collisions 61 | * @export 62 | * @class Polygon 63 | * @extends {Body} 64 | */ 65 | export class Polygon extends Body { 66 | /** 67 | * @constructor 68 | * @param {number} [x = 0] The starting X coordinate 69 | * @param {number} [y = 0] The starting Y coordinate 70 | * @param {number[][]} [points = []] An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...] 71 | * @param {number} [angle = 0] The starting rotation in radians 72 | * @param {number} [scale_x = 1] The starting scale along the X axis 73 | * @param {number} [scale_y = 1] The starting scale long the Y axis 74 | * @param {number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 75 | */ 76 | constructor(x?: number, y?: number, points?: number[][], angle?: number, scale_x?: number, scale_y?: number, padding?: number); 77 | angle: number; 78 | scale_x: number; 79 | scale_y: number; 80 | } 81 | 82 | /** 83 | * A point used to detect collisions 84 | * @export 85 | * @class Point 86 | * @extends {Body} 87 | */ 88 | export class Point extends Body { 89 | constructor(x?: number, y?: number, padding?: number); 90 | } 91 | 92 | /** 93 | * An object used to collect the detailed results of a collision test 94 | * 95 | * > **Note:** It is highly recommended you recycle the same Result object if possible in order to avoid wasting memory 96 | * @export 97 | * @class Result 98 | */ 99 | export class Result { 100 | collision: boolean; 101 | a: Body; 102 | b: Body; 103 | a_in_b: boolean; 104 | b_in_a: boolean; 105 | overlap: number; 106 | overlap_x: number; 107 | overlap_y: number; 108 | } 109 | 110 | /** 111 | * A collision system used to track bodies in order to improve collision detection performance 112 | * @export 113 | * @class Collisions 114 | */ 115 | export class Collisions { 116 | /** 117 | * Creates a {@link Circle} and inserts it into the collision system 118 | * @param {number} [x = 0] The starting X coordinate 119 | * @param {number} [y = 0] The starting Y coordinate 120 | * @param {number} [radius = 0] The radius 121 | * @param {number} [scale = 1] The scale 122 | * @param {number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 123 | * @returns {Circle} 124 | */ 125 | createCircle(x?: number, y?: number, radius?: number, scale?: number, padding?: number): Circle; 126 | 127 | /** 128 | * Creates a {@link Polygon} and inserts it into the collision system 129 | * @param {number} [x = 0] The starting X coordinate 130 | * @param {number} [y = 0] The starting Y coordinate 131 | * @param {number[][]} [points = []] An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...] 132 | * @param {number} [angle = 0] The starting rotation in radians 133 | * @param {number} [scale_x = 1] The starting scale along the X axis 134 | * @param {number} [scale_y = 1] The starting scale long the Y axis 135 | * @param {number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 136 | * @returns {Polygon} 137 | */ 138 | createPolygon(x?: number, y?: number, points?: number[][], angle?: number, scale_x?: number, scale_y?: number, padding?: number): Polygon; 139 | 140 | /** 141 | * Creates a {@link Point} and inserts it into the collision system 142 | * @param {number} [x = 0] The starting X coordinate 143 | * @param {number} [y = 0] The starting Y coordinate 144 | * @param {number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 145 | * @returns {Point} 146 | */ 147 | createPoint(x?: number, y?: number, padding?: number): Point; 148 | 149 | /** 150 | * Creates a {@link Result} used to collect the detailed results of a collision test 151 | * @returns {Result} 152 | */ 153 | createResult(): Result; 154 | 155 | /** 156 | * Inserts bodies into the collision system 157 | * @param {Body} bodies 158 | * @returns {Collisions} 159 | */ 160 | insert(bodies: Body): Collisions; 161 | 162 | /** 163 | * Removes bodies from the collision system 164 | * @param {Body} bodies 165 | * @returns {Collisions} 166 | */ 167 | remove(bodies: Body): Collisions; 168 | 169 | /** 170 | * Updates the collision system. This should be called before any collisions are tested. 171 | * @returns {Collisions} 172 | */ 173 | update(): Collisions; 174 | 175 | /** 176 | * Returns a list of potential collisions for a body 177 | * @param {Body} [body] 178 | * @returns {Body[]} 179 | */ 180 | potentials(body?: Body): Body[]; 181 | 182 | /** 183 | * Determines if two bodies are colliding 184 | * @param {Body} source 185 | * @param {Body} target 186 | * @param {Result} [result] 187 | * @param {boolean} [aabb] 188 | * @returns {boolean} 189 | */ 190 | collides(source: Body, target: Body, result?: Result, aabb?: boolean): boolean; 191 | 192 | /** 193 | * Draws the bodies within the system to a CanvasRenderingContext2D's current path 194 | * @param {CanvasRenderingContext2D} context 195 | */ 196 | draw(context: CanvasRenderingContext2D): void; 197 | 198 | /** 199 | * Draws the system's BVH to a CanvasRenderingContext2D's current path. This is useful for testing out different padding values for bodies. 200 | * @param {CanvasRenderingContext2D} context 201 | */ 202 | drawBVH(context: CanvasRenderingContext2D): void; 203 | } -------------------------------------------------------------------------------- /demo/examples/Stress.mjs: -------------------------------------------------------------------------------- 1 | import Collisions from '../../src/Collisions.mjs'; 2 | 3 | const result = Collisions.createResult(); 4 | const width = 800; 5 | const height = 600; 6 | const count = 500 7 | const speed = 1; 8 | const size = 5; 9 | 10 | let frame = 0; 11 | let fps_total = 0; 12 | 13 | export default class Stress { 14 | constructor() { 15 | this.element = document.createElement('div'); 16 | this.canvas = document.createElement('canvas'); 17 | this.context = this.canvas.getContext('2d'); 18 | this.collisions = new Collisions(); 19 | this.bodies = []; 20 | this.polygons = 0; 21 | this.circles = 0; 22 | 23 | this.canvas.width = width; 24 | this.canvas.height = height; 25 | this.context.font = '24px Arial'; 26 | 27 | // World bounds 28 | this.collisions.createPolygon(0, 0, [[0, 0], [width, 0]]); 29 | this.collisions.createPolygon(0, 0, [[width, 0], [width, height]]); 30 | this.collisions.createPolygon(0, 0, [[width, height], [0, height]]); 31 | this.collisions.createPolygon(0, 0, [[0, height], [0, 0]]); 32 | 33 | for(let i = 0; i < count; ++i) { 34 | this.createShape(!random(0, 49)); 35 | } 36 | 37 | this.element.innerHTML = ` 38 |
Total: ${count}
39 |
Polygons: ${this.polygons}
40 |
Circles: ${this.circles}
41 |
42 | `; 43 | 44 | this.bvh_checkbox = this.element.querySelector('#bvh'); 45 | this.element.appendChild(this.canvas); 46 | 47 | const self = this; 48 | 49 | let time = performance.now(); 50 | 51 | this.frame = requestAnimationFrame(function frame() { 52 | const current_time = performance.now(); 53 | 54 | self.update(1000 / (current_time - time)); 55 | self.frame = requestAnimationFrame(frame); 56 | 57 | time = current_time; 58 | }); 59 | } 60 | 61 | update(fps) { 62 | this.collisions.update(); 63 | 64 | ++frame; 65 | fps_total += fps; 66 | 67 | const average_fps = Math.round(fps_total / frame); 68 | 69 | if(frame > 100) { 70 | frame = 1; 71 | fps_total = average_fps; 72 | } 73 | 74 | for(let i = 0; i < this.bodies.length; ++i) { 75 | const body = this.bodies[i]; 76 | 77 | body.x += body.direction_x * speed; 78 | body.y += body.direction_y * speed; 79 | 80 | const potentials = body.potentials(); 81 | 82 | for(const body2 of potentials) { 83 | if(body.collides(body2, result)) { 84 | body.x -= result.overlap * result.overlap_x; 85 | body.y -= result.overlap * result.overlap_y; 86 | 87 | let dot = body.direction_x * result.overlap_y + body.direction_y * -result.overlap_x; 88 | 89 | body.direction_x = 2 * dot * result.overlap_y - body.direction_x; 90 | body.direction_y = 2 * dot * -result.overlap_x - body.direction_y; 91 | 92 | dot = body2.direction_x * result.overlap_y + body2.direction_y * -result.overlap_x; 93 | 94 | body2.direction_x = 2 * dot * result.overlap_y - body2.direction_x; 95 | body2.direction_y = 2 * dot * -result.overlap_x - body2.direction_y; 96 | } 97 | } 98 | } 99 | 100 | // Clear the canvas 101 | this.context.fillStyle = '#000000'; 102 | this.context.fillRect(0, 0, width, height); 103 | 104 | // Render the bodies 105 | this.context.strokeStyle = '#FFFFFF'; 106 | this.context.beginPath(); 107 | this.collisions.draw(this.context); 108 | this.context.stroke(); 109 | 110 | // Render the BVH 111 | if(this.bvh_checkbox.checked) { 112 | this.context.strokeStyle = '#00FF00'; 113 | this.context.beginPath(); 114 | this.collisions.drawBVH(this.context); 115 | this.context.stroke(); 116 | } 117 | 118 | // Render the FPS 119 | this.context.fillStyle = '#FFCC00'; 120 | this.context.fillText(average_fps, 10, 30); 121 | } 122 | 123 | createShape(large) { 124 | const min_size = size * 0.75 * (large ? 3 : 1); 125 | const max_size = size * 1.25 * (large ? 5 : 1); 126 | const x = random(0, width); 127 | const y = random(0, height); 128 | const direction = random(0, 360) * Math.PI / 180; 129 | 130 | let body; 131 | 132 | if(random(0, 2)) { 133 | body = this.collisions.createCircle(x, y, random(min_size, max_size)); 134 | 135 | ++this.circles; 136 | } 137 | else { 138 | body = this.collisions.createPolygon(x, y, [ 139 | [-random(min_size, max_size), -random(min_size, max_size)], 140 | [random(min_size, max_size), -random(min_size, max_size)], 141 | [random(min_size, max_size), random(min_size, max_size)], 142 | [-random(min_size, max_size), random(3, size)], 143 | ], random(0, 360) * Math.PI / 180); 144 | 145 | ++this.polygons; 146 | } 147 | 148 | body.direction_x = Math.cos(direction); 149 | body.direction_y = Math.sin(direction); 150 | 151 | this.bodies.push(body); 152 | } 153 | } 154 | 155 | function random(min, max) { 156 | return Math.floor(Math.random() * max) + min; 157 | } 158 | -------------------------------------------------------------------------------- /demo/examples/Tank.mjs: -------------------------------------------------------------------------------- 1 | import Collisions from '../../src/Collisions.mjs'; 2 | 3 | const width = 800; 4 | const height = 600; 5 | const result = Collisions.createResult(); 6 | 7 | export default class Tank { 8 | constructor() { 9 | const collisions = new Collisions(); 10 | 11 | this.element = document.createElement('div'); 12 | this.canvas = document.createElement('canvas'); 13 | this.context = this.canvas.getContext('2d'); 14 | this.collisions = collisions; 15 | this.bodies = []; 16 | 17 | this.canvas.width = width; 18 | this.canvas.height = height; 19 | this.player = null; 20 | 21 | this.up = false; 22 | this.down = false; 23 | this.left = false; 24 | this.right = false; 25 | 26 | this.element.innerHTML = ` 27 |
W, S - Accelerate/Decelerate
28 |
A, D - Turn
29 |
30 | `; 31 | 32 | const updateKeys = (e) => { 33 | const keydown = e.type === 'keydown'; 34 | const key = e.key.toLowerCase(); 35 | 36 | key === 'w' && (this.up = keydown); 37 | key === 's' && (this.down = keydown); 38 | key === 'a' && (this.left = keydown); 39 | key === 'd' && (this.right = keydown); 40 | }; 41 | 42 | document.addEventListener('keydown', updateKeys); 43 | document.addEventListener('keyup', updateKeys); 44 | 45 | this.bvh_checkbox = this.element.querySelector('#bvh'); 46 | this.element.appendChild(this.canvas); 47 | 48 | this.createPlayer(400, 300); 49 | this.createMap(); 50 | 51 | const frame = () => { 52 | this.update(); 53 | requestAnimationFrame(frame); 54 | }; 55 | 56 | frame(); 57 | } 58 | 59 | update() { 60 | this.handleInput(); 61 | this.processGameLogic(); 62 | this.handleCollisions(); 63 | this.render(); 64 | } 65 | 66 | handleInput() { 67 | this.up && (this.player.velocity += 0.1); 68 | this.down && (this.player.velocity -= 0.1); 69 | this.left && (this.player.angle -= 0.04); 70 | this.right && (this.player.angle += 0.04); 71 | } 72 | 73 | processGameLogic() { 74 | const x = Math.cos(this.player.angle); 75 | const y = Math.sin(this.player.angle); 76 | 77 | if(this.player.velocity > 0) { 78 | this.player.velocity -= 0.05; 79 | 80 | if(this.player.velocity > 3) { 81 | this.player.velocity = 3; 82 | } 83 | } 84 | else if(this.player.velocity < 0) { 85 | this.player.velocity += 0.05; 86 | 87 | if(this.player.velocity < -2) { 88 | this.player.velocity = -2; 89 | } 90 | } 91 | 92 | if(!Math.round(this.player.velocity * 100)) { 93 | this.player.velocity = 0; 94 | } 95 | 96 | if(this.player.velocity) { 97 | this.player.x += x * this.player.velocity; 98 | this.player.y += y * this.player.velocity; 99 | } 100 | } 101 | 102 | handleCollisions() { 103 | this.collisions.update(); 104 | 105 | const potentials = this.player.potentials(); 106 | 107 | // Negate any collisions 108 | for(const body of potentials) { 109 | if(this.player.collides(body, result)) { 110 | this.player.x -= result.overlap * result.overlap_x; 111 | this.player.y -= result.overlap * result.overlap_y; 112 | 113 | this.player.velocity *= 0.9 114 | } 115 | } 116 | } 117 | 118 | render() { 119 | this.context.fillStyle = '#000000'; 120 | this.context.fillRect(0, 0, 800, 600); 121 | 122 | this.context.strokeStyle = '#FFFFFF'; 123 | this.context.beginPath(); 124 | this.collisions.draw(this.context); 125 | this.context.stroke(); 126 | 127 | if(this.bvh_checkbox.checked) { 128 | this.context.strokeStyle = '#00FF00'; 129 | this.context.beginPath(); 130 | this.collisions.drawBVH(this.context); 131 | this.context.stroke(); 132 | } 133 | } 134 | 135 | createPlayer(x, y) { 136 | const size = 15; 137 | 138 | this.player = this.collisions.createPolygon(x, y, [ 139 | [-20, -10], 140 | [20, -10], 141 | [20, 10], 142 | [-20, 10], 143 | ], 0.2); 144 | 145 | this.player.velocity = 0; 146 | } 147 | 148 | createMap() { 149 | // World bounds 150 | this.collisions.createPolygon(0, 0, [[0, 0], [width, 0]]); 151 | this.collisions.createPolygon(0, 0, [[width, 0], [width, height]]); 152 | this.collisions.createPolygon(0, 0, [[width, height], [0, height]]); 153 | this.collisions.createPolygon(0, 0, [[0, height], [0, 0]]); 154 | 155 | // Factory 156 | this.collisions.createPolygon(100, 100, [[-50, -50], [50, -50], [50, 50], [-50, 50],], 0.4); 157 | this.collisions.createPolygon(190, 105, [[-20, -20], [20, -20], [20, 20], [-20, 20],], 0.4); 158 | this.collisions.createCircle(170, 140, 8); 159 | this.collisions.createCircle(185, 155, 8); 160 | this.collisions.createCircle(165, 165, 8); 161 | this.collisions.createCircle(145, 165, 8); 162 | 163 | // Airstrip 164 | this.collisions.createPolygon(230, 50, [[-150, -30], [150, -30], [150, 30], [-150, 30],], 0.4); 165 | 166 | // HQ 167 | this.collisions.createPolygon(100, 500, [[-40, -50], [40, -50], [50, 50], [-50, 50],], 0.2); 168 | this.collisions.createCircle(180, 490, 20); 169 | this.collisions.createCircle(175, 540, 20); 170 | 171 | // Barracks 172 | this.collisions.createPolygon(400, 500, [[-60, -20], [60, -20], [60, 20], [-60, 20]], 1.7); 173 | this.collisions.createPolygon(350, 494, [[-60, -20], [60, -20], [60, 20], [-60, 20]], 1.7); 174 | 175 | // Mountains 176 | this.collisions.createPolygon(750, 0, [[0, 0], [-20, 100]]); 177 | this.collisions.createPolygon(750, 0, [[-20, 100], [30, 250]]); 178 | this.collisions.createPolygon(750, 0, [[30, 250], [20, 300]]); 179 | this.collisions.createPolygon(750, 0, [[20, 300], [-50, 320]]); 180 | this.collisions.createPolygon(750, 0, [[-50, 320], [-90, 500]]); 181 | this.collisions.createPolygon(750, 0, [[-90, 500], [-200, 600]]); 182 | 183 | // Lake 184 | this.collisions.createPolygon(550, 100, [ 185 | [-60, -20], 186 | [-20, -40], 187 | [30, -30], 188 | [60, 20], 189 | [40, 70], 190 | [10, 100], 191 | [-30, 110], 192 | [-80, 90], 193 | [-110, 50], 194 | [-100, 20], 195 | ]); 196 | } 197 | } 198 | 199 | function random(min, max) { 200 | return Math.floor(Math.random() * max) + min; 201 | } 202 | -------------------------------------------------------------------------------- /demo/index.mjs: -------------------------------------------------------------------------------- 1 | import Tank from './examples/Tank.mjs'; 2 | import Stress from './examples/Stress.mjs'; 3 | 4 | let example; 5 | 6 | switch(window.location.search) { 7 | case '?stress': 8 | example = new Stress(); 9 | break; 10 | 11 | default: 12 | example = new Tank(); 13 | break; 14 | } 15 | 16 | document.body.appendChild(example.element); 17 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | document 13 | document 14 | 100% 15 | 100% 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage": "100%", 3 | "expectCount": 110, 4 | "actualCount": 110, 5 | "files": { 6 | "src/Collisions.mjs": { 7 | "expectCount": 15, 8 | "actualCount": 15, 9 | "undocumentLines": [] 10 | }, 11 | "src/modules/BVH.mjs": { 12 | "expectCount": 11, 13 | "actualCount": 11, 14 | "undocumentLines": [] 15 | }, 16 | "src/modules/BVHBranch.mjs": { 17 | "expectCount": 15, 18 | "actualCount": 15, 19 | "undocumentLines": [] 20 | }, 21 | "src/modules/Body.mjs": { 22 | "expectCount": 21, 23 | "actualCount": 21, 24 | "undocumentLines": [] 25 | }, 26 | "src/modules/Circle.mjs": { 27 | "expectCount": 5, 28 | "actualCount": 5, 29 | "undocumentLines": [] 30 | }, 31 | "src/modules/Point.mjs": { 32 | "expectCount": 3, 33 | "actualCount": 3, 34 | "undocumentLines": [] 35 | }, 36 | "src/modules/Polygon.mjs": { 37 | "expectCount": 25, 38 | "actualCount": 25, 39 | "undocumentLines": [] 40 | }, 41 | "src/modules/Result.mjs": { 42 | "expectCount": 9, 43 | "actualCount": 9, 44 | "undocumentLines": [] 45 | }, 46 | "src/modules/SAT.mjs": { 47 | "expectCount": 6, 48 | "actualCount": 6, 49 | "undocumentLines": [] 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /docs/css/github.css: -------------------------------------------------------------------------------- 1 | /* github markdown */ 2 | .github-markdown { 3 | font-size: 16px; 4 | } 5 | 6 | .github-markdown h1, 7 | .github-markdown h2, 8 | .github-markdown h3, 9 | .github-markdown h4, 10 | .github-markdown h5 { 11 | margin-top: 1em; 12 | margin-bottom: 16px; 13 | font-weight: bold; 14 | padding: 0; 15 | } 16 | 17 | .github-markdown h1:nth-of-type(1) { 18 | margin-top: 0; 19 | } 20 | 21 | .github-markdown h1 { 22 | font-size: 2em; 23 | padding-bottom: 0.3em; 24 | } 25 | 26 | .github-markdown h2 { 27 | font-size: 1.75em; 28 | padding-bottom: 0.3em; 29 | } 30 | 31 | .github-markdown h3 { 32 | font-size: 1.5em; 33 | } 34 | 35 | .github-markdown h4 { 36 | font-size: 1.25em; 37 | } 38 | 39 | .github-markdown h5 { 40 | font-size: 1em; 41 | } 42 | 43 | .github-markdown ul, .github-markdown ol { 44 | padding-left: 2em; 45 | } 46 | 47 | .github-markdown pre > code { 48 | font-size: 0.85em; 49 | } 50 | 51 | .github-markdown table { 52 | margin-bottom: 1em; 53 | border-collapse: collapse; 54 | border-spacing: 0; 55 | } 56 | 57 | .github-markdown table tr { 58 | background-color: #fff; 59 | border-top: 1px solid #ccc; 60 | } 61 | 62 | .github-markdown table th, 63 | .github-markdown table td { 64 | padding: 6px 13px; 65 | border: 1px solid #ddd; 66 | } 67 | 68 | .github-markdown table tr:nth-child(2n) { 69 | background-color: #f8f8f8; 70 | } 71 | 72 | .github-markdown hr { 73 | border-right: 0; 74 | border-bottom: 1px solid #e5e5e5; 75 | border-left: 0; 76 | border-top: 0; 77 | } 78 | 79 | /** badge(.svg) does not have border */ 80 | .github-markdown img:not([src*=".svg"]) { 81 | max-width: 100%; 82 | box-shadow: 1px 1px 1px rgba(0,0,0,0.5); 83 | } 84 | -------------------------------------------------------------------------------- /docs/css/identifiers.css: -------------------------------------------------------------------------------- 1 | .identifiers-wrap { 2 | display: flex; 3 | align-items: flex-start; 4 | } 5 | 6 | .identifier-dir-tree { 7 | background: #fff; 8 | border: solid 1px #ddd; 9 | border-radius: 0.25em; 10 | top: 52px; 11 | position: -webkit-sticky; 12 | position: sticky; 13 | max-height: calc(100vh - 155px); 14 | overflow-y: scroll; 15 | min-width: 200px; 16 | margin-left: 1em; 17 | } 18 | 19 | .identifier-dir-tree-header { 20 | padding: 0.5em; 21 | background-color: #fafafa; 22 | border-bottom: solid 1px #ddd; 23 | } 24 | 25 | .identifier-dir-tree-content { 26 | padding: 0 0.5em 0; 27 | } 28 | 29 | .identifier-dir-tree-content > div { 30 | padding-top: 0.25em; 31 | padding-bottom: 0.25em; 32 | } 33 | 34 | .identifier-dir-tree-content a { 35 | color: inherit; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /docs/css/manual.css: -------------------------------------------------------------------------------- 1 | .github-markdown .manual-toc { 2 | padding-left: 0; 3 | } 4 | 5 | .manual-index .manual-cards { 6 | display: flex; 7 | flex-wrap: wrap; 8 | } 9 | 10 | .manual-index .manual-card-wrap { 11 | width: 280px; 12 | padding: 10px 20px 10px 0; 13 | box-sizing: border-box; 14 | } 15 | 16 | .manual-index .manual-card-wrap > h1 { 17 | margin: 0; 18 | font-size: 1em; 19 | font-weight: 600; 20 | padding: 0.2em 0 0.2em 0.5em; 21 | border-radius: 0.1em 0.1em 0 0; 22 | border: none; 23 | } 24 | 25 | .manual-index .manual-card-wrap > h1 span { 26 | color: #555; 27 | } 28 | 29 | .manual-index .manual-card { 30 | height: 200px; 31 | overflow: hidden; 32 | border: solid 1px rgba(230, 230, 230, 0.84); 33 | border-radius: 0 0 0.1em 0.1em; 34 | padding: 8px; 35 | position: relative; 36 | } 37 | 38 | .manual-index .manual-card > div { 39 | transform: scale(0.4); 40 | transform-origin: 0 0; 41 | width: 250%; 42 | } 43 | 44 | .manual-index .manual-card > a { 45 | position: absolute; 46 | top: 0; 47 | left: 0; 48 | width: 100%; 49 | height: 100%; 50 | background: rgba(210, 210, 210, 0.1); 51 | } 52 | 53 | .manual-index .manual-card > a:hover { 54 | background: none; 55 | } 56 | 57 | .manual-index .manual-badge { 58 | margin: 0; 59 | } 60 | 61 | .manual-index .manual-user-index { 62 | margin-bottom: 1em; 63 | border-bottom: solid 1px #ddd; 64 | } 65 | 66 | .manual-root .navigation { 67 | padding-left: 4px; 68 | margin-top: 4px; 69 | } 70 | 71 | .navigation .manual-toc-root > div { 72 | padding-left: 0.25em; 73 | padding-right: 0.75em; 74 | } 75 | 76 | .github-markdown .manual-toc-title a { 77 | color: inherit; 78 | } 79 | 80 | .manual-breadcrumb-list { 81 | font-size: 0.8em; 82 | margin-bottom: 1em; 83 | } 84 | 85 | .manual-toc-title a:hover { 86 | color: #039BE5; 87 | } 88 | 89 | .manual-toc li { 90 | margin: 0.75em 0; 91 | list-style-type: none; 92 | } 93 | 94 | .navigation .manual-toc [class^="indent-h"] a { 95 | color: #666; 96 | } 97 | 98 | .navigation .manual-toc .indent-h1 a { 99 | color: #555; 100 | font-weight: 600; 101 | display: block; 102 | } 103 | 104 | .manual-toc .indent-h1 { 105 | display: block; 106 | margin: 0.4em 0 0 0.25em; 107 | padding: 0.2em 0 0.2em 0.5em; 108 | border-radius: 0.1em; 109 | } 110 | 111 | .manual-root .navigation .manual-toc li:not(.indent-h1) { 112 | margin-top: 0.5em; 113 | } 114 | 115 | .manual-toc .indent-h2 { 116 | display: none; 117 | margin-left: 1.5em; 118 | } 119 | .manual-toc .indent-h3 { 120 | display: none; 121 | margin-left: 2.5em; 122 | } 123 | .manual-toc .indent-h4 { 124 | display: none; 125 | margin-left: 3.5em; 126 | } 127 | .manual-toc .indent-h5 { 128 | display: none; 129 | margin-left: 4.5em; 130 | } 131 | 132 | .manual-nav li { 133 | margin: 0.75em 0; 134 | } 135 | -------------------------------------------------------------------------------- /docs/css/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /docs/css/search.css: -------------------------------------------------------------------------------- 1 | /* search box */ 2 | .search-box { 3 | position: absolute; 4 | top: 10px; 5 | right: 50px; 6 | padding-right: 8px; 7 | padding-bottom: 10px; 8 | line-height: normal; 9 | font-size: 12px; 10 | } 11 | 12 | .search-box img { 13 | width: 20px; 14 | vertical-align: top; 15 | } 16 | 17 | .search-input { 18 | display: inline; 19 | visibility: hidden; 20 | width: 0; 21 | padding: 2px; 22 | height: 1.5em; 23 | outline: none; 24 | background: transparent; 25 | border: 1px #0af; 26 | border-style: none none solid none; 27 | vertical-align: bottom; 28 | } 29 | 30 | .search-input-edge { 31 | display: none; 32 | width: 1px; 33 | height: 5px; 34 | background-color: #0af; 35 | vertical-align: bottom; 36 | } 37 | 38 | .search-result { 39 | position: absolute; 40 | display: none; 41 | height: 600px; 42 | width: 100%; 43 | padding: 0; 44 | margin-top: 5px; 45 | margin-left: 24px; 46 | background: white; 47 | box-shadow: 1px 1px 4px rgb(0,0,0); 48 | white-space: nowrap; 49 | overflow-y: scroll; 50 | } 51 | 52 | .search-result-import-path { 53 | color: #aaa; 54 | font-size: 12px; 55 | } 56 | 57 | .search-result li { 58 | list-style: none; 59 | padding: 2px 4px; 60 | } 61 | 62 | .search-result li a { 63 | display: block; 64 | } 65 | 66 | .search-result li.selected { 67 | background: #ddd; 68 | } 69 | 70 | .search-result li.search-separator { 71 | background: rgb(37, 138, 175); 72 | color: white; 73 | } 74 | 75 | .search-box.active .search-input { 76 | visibility: visible; 77 | transition: width 0.2s ease-out; 78 | width: 300px; 79 | } 80 | 81 | .search-box.active .search-input-edge { 82 | display: inline-block; 83 | } 84 | 85 | -------------------------------------------------------------------------------- /docs/css/source.css: -------------------------------------------------------------------------------- 1 | table.files-summary { 2 | width: 100%; 3 | margin: 10px 0; 4 | border-spacing: 0; 5 | border: 0; 6 | border-collapse: collapse; 7 | text-align: right; 8 | } 9 | 10 | table.files-summary tbody tr:hover { 11 | background: #eee; 12 | } 13 | 14 | table.files-summary td:first-child, 15 | table.files-summary td:nth-of-type(2) { 16 | text-align: left; 17 | } 18 | 19 | table.files-summary[data-use-coverage="false"] td.coverage { 20 | display: none; 21 | } 22 | 23 | table.files-summary thead { 24 | background: #fafafa; 25 | } 26 | 27 | table.files-summary td { 28 | border: solid 1px #ddd; 29 | padding: 4px 10px; 30 | vertical-align: top; 31 | } 32 | 33 | table.files-summary td.identifiers > span { 34 | display: block; 35 | margin-top: 4px; 36 | } 37 | table.files-summary td.identifiers > span:first-child { 38 | margin-top: 0; 39 | } 40 | 41 | table.files-summary .coverage-count { 42 | font-size: 12px; 43 | color: #aaa; 44 | display: inline-block; 45 | min-width: 40px; 46 | } 47 | 48 | .total-coverage-count { 49 | position: relative; 50 | bottom: 2px; 51 | font-size: 12px; 52 | color: #666; 53 | font-weight: 500; 54 | padding-left: 5px; 55 | } 56 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,300,700); 2 | @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600,700); 3 | @import url(./manual.css); 4 | @import url(./source.css); 5 | @import url(./test.css); 6 | @import url(./identifiers.css); 7 | @import url(./github.css); 8 | @import url(./search.css); 9 | 10 | * { 11 | margin: 0; 12 | padding: 0; 13 | text-decoration: none; 14 | } 15 | 16 | html 17 | { 18 | font-family: 'Source Sans Pro', 'Roboto', sans-serif; 19 | overflow: auto; 20 | /*font-size: 14px;*/ 21 | /*color: #4d4e53;*/ 22 | /*color: rgba(0, 0, 0, .68);*/ 23 | color: #555; 24 | background-color: #fff; 25 | } 26 | 27 | a { 28 | /*color: #0095dd;*/ 29 | /*color:rgb(37, 138, 175);*/ 30 | color: #039BE5; 31 | } 32 | 33 | code a:hover { 34 | text-decoration: underline; 35 | } 36 | 37 | ul, ol { 38 | padding-left: 20px; 39 | } 40 | 41 | ul li { 42 | list-style: disc; 43 | margin: 4px 0; 44 | } 45 | 46 | ol li { 47 | margin: 4px 0; 48 | } 49 | 50 | h1 { 51 | margin-bottom: 10px; 52 | font-size: 34px; 53 | font-weight: 300; 54 | border-bottom: solid 1px #ddd; 55 | } 56 | 57 | h2 { 58 | margin-top: 24px; 59 | margin-bottom: 10px; 60 | font-size: 20px; 61 | border-bottom: solid 1px #ddd; 62 | font-weight: 300; 63 | } 64 | 65 | h3 { 66 | position: relative; 67 | font-size: 16px; 68 | margin-bottom: 12px; 69 | padding: 4px; 70 | font-weight: 300; 71 | } 72 | 73 | details { 74 | cursor: pointer; 75 | } 76 | 77 | del { 78 | text-decoration: line-through; 79 | } 80 | 81 | p { 82 | margin-bottom: 15px; 83 | line-height: 1.5; 84 | } 85 | 86 | code { 87 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 88 | } 89 | 90 | pre > code { 91 | display: block; 92 | } 93 | 94 | pre.prettyprint, pre > code { 95 | padding: 4px; 96 | margin: 1em 0; 97 | background-color: #f5f5f5; 98 | border-radius: 3px; 99 | } 100 | 101 | pre.prettyprint > code { 102 | margin: 0; 103 | } 104 | 105 | p > code, 106 | li > code { 107 | padding: 0.2em 0.5em; 108 | margin: 0; 109 | font-size: 85%; 110 | background-color: rgba(0,0,0,0.04); 111 | border-radius: 3px; 112 | } 113 | 114 | .code { 115 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 116 | font-size: 13px; 117 | } 118 | 119 | .import-path pre.prettyprint, 120 | .import-path pre.prettyprint code { 121 | margin: 0; 122 | padding: 0; 123 | border: none; 124 | background: white; 125 | } 126 | 127 | .layout-container { 128 | /*display: flex;*/ 129 | /*flex-direction: row;*/ 130 | /*justify-content: flex-start;*/ 131 | /*align-items: stretch;*/ 132 | } 133 | 134 | .layout-container > header { 135 | display: flex; 136 | height: 40px; 137 | line-height: 40px; 138 | font-size: 16px; 139 | padding: 0 10px; 140 | margin: 0; 141 | position: fixed; 142 | width: 100%; 143 | z-index: 1; 144 | background-color: #fafafa; 145 | top: 0; 146 | border-bottom: solid 1px #ddd; 147 | } 148 | .layout-container > header > a{ 149 | margin: 0 5px; 150 | color: #444; 151 | } 152 | 153 | .layout-container > header > a.repo-url-github { 154 | font-size: 0; 155 | display: inline-block; 156 | width: 20px; 157 | height: 38px; 158 | background: url("../image/github.png") no-repeat center; 159 | background-size: 20px; 160 | vertical-align: top; 161 | } 162 | 163 | .navigation { 164 | position: fixed; 165 | top: 0; 166 | left: 0; 167 | box-sizing: border-box; 168 | width: 250px; 169 | height: 100%; 170 | padding-top: 40px; 171 | padding-left: 15px; 172 | padding-bottom: 2em; 173 | margin-top:1em; 174 | overflow-x: scroll; 175 | box-shadow: rgba(255, 255, 255, 1) -1px 0 0 inset; 176 | border-right: 1px solid #ddd; 177 | } 178 | 179 | .navigation ul { 180 | padding: 0; 181 | } 182 | 183 | .navigation li { 184 | list-style: none; 185 | margin: 4px 0; 186 | white-space: nowrap; 187 | } 188 | 189 | .navigation li a { 190 | color: #666; 191 | } 192 | 193 | .navigation .nav-dir-path { 194 | display: block; 195 | margin-top: 0.7em; 196 | margin-bottom: 0.25em; 197 | font-weight: 600; 198 | } 199 | 200 | .kind-class, 201 | .kind-interface, 202 | .kind-function, 203 | .kind-typedef, 204 | .kind-variable, 205 | .kind-external { 206 | margin-left: 0.75em; 207 | width: 1.2em; 208 | height: 1.2em; 209 | display: inline-block; 210 | text-align: center; 211 | border-radius: 0.2em; 212 | margin-right: 0.2em; 213 | font-weight: bold; 214 | line-height: 1.2em; 215 | } 216 | 217 | .kind-class { 218 | color: #009800; 219 | background-color: #bfe5bf; 220 | } 221 | 222 | .kind-interface { 223 | color: #fbca04; 224 | background-color: #fef2c0; 225 | } 226 | 227 | .kind-function { 228 | color: #6b0090; 229 | background-color: #d6bdde; 230 | } 231 | 232 | .kind-variable { 233 | color: #eb6420; 234 | background-color: #fad8c7; 235 | } 236 | 237 | .kind-typedef { 238 | color: #db001e; 239 | background-color: #edbec3; 240 | } 241 | 242 | .kind-external { 243 | color: #0738c3; 244 | background-color: #bbcbea; 245 | } 246 | 247 | .summary span[class^="kind-"] { 248 | margin-left: 0; 249 | } 250 | 251 | h1 .version, 252 | h1 .url a { 253 | font-size: 14px; 254 | color: #aaa; 255 | } 256 | 257 | .content { 258 | margin-top: 40px; 259 | margin-left: 250px; 260 | padding: 10px 50px 10px 20px; 261 | } 262 | 263 | .header-notice { 264 | font-size: 14px; 265 | color: #aaa; 266 | margin: 0; 267 | } 268 | 269 | .expression-extends .prettyprint { 270 | margin-left: 10px; 271 | background: white; 272 | } 273 | 274 | .extends-chain { 275 | border-bottom: 1px solid#ddd; 276 | padding-bottom: 10px; 277 | margin-bottom: 10px; 278 | } 279 | 280 | .extends-chain span:nth-of-type(1) { 281 | padding-left: 10px; 282 | } 283 | 284 | .extends-chain > div { 285 | margin: 5px 0; 286 | } 287 | 288 | .description table { 289 | font-size: 14px; 290 | border-spacing: 0; 291 | border: 0; 292 | border-collapse: collapse; 293 | } 294 | 295 | .description thead { 296 | background: #999; 297 | color: white; 298 | } 299 | 300 | .description table td, 301 | .description table th { 302 | border: solid 1px #ddd; 303 | padding: 4px; 304 | font-weight: normal; 305 | } 306 | 307 | .flat-list ul { 308 | padding-left: 0; 309 | } 310 | 311 | .flat-list li { 312 | display: inline; 313 | list-style: none; 314 | } 315 | 316 | table.summary { 317 | width: 100%; 318 | margin: 10px 0; 319 | border-spacing: 0; 320 | border: 0; 321 | border-collapse: collapse; 322 | } 323 | 324 | table.summary thead { 325 | background: #fafafa; 326 | } 327 | 328 | table.summary td { 329 | border: solid 1px #ddd; 330 | padding: 4px 10px; 331 | } 332 | 333 | table.summary tbody td:nth-child(1) { 334 | text-align: right; 335 | white-space: nowrap; 336 | min-width: 64px; 337 | vertical-align: top; 338 | } 339 | 340 | table.summary tbody td:nth-child(2) { 341 | width: 100%; 342 | border-right: none; 343 | } 344 | 345 | table.summary tbody td:nth-child(3) { 346 | white-space: nowrap; 347 | border-left: none; 348 | vertical-align: top; 349 | } 350 | 351 | table.summary td > div:nth-of-type(2) { 352 | padding-top: 4px; 353 | padding-left: 15px; 354 | } 355 | 356 | table.summary td p { 357 | margin-bottom: 0; 358 | } 359 | 360 | .inherited-summary thead td { 361 | padding-left: 2px; 362 | } 363 | 364 | .inherited-summary thead a { 365 | color: white; 366 | } 367 | 368 | .inherited-summary .summary tbody { 369 | display: none; 370 | } 371 | 372 | .inherited-summary .summary .toggle { 373 | padding: 0 4px; 374 | font-size: 12px; 375 | cursor: pointer; 376 | } 377 | .inherited-summary .summary .toggle.closed:before { 378 | content: "▶"; 379 | } 380 | .inherited-summary .summary .toggle.opened:before { 381 | content: "▼"; 382 | } 383 | 384 | .member, .method { 385 | margin-bottom: 24px; 386 | } 387 | 388 | table.params { 389 | width: 100%; 390 | margin: 10px 0; 391 | border-spacing: 0; 392 | border: 0; 393 | border-collapse: collapse; 394 | } 395 | 396 | table.params thead { 397 | background: #eee; 398 | color: #aaa; 399 | } 400 | 401 | table.params td { 402 | padding: 4px; 403 | border: solid 1px #ddd; 404 | } 405 | 406 | table.params td p { 407 | margin: 0; 408 | } 409 | 410 | .content .detail > * { 411 | margin: 15px 0; 412 | } 413 | 414 | .content .detail > h3 { 415 | color: black; 416 | background-color: #f0f0f0; 417 | } 418 | 419 | .content .detail > div { 420 | margin-left: 10px; 421 | } 422 | 423 | .content .detail > .import-path { 424 | margin-top: -8px; 425 | } 426 | 427 | .content .detail + .detail { 428 | margin-top: 30px; 429 | } 430 | 431 | .content .detail .throw td:first-child { 432 | padding-right: 10px; 433 | } 434 | 435 | .content .detail h4 + :not(pre) { 436 | padding-left: 0; 437 | margin-left: 10px; 438 | } 439 | 440 | .content .detail h4 + ul li { 441 | list-style: none; 442 | } 443 | 444 | .return-param * { 445 | display: inline; 446 | } 447 | 448 | .argument-params { 449 | margin-bottom: 20px; 450 | } 451 | 452 | .return-type { 453 | padding-right: 10px; 454 | font-weight: normal; 455 | } 456 | 457 | .return-desc { 458 | margin-left: 10px; 459 | margin-top: 4px; 460 | } 461 | 462 | .return-desc p { 463 | margin: 0; 464 | } 465 | 466 | .deprecated, .experimental, .instance-docs { 467 | border-left: solid 5px orange; 468 | padding-left: 4px; 469 | margin: 4px 0; 470 | } 471 | 472 | tr.listen p, 473 | tr.throw p, 474 | tr.emit p{ 475 | margin-bottom: 10px; 476 | } 477 | 478 | .version, .since { 479 | color: #aaa; 480 | } 481 | 482 | h3 .right-info { 483 | position: absolute; 484 | right: 4px; 485 | font-size: 14px; 486 | } 487 | 488 | .version + .since:before { 489 | content: '| '; 490 | } 491 | 492 | .see { 493 | margin-top: 10px; 494 | } 495 | 496 | .see h4 { 497 | margin: 4px 0; 498 | } 499 | 500 | .content .detail h4 + .example-doc { 501 | margin: 6px 0; 502 | } 503 | 504 | .example-caption { 505 | position: relative; 506 | bottom: -1px; 507 | display: inline-block; 508 | padding: 4px; 509 | font-style: italic; 510 | background-color: #f5f5f5; 511 | font-weight: bold; 512 | border-radius: 3px; 513 | border-bottom-left-radius: 0; 514 | border-bottom-right-radius: 0; 515 | } 516 | 517 | .example-caption + pre.source-code { 518 | margin-top: 0; 519 | border-top-left-radius: 0; 520 | } 521 | 522 | footer, .file-footer { 523 | text-align: right; 524 | font-style: italic; 525 | font-weight: 100; 526 | font-size: 13px; 527 | margin-right: 50px; 528 | margin-left: 270px; 529 | border-top: 1px solid #ddd; 530 | padding-top: 30px; 531 | margin-top: 20px; 532 | padding-bottom: 10px; 533 | } 534 | 535 | footer img { 536 | width: 24px; 537 | vertical-align: middle; 538 | padding-left: 4px; 539 | position: relative; 540 | top: -3px; 541 | opacity: 0.6; 542 | } 543 | 544 | pre.source-code { 545 | padding: 4px; 546 | } 547 | 548 | pre.raw-source-code > code { 549 | padding: 0; 550 | margin: 0; 551 | font-size: 12px; 552 | background: #fff; 553 | border: solid 1px #ddd; 554 | line-height: 1.5; 555 | } 556 | 557 | pre.raw-source-code > code > ol { 558 | counter-reset:number; 559 | list-style:none; 560 | margin:0; 561 | padding:0; 562 | overflow: hidden; 563 | } 564 | 565 | pre.raw-source-code > code > ol li:before { 566 | counter-increment: number; 567 | content: counter(number); 568 | display: inline-block; 569 | min-width: 3em; 570 | color: #aaa; 571 | text-align: right; 572 | padding-right: 1em; 573 | } 574 | 575 | pre.source-code.line-number { 576 | padding: 0; 577 | } 578 | 579 | pre.source-code ol { 580 | background: #eee; 581 | padding-left: 40px; 582 | } 583 | 584 | pre.source-code li { 585 | background: white; 586 | padding-left: 4px; 587 | list-style: decimal; 588 | margin: 0; 589 | } 590 | 591 | pre.source-code.line-number li.active { 592 | background: rgb(255, 255, 150) !important; 593 | } 594 | 595 | pre.source-code.line-number li.error-line { 596 | background: #ffb8bf; 597 | } 598 | 599 | .inner-link-active { 600 | /*background: rgb(255, 255, 150) !important;*/ 601 | background: #039BE5 !important; 602 | color: #fff !important; 603 | padding-left: 0.1em !important; 604 | } 605 | 606 | .inner-link-active a { 607 | color: inherit; 608 | } 609 | -------------------------------------------------------------------------------- /docs/css/test.css: -------------------------------------------------------------------------------- 1 | table.test-summary thead { 2 | background: #fafafa; 3 | } 4 | 5 | table.test-summary thead .test-description { 6 | width: 50%; 7 | } 8 | 9 | table.test-summary { 10 | width: 100%; 11 | margin: 10px 0; 12 | border-spacing: 0; 13 | border: 0; 14 | border-collapse: collapse; 15 | } 16 | 17 | table.test-summary thead .test-count { 18 | width: 3em; 19 | } 20 | 21 | table.test-summary tbody tr:hover { 22 | background-color: #eee; 23 | } 24 | 25 | table.test-summary td { 26 | border: solid 1px #ddd; 27 | padding: 4px 10px; 28 | vertical-align: top; 29 | } 30 | 31 | table.test-summary td p { 32 | margin: 0; 33 | } 34 | 35 | table.test-summary tr.test-interface .toggle { 36 | display: inline-block; 37 | float: left; 38 | margin-right: 4px; 39 | cursor: pointer; 40 | font-size: 0.8em; 41 | padding-top: 0.25em; 42 | } 43 | 44 | table.test-summary tr.test-interface .toggle.opened:before { 45 | content: '▼'; 46 | } 47 | 48 | table.test-summary tr.test-interface .toggle.closed:before { 49 | content: '▶'; 50 | } 51 | 52 | table.test-summary .test-target > span { 53 | display: block; 54 | margin-top: 4px; 55 | } 56 | table.test-summary .test-target > span:first-child { 57 | margin-top: 0; 58 | } 59 | -------------------------------------------------------------------------------- /docs/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Collisions - Collision detection for circles, polygons, and points 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/file/src/Collisions.mjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/Collisions.mjs | collisions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Home 16 | 17 | Reference 18 | Source 19 | 20 | 27 |
28 | 29 | 41 | 42 |

src/Collisions.mjs

43 |
import BVH     from './modules/BVH.mjs';
 44 | import Circle  from './modules/Circle.mjs';
 45 | import Polygon from './modules/Polygon.mjs';
 46 | import Point   from './modules/Point.mjs';
 47 | import Result  from './modules/Result.mjs';
 48 | import SAT     from './modules/SAT.mjs';
 49 | 
 50 | /**
 51 |  * A collision system used to track bodies in order to improve collision detection performance
 52 |  * @class
 53 |  */
 54 | class Collisions {
 55 | 	/**
 56 | 	 * @constructor
 57 | 	 */
 58 | 	constructor() {
 59 | 		/** @private */
 60 | 		this._bvh = new BVH();
 61 | 	}
 62 | 
 63 | 	/**
 64 | 	 * Creates a {@link Circle} and inserts it into the collision system
 65 | 	 * @param {Number} [x = 0] The starting X coordinate
 66 | 	 * @param {Number} [y = 0] The starting Y coordinate
 67 | 	 * @param {Number} [radius = 0] The radius
 68 | 	 * @param {Number} [scale = 1] The scale
 69 | 	 * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions
 70 | 	 * @returns {Circle}
 71 | 	 */
 72 | 	createCircle(x = 0, y = 0, radius = 0, scale = 1, padding = 0) {
 73 | 		const body = new Circle(x, y, radius, scale, padding);
 74 | 
 75 | 		this._bvh.insert(body);
 76 | 
 77 | 		return body;
 78 | 	}
 79 | 
 80 | 	/**
 81 | 	 * Creates a {@link Polygon} and inserts it into the collision system
 82 | 	 * @param {Number} [x = 0] The starting X coordinate
 83 | 	 * @param {Number} [y = 0] The starting Y coordinate
 84 | 	 * @param {Array<Number[]>} [points = []] An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...]
 85 | 	 * @param {Number} [angle = 0] The starting rotation in radians
 86 | 	 * @param {Number} [scale_x = 1] The starting scale along the X axis
 87 | 	 * @param {Number} [scale_y = 1] The starting scale long the Y axis
 88 | 	 * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions
 89 | 	 * @returns {Polygon}
 90 | 	 */
 91 | 	createPolygon(x = 0, y = 0, points = [[0, 0]], angle = 0, scale_x = 1, scale_y = 1, padding = 0) {
 92 | 		const body = new Polygon(x, y, points, angle, scale_x, scale_y, padding);
 93 | 
 94 | 		this._bvh.insert(body);
 95 | 
 96 | 		return body;
 97 | 	}
 98 | 
 99 | 	/**
100 | 	 * Creates a {@link Point} and inserts it into the collision system
101 | 	 * @param {Number} [x = 0] The starting X coordinate
102 | 	 * @param {Number} [y = 0] The starting Y coordinate
103 | 	 * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions
104 | 	 * @returns {Point}
105 | 	 */
106 | 	createPoint(x = 0, y = 0, padding = 0) {
107 | 		const body = new Point(x, y, padding);
108 | 
109 | 		this._bvh.insert(body);
110 | 
111 | 		return body;
112 | 	}
113 | 
114 | 	/**
115 | 	 * Creates a {@link Result} used to collect the detailed results of a collision test
116 | 	 */
117 | 	createResult() {
118 | 		return new Result();
119 | 	}
120 | 
121 | 	/**
122 | 	 * Creates a Result used to collect the detailed results of a collision test
123 | 	 */
124 | 	static createResult() {
125 | 		return new Result();
126 | 	}
127 | 
128 | 	/**
129 | 	 * Inserts bodies into the collision system
130 | 	 * @param {...Circle|...Polygon|...Point} bodies
131 | 	 */
132 | 	insert(...bodies) {
133 | 		for(const body of bodies) {
134 | 			this._bvh.insert(body, false);
135 | 		}
136 | 
137 | 		return this;
138 | 	}
139 | 
140 | 	/**
141 | 	 * Removes bodies from the collision system
142 | 	 * @param {...Circle|...Polygon|...Point} bodies
143 | 	 */
144 | 	remove(...bodies) {
145 | 		for(const body of bodies) {
146 | 			this._bvh.remove(body, false);
147 | 		}
148 | 
149 | 		return this;
150 | 	}
151 | 
152 | 	/**
153 | 	 * Updates the collision system. This should be called before any collisions are tested.
154 | 	 */
155 | 	update() {
156 | 		this._bvh.update();
157 | 
158 | 		return this;
159 | 	}
160 | 
161 | 	/**
162 | 	 * Draws the bodies within the system to a CanvasRenderingContext2D's current path
163 | 	 * @param {CanvasRenderingContext2D} context The context to draw to
164 | 	 */
165 | 	draw(context) {
166 | 		return this._bvh.draw(context);
167 | 	}
168 | 
169 | 	/**
170 | 	 * Draws the system's BVH to a CanvasRenderingContext2D's current path. This is useful for testing out different padding values for bodies.
171 | 	 * @param {CanvasRenderingContext2D} context The context to draw to
172 | 	 */
173 | 	drawBVH(context) {
174 | 		return this._bvh.drawBVH(context);
175 | 	}
176 | 
177 | 	/**
178 | 	 * Returns a list of potential collisions for a body
179 | 	 * @param {Circle|Polygon|Point} body The body to test for potential collisions against
180 | 	 * @returns {Array<Body>}
181 | 	 */
182 | 	potentials(body) {
183 | 		return this._bvh.potentials(body);
184 | 	}
185 | 
186 | 	/**
187 | 	 * Determines if two bodies are colliding
188 | 	 * @param {Circle|Polygon|Point} target The target body to test against
189 | 	 * @param {Result} [result = null] A Result object on which to store information about the collision
190 | 	 * @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own potential collision heuristic)
191 | 	 * @returns {Boolean}
192 | 	 */
193 | 	collides(source, target, result = null, aabb = true) {
194 | 		return SAT(source, target, result, aabb);
195 | 	}
196 | };
197 | 
198 | export {
199 | 	Collisions as default,
200 | 	Collisions,
201 | 	Result,
202 | 	Circle,
203 | 	Polygon,
204 | 	Point,
205 | };
206 | 
207 | 208 |
209 | 210 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /docs/file/src/modules/BVHBranch.mjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/modules/BVHBranch.mjs | collisions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Home 16 | 17 | Reference 18 | Source 19 | 20 | 27 |
28 | 29 | 41 | 42 |

src/modules/BVHBranch.mjs

43 |
/**
 44 |  * @private
 45 |  */
 46 | const branch_pool = [];
 47 | 
 48 | /**
 49 |  * A branch within a BVH
 50 |  * @class
 51 |  * @private
 52 |  */
 53 | export default class BVHBranch {
 54 | 	/**
 55 | 	 * @constructor
 56 | 	 */
 57 | 	constructor() {
 58 | 		/** @private */
 59 | 		this._bvh_parent = null;
 60 | 
 61 | 		/** @private */
 62 | 		this._bvh_branch = true;
 63 | 
 64 | 		/** @private */
 65 | 		this._bvh_left = null;
 66 | 
 67 | 		/** @private */
 68 | 		this._bvh_right = null;
 69 | 
 70 | 		/** @private */
 71 | 		this._bvh_sort = 0;
 72 | 
 73 | 		/** @private */
 74 | 		this._bvh_min_x = 0;
 75 | 
 76 | 		/** @private */
 77 | 		this._bvh_min_y = 0;
 78 | 
 79 | 		/** @private */
 80 | 		this._bvh_max_x = 0;
 81 | 
 82 | 		/** @private */
 83 | 		this._bvh_max_y = 0;
 84 | 	}
 85 | 
 86 | 	/**
 87 | 	 * Returns a branch from the branch pool or creates a new branch
 88 | 	 * @returns {BVHBranch}
 89 | 	 */
 90 | 	static getBranch() {
 91 | 		if(branch_pool.length) {
 92 | 			return branch_pool.pop();
 93 | 		}
 94 | 
 95 | 		return new BVHBranch();
 96 | 	}
 97 | 
 98 | 	/**
 99 | 	 * Releases a branch back into the branch pool
100 | 	 * @param {BVHBranch} branch The branch to release
101 | 	 */
102 | 	static releaseBranch(branch) {
103 | 		branch_pool.push(branch);
104 | 	}
105 | 
106 | 	/**
107 | 	 * Sorting callback used to sort branches by deepest first
108 | 	 * @param {BVHBranch} a The first branch
109 | 	 * @param {BVHBranch} b The second branch
110 | 	 * @returns {Number}
111 | 	 */
112 | 	static sortBranches(a, b) {
113 | 		return a.sort > b.sort ? -1 : 1;
114 | 	}
115 | };
116 | 
117 | 118 |
119 | 120 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /docs/file/src/modules/Body.mjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/modules/Body.mjs | collisions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Home 16 | 17 | Reference 18 | Source 19 | 20 | 27 |
28 | 29 | 41 | 42 |

src/modules/Body.mjs

43 |
import Result from './Result.mjs';
 44 | import SAT    from './SAT.mjs';
 45 | 
 46 | /**
 47 |  * The base class for bodies used to detect collisions
 48 |  * @class
 49 |  * @protected
 50 |  */
 51 | export default class Body {
 52 | 	/**
 53 | 	 * @constructor
 54 | 	 * @param {Number} [x = 0] The starting X coordinate
 55 | 	 * @param {Number} [y = 0] The starting Y coordinate
 56 | 	 * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions
 57 | 	 */
 58 | 	constructor(x = 0, y = 0, padding = 0) {
 59 | 		/**
 60 | 		 * @desc The X coordinate of the body
 61 | 		 * @type {Number}
 62 | 		 */
 63 | 		this.x = x;
 64 | 
 65 | 		/**
 66 | 		 * @desc The Y coordinate of the body
 67 | 		 * @type {Number}
 68 | 		 */
 69 | 		this.y = y;
 70 | 
 71 | 		/**
 72 | 		 * @desc The amount to pad the bounding volume when testing for potential collisions
 73 | 		 * @type {Number}
 74 | 		 */
 75 | 		this.padding = padding;
 76 | 
 77 | 		/** @private */
 78 | 		this._circle = false;
 79 | 
 80 | 		/** @private */
 81 | 		this._polygon = false;
 82 | 
 83 | 		/** @private */
 84 | 		this._point = false;
 85 | 
 86 | 		/** @private */
 87 | 		this._bvh = null;
 88 | 
 89 | 		/** @private */
 90 | 		this._bvh_parent = null;
 91 | 
 92 | 		/** @private */
 93 | 		this._bvh_branch = false;
 94 | 
 95 | 		/** @private */
 96 | 		this._bvh_padding = padding;
 97 | 
 98 | 		/** @private */
 99 | 		this._bvh_min_x = 0;
100 | 
101 | 		/** @private */
102 | 		this._bvh_min_y = 0;
103 | 
104 | 		/** @private */
105 | 		this._bvh_max_x = 0;
106 | 
107 | 		/** @private */
108 | 		this._bvh_max_y = 0;
109 | 	}
110 | 
111 | 	/**
112 | 	 * Determines if the body is colliding with another body
113 | 	 * @param {Circle|Polygon|Point} target The target body to test against
114 | 	 * @param {Result} [result = null] A Result object on which to store information about the collision
115 | 	 * @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own potential collision heuristic)
116 | 	 * @returns {Boolean}
117 | 	 */
118 | 	collides(target, result = null, aabb = true) {
119 | 		return SAT(this, target, result, aabb);
120 | 	}
121 | 
122 | 	/**
123 | 	 * Returns a list of potential collisions
124 | 	 * @returns {Array<Body>}
125 | 	 */
126 | 	potentials() {
127 | 		const bvh = this._bvh;
128 | 
129 | 		if(bvh === null) {
130 | 			throw new Error('Body does not belong to a collision system');
131 | 		}
132 | 
133 | 		return bvh.potentials(this);
134 | 	}
135 | 
136 | 	/**
137 | 	 * Removes the body from its current collision system
138 | 	 */
139 | 	remove() {
140 | 		const bvh = this._bvh;
141 | 
142 | 		if(bvh) {
143 | 			bvh.remove(this, false);
144 | 		}
145 | 	}
146 | 
147 | 	/**
148 | 	 * Creates a {@link Result} used to collect the detailed results of a collision test
149 | 	 */
150 | 	createResult() {
151 | 		return new Result();
152 | 	}
153 | 
154 | 	/**
155 | 	 * Creates a Result used to collect the detailed results of a collision test
156 | 	 */
157 | 	static createResult() {
158 | 		return new Result();
159 | 	}
160 | };
161 | 
162 | 163 |
164 | 165 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /docs/file/src/modules/Circle.mjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/modules/Circle.mjs | collisions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Home 16 | 17 | Reference 18 | Source 19 | 20 | 27 |
28 | 29 | 41 | 42 |

src/modules/Circle.mjs

43 |
import Body from './Body.mjs';
 44 | 
 45 | /**
 46 |  * A circle used to detect collisions
 47 |  * @class
 48 |  */
 49 | export default class Circle extends Body {
 50 | 	/**
 51 | 	 * @constructor
 52 | 	 * @param {Number} [x = 0] The starting X coordinate
 53 | 	 * @param {Number} [y = 0] The starting Y coordinate
 54 | 	 * @param {Number} [radius = 0] The radius
 55 | 	 * @param {Number} [scale = 1] The scale
 56 | 	 * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions
 57 | 	 */
 58 | 	constructor(x = 0, y = 0, radius = 0, scale = 1, padding = 0) {
 59 | 		super(x, y, padding);
 60 | 
 61 | 		/**
 62 | 		 * @desc
 63 | 		 * @type {Number}
 64 | 		 */
 65 | 		this.radius = radius;
 66 | 
 67 | 		/**
 68 | 		 * @desc
 69 | 		 * @type {Number}
 70 | 		 */
 71 | 		this.scale = scale;
 72 | 	}
 73 | 
 74 | 	/**
 75 | 	 * Draws the circle to a CanvasRenderingContext2D's current path
 76 | 	 * @param {CanvasRenderingContext2D} context The context to add the arc to
 77 | 	 */
 78 | 	draw(context) {
 79 | 		const x      = this.x;
 80 | 		const y      = this.y;
 81 | 		const radius = this.radius * this.scale;
 82 | 
 83 | 		context.moveTo(x + radius, y);
 84 | 		context.arc(x, y, radius, 0, Math.PI * 2);
 85 | 	}
 86 | };
 87 | 
88 | 89 |
90 | 91 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /docs/file/src/modules/Point.mjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/modules/Point.mjs | collisions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Home 16 | 17 | Reference 18 | Source 19 | 20 | 27 |
28 | 29 | 41 | 42 |

src/modules/Point.mjs

43 |
import Polygon from './Polygon.mjs';
44 | 
45 | /**
46 |  * A point used to detect collisions
47 |  * @class
48 |  */
49 | export default class Point extends Polygon {
50 | 	/**
51 | 	 * @constructor
52 | 	 * @param {Number} [x = 0] The starting X coordinate
53 | 	 * @param {Number} [y = 0] The starting Y coordinate
54 | 	 * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions
55 | 	 */
56 | 	constructor(x = 0, y = 0, padding = 0) {
57 | 		super(x, y, [[0, 0]], 0, 1, 1, padding);
58 | 
59 | 		/** @private */
60 | 		this._point = true;
61 | 	}
62 | };
63 | 
64 | Point.prototype.setPoints = undefined;
65 | 
66 | 67 |
68 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /docs/file/src/modules/Polygon.mjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/modules/Polygon.mjs | collisions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Home 16 | 17 | Reference 18 | Source 19 | 20 | 27 |
28 | 29 | 41 | 42 |

src/modules/Polygon.mjs

43 |
import Body from './Body.mjs';
 44 | 
 45 | /**
 46 |  * A polygon used to detect collisions
 47 |  * @class
 48 |  */
 49 | export default class Polygon extends Body {
 50 | 	/**
 51 | 	 * @constructor
 52 | 	 * @param {Number} [x = 0] The starting X coordinate
 53 | 	 * @param {Number} [y = 0] The starting Y coordinate
 54 | 	 * @param {Array<Number[]>} [points = []] An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...]
 55 | 	 * @param {Number} [angle = 0] The starting rotation in radians
 56 | 	 * @param {Number} [scale_x = 1] The starting scale along the X axis
 57 | 	 * @param {Number} [scale_y = 1] The starting scale long the Y axis
 58 | 	 * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions
 59 | 	 */
 60 | 	constructor(x = 0, y = 0, points = [], angle = 0, scale_x = 1, scale_y = 1, padding = 0) {
 61 | 		super(x, y, padding);
 62 | 
 63 | 		/**
 64 | 		 * @desc The angle of the body in radians
 65 | 		 * @type {Number}
 66 | 		 */
 67 | 		this.angle = angle;
 68 | 
 69 | 		/**
 70 | 		 * @desc The scale of the body along the X axis
 71 | 		 * @type {Number}
 72 | 		 */
 73 | 		this.scale_x = scale_x;
 74 | 
 75 | 		/**
 76 | 		 * @desc The scale of the body along the Y axis
 77 | 		 * @type {Number}
 78 | 		 */
 79 | 		this.scale_y = scale_y;
 80 | 
 81 | 
 82 | 		/** @private */
 83 | 		this._polygon = true;
 84 | 
 85 | 		/** @private */
 86 | 		this._x = x;
 87 | 
 88 | 		/** @private */
 89 | 		this._y = y;
 90 | 
 91 | 		/** @private */
 92 | 		this._angle = angle;
 93 | 
 94 | 		/** @private */
 95 | 		this._scale_x = scale_x;
 96 | 
 97 | 		/** @private */
 98 | 		this._scale_y = scale_y;
 99 | 
100 | 		/** @private */
101 | 		this._min_x = 0;
102 | 
103 | 		/** @private */
104 | 		this._min_y = 0;
105 | 
106 | 		/** @private */
107 | 		this._max_x = 0;
108 | 
109 | 		/** @private */
110 | 		this._max_y = 0;
111 | 
112 | 		/** @private */
113 | 		this._points = null;
114 | 
115 | 		/** @private */
116 | 		this._coords = null;
117 | 
118 | 		/** @private */
119 | 		this._edges = null;
120 | 
121 | 		/** @private */
122 | 		this._normals = null;
123 | 
124 | 		/** @private */
125 | 		this._dirty_coords = true;
126 | 
127 | 		/** @private */
128 | 		this._dirty_normals = true;
129 | 
130 | 		Polygon.prototype.setPoints.call(this, points);
131 | 	}
132 | 
133 | 	/**
134 | 	 * Draws the polygon to a CanvasRenderingContext2D's current path
135 | 	 * @param {CanvasRenderingContext2D} context The context to add the shape to
136 | 	 */
137 | 	draw(context) {
138 | 		if(
139 | 			this._dirty_coords ||
140 | 			this.x       !== this._x ||
141 | 			this.y       !== this._y ||
142 | 			this.angle   !== this._angle ||
143 | 			this.scale_x !== this._scale_x ||
144 | 			this.scale_y !== this._scale_y
145 | 		) {
146 | 			this._calculateCoords();
147 | 		}
148 | 
149 | 		const coords = this._coords;
150 | 
151 | 		if(coords.length === 2) {
152 | 			context.moveTo(coords[0], coords[1]);
153 | 			context.arc(coords[0], coords[1], 1, 0, Math.PI * 2);
154 | 		}
155 | 		else {
156 | 			context.moveTo(coords[0], coords[1]);
157 | 
158 | 			for(let i = 2; i < coords.length; i += 2) {
159 | 				context.lineTo(coords[i], coords[i + 1]);
160 | 			}
161 | 
162 | 			if(coords.length > 4) {
163 | 				context.lineTo(coords[0], coords[1]);
164 | 			}
165 | 		}
166 | 	}
167 | 
168 | 	/**
169 | 	 * Sets the points making up the polygon. It's important to use this function when changing the polygon's shape to ensure internal data is also updated.
170 | 	 * @param {Array<Number[]>} new_points An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...]
171 | 	 */
172 | 	setPoints(new_points) {
173 | 		const count = new_points.length;
174 | 
175 | 		this._points  = new Float64Array(count * 2);
176 | 		this._coords  = new Float64Array(count * 2);
177 | 		this._edges   = new Float64Array(count * 2);
178 | 		this._normals = new Float64Array(count * 2);
179 | 
180 | 		const points = this._points;
181 | 
182 | 		for(let i = 0, ix = 0, iy = 1; i < count; ++i, ix += 2, iy += 2) {
183 | 			const new_point = new_points[i];
184 | 
185 | 			points[ix] = new_point[0];
186 | 			points[iy] = new_point[1];
187 | 		}
188 | 
189 | 		this._dirty_coords = true;
190 | 	}
191 | 
192 | 	/**
193 | 	 * Calculates and caches the polygon's world coordinates based on its points, angle, and scale
194 | 	 */
195 | 	_calculateCoords() {
196 | 		const x       = this.x;
197 | 		const y       = this.y;
198 | 		const angle   = this.angle;
199 | 		const scale_x = this.scale_x;
200 | 		const scale_y = this.scale_y;
201 | 		const points  = this._points;
202 | 		const coords  = this._coords;
203 | 		const count   = points.length;
204 | 
205 | 		let min_x;
206 | 		let max_x;
207 | 		let min_y;
208 | 		let max_y;
209 | 
210 | 		for(let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) {
211 | 			let coord_x = points[ix] * scale_x;
212 | 			let coord_y = points[iy] * scale_y;
213 | 
214 | 			if(angle) {
215 | 				const cos   = Math.cos(angle);
216 | 				const sin   = Math.sin(angle);
217 | 				const tmp_x = coord_x;
218 | 				const tmp_y = coord_y;
219 | 
220 | 				coord_x = tmp_x * cos - tmp_y * sin;
221 | 				coord_y = tmp_x * sin + tmp_y * cos;
222 | 			}
223 | 
224 | 			coord_x += x;
225 | 			coord_y += y;
226 | 
227 | 			coords[ix] = coord_x;
228 | 			coords[iy] = coord_y;
229 | 
230 | 			if(ix === 0) {
231 | 				min_x = max_x = coord_x;
232 | 				min_y = max_y = coord_y;
233 | 			}
234 | 			else {
235 | 				if(coord_x < min_x) {
236 | 					min_x = coord_x;
237 | 				}
238 | 				else if(coord_x > max_x) {
239 | 					max_x = coord_x;
240 | 				}
241 | 
242 | 				if(coord_y < min_y) {
243 | 					min_y = coord_y;
244 | 				}
245 | 				else if(coord_y > max_y) {
246 | 					max_y = coord_y;
247 | 				}
248 | 			}
249 | 		}
250 | 
251 | 		this._x             = x;
252 | 		this._y             = y;
253 | 		this._angle         = angle;
254 | 		this._scale_x       = scale_x;
255 | 		this._scale_y       = scale_y;
256 | 		this._min_x         = min_x;
257 | 		this._min_y         = min_y;
258 | 		this._max_x         = max_x;
259 | 		this._max_y         = max_y;
260 | 		this._dirty_coords  = false;
261 | 		this._dirty_normals = true;
262 | 	}
263 | 
264 | 	/**
265 | 	 * Calculates the normals and edges of the polygon's sides
266 | 	 */
267 | 	_calculateNormals() {
268 | 		const coords  = this._coords;
269 | 		const edges   = this._edges;
270 | 		const normals = this._normals;
271 | 		const count   = coords.length;
272 | 
273 | 		for(let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) {
274 | 			const next   = ix + 2 < count ? ix + 2 : 0;
275 | 			const x      = coords[next] - coords[ix];
276 | 			const y      = coords[next + 1] - coords[iy];
277 | 			const length = x || y ? Math.sqrt(x * x + y * y) : 0;
278 | 
279 | 			edges[ix]   = x;
280 | 			edges[iy]   = y;
281 | 			normals[ix] = length ? y / length : 0;
282 | 			normals[iy] = length ? -x / length : 0;
283 | 		}
284 | 
285 | 		this._dirty_normals = false;
286 | 	}
287 | };
288 | 
289 | 290 |
291 | 292 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /docs/file/src/modules/Result.mjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/modules/Result.mjs | collisions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Home 16 | 17 | Reference 18 | Source 19 | 20 | 27 |
28 | 29 | 41 | 42 |

src/modules/Result.mjs

43 |
/**
 44 |  * An object used to collect the detailed results of a collision test
 45 |  *
 46 |  * > **Note:** It is highly recommended you recycle the same Result object if possible in order to avoid wasting memory
 47 |  * @class
 48 |  */
 49 | export default class Result {
 50 | 	/**
 51 | 	 * @constructor
 52 | 	 */
 53 | 	constructor() {
 54 | 		/**
 55 | 		 * @desc True if a collision was detected
 56 | 		 * @type {Boolean}
 57 | 		 */
 58 | 		this.collision = false;
 59 | 
 60 | 		/**
 61 | 		 * @desc The source body tested
 62 | 		 * @type {Circle|Polygon|Point}
 63 | 		 */
 64 | 		this.a = null;
 65 | 
 66 | 		/**
 67 | 		 * @desc The target body tested against
 68 | 		 * @type {Circle|Polygon|Point}
 69 | 		 */
 70 | 		this.b = null;
 71 | 
 72 | 		/**
 73 | 		 * @desc True if A is completely contained within B
 74 | 		 * @type {Boolean}
 75 | 		 */
 76 | 		this.a_in_b = false;
 77 | 
 78 | 		/**
 79 | 		 * @desc True if B is completely contained within A
 80 | 		 * @type {Boolean}
 81 | 		 */
 82 | 		this.a_in_b = false;
 83 | 
 84 | 		/**
 85 | 		 * @desc The magnitude of the shortest axis of overlap
 86 | 		 * @type {Number}
 87 | 		 */
 88 | 		this.overlap = 0;
 89 | 
 90 | 		/**
 91 | 		 * @desc The X direction of the shortest axis of overlap
 92 | 		 * @type {Number}
 93 | 		 */
 94 | 		this.overlap_x = 0;
 95 | 
 96 | 		/**
 97 | 		 * @desc The Y direction of the shortest axis of overlap
 98 | 		 * @type {Number}
 99 | 		 */
100 | 		this.overlap_y = 0;
101 | 	}
102 | };
103 | 
104 | 105 |
106 | 107 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /docs/file/src/modules/SAT.mjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/modules/SAT.mjs | collisions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Home 16 | 17 | Reference 18 | Source 19 | 20 | 27 |
28 | 29 | 41 | 42 |

src/modules/SAT.mjs

43 |
/**
 44 |  * Determines if two bodies are colliding using the Separating Axis Theorem
 45 |  * @private
 46 |  * @param {Circle|Polygon|Point} a The source body to test
 47 |  * @param {Circle|Polygon|Point} b The target body to test against
 48 |  * @param {Result} [result = null] A Result object on which to store information about the collision
 49 |  * @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own collision heuristic)
 50 |  * @returns {Boolean}
 51 |  */
 52 | export default function SAT(a, b, result = null, aabb = true) {
 53 | 	const a_polygon = a._polygon;
 54 | 	const b_polygon = b._polygon;
 55 | 
 56 | 	let collision = false;
 57 | 
 58 | 	if(result) {
 59 | 		result.a         = a;
 60 | 		result.b         = b;
 61 | 		result.a_in_b    = true;
 62 | 		result.b_in_a    = true;
 63 | 		result.overlap   = null;
 64 | 		result.overlap_x = 0;
 65 | 		result.overlap_y = 0;
 66 | 	}
 67 | 
 68 | 	if(a_polygon) {
 69 | 		if(
 70 | 			a._dirty_coords ||
 71 | 			a.x       !== a._x ||
 72 | 			a.y       !== a._y ||
 73 | 			a.angle   !== a._angle ||
 74 | 			a.scale_x !== a._scale_x ||
 75 | 			a.scale_y !== a._scale_y
 76 | 		) {
 77 | 			a._calculateCoords();
 78 | 		}
 79 | 	}
 80 | 
 81 | 	if(b_polygon) {
 82 | 		if(
 83 | 			b._dirty_coords ||
 84 | 			b.x       !== b._x ||
 85 | 			b.y       !== b._y ||
 86 | 			b.angle   !== b._angle ||
 87 | 			b.scale_x !== b._scale_x ||
 88 | 			b.scale_y !== b._scale_y
 89 | 		) {
 90 | 			b._calculateCoords();
 91 | 		}
 92 | 	}
 93 | 
 94 | 	if(!aabb || aabbAABB(a, b)) {
 95 | 		if(a_polygon && a._dirty_normals) {
 96 | 			a._calculateNormals();
 97 | 		}
 98 | 
 99 | 		if(b_polygon && b._dirty_normals) {
100 | 			b._calculateNormals();
101 | 		}
102 | 
103 | 		collision = (
104 | 			a_polygon && b_polygon ? polygonPolygon(a, b, result) :
105 | 			a_polygon ? polygonCircle(a, b, result, false) :
106 | 			b_polygon ? polygonCircle(b, a, result, true) :
107 | 			circleCircle(a, b, result)
108 | 		);
109 | 	}
110 | 
111 | 	if(result) {
112 | 		result.collision = collision;
113 | 	}
114 | 
115 | 	return collision;
116 | };
117 | 
118 | /**
119 |  * Determines if two bodies' axis aligned bounding boxes are colliding
120 |  * @param {Circle|Polygon|Point} a The source body to test
121 |  * @param {Circle|Polygon|Point} b The target body to test against
122 |  */
123 | function aabbAABB(a, b) {
124 | 	const a_polygon = a._polygon;
125 | 	const a_x       = a_polygon ? 0 : a.x;
126 | 	const a_y       = a_polygon ? 0 : a.y;
127 | 	const a_radius  = a_polygon ? 0 : a.radius * a.scale;
128 | 	const a_min_x   = a_polygon ? a._min_x : a_x - a_radius;
129 | 	const a_min_y   = a_polygon ? a._min_y : a_y - a_radius;
130 | 	const a_max_x   = a_polygon ? a._max_x : a_x + a_radius;
131 | 	const a_max_y   = a_polygon ? a._max_y : a_y + a_radius;
132 | 
133 | 	const b_polygon = b._polygon;
134 | 	const b_x       = b_polygon ? 0 : b.x;
135 | 	const b_y       = b_polygon ? 0 : b.y;
136 | 	const b_radius  = b_polygon ? 0 : b.radius * b.scale;
137 | 	const b_min_x   = b_polygon ? b._min_x : b_x - b_radius;
138 | 	const b_min_y   = b_polygon ? b._min_y : b_y - b_radius;
139 | 	const b_max_x   = b_polygon ? b._max_x : b_x + b_radius;
140 | 	const b_max_y   = b_polygon ? b._max_y : b_y + b_radius;
141 | 
142 | 	return a_min_x < b_max_x && a_min_y < b_max_y && a_max_x > b_min_x && a_max_y > b_min_y;
143 | }
144 | 
145 | /**
146 |  * Determines if two polygons are colliding
147 |  * @param {Polygon} a The source polygon to test
148 |  * @param {Polygon} b The target polygon to test against
149 |  * @param {Result} [result = null] A Result object on which to store information about the collision
150 |  * @returns {Boolean}
151 |  */
152 | function polygonPolygon(a, b, result = null) {
153 | 	const a_count = a._coords.length;
154 | 	const b_count = b._coords.length;
155 | 
156 | 	// Handle points specially
157 | 	if(a_count === 2 && b_count === 2) {
158 | 		const a_coords = a._coords;
159 | 		const b_coords = b._coords;
160 | 
161 | 		if(result) {
162 | 			result.overlap = 0;
163 | 		}
164 | 
165 | 		return a_coords[0] === b_coords[0] && a_coords[1] === b_coords[1];
166 | 	}
167 | 
168 | 	const a_coords  = a._coords;
169 | 	const b_coords  = b._coords;
170 | 	const a_normals = a._normals;
171 | 	const b_normals = b._normals;
172 | 
173 | 	if(a_count > 2) {
174 | 		for(let ix = 0, iy = 1; ix < a_count; ix += 2, iy += 2) {
175 | 			if(separatingAxis(a_coords, b_coords, a_normals[ix], a_normals[iy], result)) {
176 | 				return false;
177 | 			}
178 | 		}
179 | 	}
180 | 
181 | 	if(b_count > 2) {
182 | 		for(let ix = 0, iy = 1; ix < b_count; ix += 2, iy += 2) {
183 | 			if(separatingAxis(a_coords, b_coords, b_normals[ix], b_normals[iy], result)) {
184 | 				return false;
185 | 			}
186 | 		}
187 | 	}
188 | 
189 | 	return true;
190 | }
191 | 
192 | /**
193 |  * Determines if a polygon and a circle are colliding
194 |  * @param {Polygon} a The source polygon to test
195 |  * @param {Circle} b The target circle to test against
196 |  * @param {Result} [result = null] A Result object on which to store information about the collision
197 |  * @param {Boolean} [reverse = false] Set to true to reverse a and b in the result parameter when testing circle->polygon instead of polygon->circle
198 |  * @returns {Boolean}
199 |  */
200 | function polygonCircle(a, b, result = null, reverse = false) {
201 | 	const a_coords       = a._coords;
202 | 	const a_edges        = a._edges;
203 | 	const a_normals      = a._normals;
204 | 	const b_x            = b.x;
205 | 	const b_y            = b.y;
206 | 	const b_radius       = b.radius * b.scale;
207 | 	const b_radius2      = b_radius * 2;
208 | 	const radius_squared = b_radius * b_radius;
209 | 	const count          = a_coords.length;
210 | 
211 | 	let a_in_b    = true;
212 | 	let b_in_a    = true;
213 | 	let overlap   = null;
214 | 	let overlap_x = 0;
215 | 	let overlap_y = 0;
216 | 
217 | 	// Handle points specially
218 | 	if(count === 2) {
219 | 		const coord_x        = b_x - a_coords[0];
220 | 		const coord_y        = b_y - a_coords[1];
221 | 		const length_squared = coord_x * coord_x + coord_y * coord_y;
222 | 
223 | 		if(length_squared > radius_squared) {
224 | 			return false;
225 | 		}
226 | 
227 | 		if(result) {
228 | 			const length = Math.sqrt(length_squared);
229 | 
230 | 			overlap   = b_radius - length;
231 | 			overlap_x = coord_x / length;
232 | 			overlap_y = coord_y / length;
233 | 			b_in_a    = false;
234 | 		}
235 | 	}
236 | 	else {
237 | 		for(let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) {
238 | 			const coord_x = b_x - a_coords[ix];
239 | 			const coord_y = b_y - a_coords[iy];
240 | 			const edge_x  = a_edges[ix];
241 | 			const edge_y  = a_edges[iy];
242 | 			const dot     = coord_x * edge_x + coord_y * edge_y;
243 | 			const region  = dot < 0 ? -1 : dot > edge_x * edge_x + edge_y * edge_y ? 1 : 0;
244 | 
245 | 			let tmp_overlapping = false;
246 | 			let tmp_overlap     = 0;
247 | 			let tmp_overlap_x   = 0;
248 | 			let tmp_overlap_y   = 0;
249 | 
250 | 			if(result && a_in_b && coord_x * coord_x + coord_y * coord_y > radius_squared) {
251 | 				a_in_b = false;
252 | 			}
253 | 
254 | 			if(region) {
255 | 				const left     = region === -1;
256 | 				const other_x  = left ? (ix === 0 ? count - 2 : ix - 2) : (ix === count - 2 ? 0 : ix + 2);
257 | 				const other_y  = other_x + 1;
258 | 				const coord2_x = b_x - a_coords[other_x];
259 | 				const coord2_y = b_y - a_coords[other_y];
260 | 				const edge2_x  = a_edges[other_x];
261 | 				const edge2_y  = a_edges[other_y];
262 | 				const dot2     = coord2_x * edge2_x + coord2_y * edge2_y;
263 | 				const region2  = dot2 < 0 ? -1 : dot2 > edge2_x * edge2_x + edge2_y * edge2_y ? 1 : 0;
264 | 
265 | 				if(region2 === -region) {
266 | 					const target_x       = left ? coord_x : coord2_x;
267 | 					const target_y       = left ? coord_y : coord2_y;
268 | 					const length_squared = target_x * target_x + target_y * target_y;
269 | 
270 | 					if(length_squared > radius_squared) {
271 | 						return false;
272 | 					}
273 | 
274 | 					if(result) {
275 | 						const length = Math.sqrt(length_squared);
276 | 
277 | 						tmp_overlapping = true;
278 | 						tmp_overlap     = b_radius - length;
279 | 						tmp_overlap_x   = target_x / length;
280 | 						tmp_overlap_y   = target_y / length;
281 | 						b_in_a          = false;
282 | 					}
283 | 				}
284 | 			}
285 | 			else {
286 | 				const normal_x        = a_normals[ix];
287 | 				const normal_y        = a_normals[iy];
288 | 				const length          = coord_x * normal_x + coord_y * normal_y;
289 | 				const absolute_length = length < 0 ? -length : length;
290 | 
291 | 				if(length > 0 && absolute_length > b_radius) {
292 | 					return false;
293 | 				}
294 | 
295 | 				if(result) {
296 | 					tmp_overlapping = true;
297 | 					tmp_overlap     = b_radius - length;
298 | 					tmp_overlap_x   = normal_x;
299 | 					tmp_overlap_y   = normal_y;
300 | 
301 | 					if(b_in_a && length >= 0 || tmp_overlap < b_radius2) {
302 | 						b_in_a = false;
303 | 					}
304 | 				}
305 | 			}
306 | 
307 | 			if(tmp_overlapping && (overlap === null || overlap > tmp_overlap)) {
308 | 				overlap   = tmp_overlap;
309 | 				overlap_x = tmp_overlap_x;
310 | 				overlap_y = tmp_overlap_y;
311 | 			}
312 | 		}
313 | 	}
314 | 
315 | 	if(result) {
316 | 		result.a_in_b    = reverse ? b_in_a : a_in_b;
317 | 		result.b_in_a    = reverse ? a_in_b : b_in_a;
318 | 		result.overlap   = overlap;
319 | 		result.overlap_x = reverse ? -overlap_x : overlap_x;
320 | 		result.overlap_y = reverse ? -overlap_y : overlap_y;
321 | 	}
322 | 
323 | 	return true;
324 | }
325 | 
326 | /**
327 |  * Determines if two circles are colliding
328 |  * @param {Circle} a The source circle to test
329 |  * @param {Circle} b The target circle to test against
330 |  * @param {Result} [result = null] A Result object on which to store information about the collision
331 |  * @returns {Boolean}
332 |  */
333 | function circleCircle(a, b, result = null) {
334 | 	const a_radius       = a.radius * a.scale;
335 | 	const b_radius       = b.radius * b.scale;
336 | 	const difference_x   = b.x - a.x;
337 | 	const difference_y   = b.y - a.y;
338 | 	const radius_sum     = a_radius + b_radius;
339 | 	const length_squared = difference_x * difference_x + difference_y * difference_y;
340 | 
341 | 	if(length_squared > radius_sum * radius_sum) {
342 | 		return false;
343 | 	}
344 | 
345 | 	if(result) {
346 | 		const length = Math.sqrt(length_squared);
347 | 
348 | 		result.a_in_b    = a_radius <= b_radius && length <= b_radius - a_radius;
349 | 		result.b_in_a    = b_radius <= a_radius && length <= a_radius - b_radius;
350 | 		result.overlap   = radius_sum - length;
351 | 		result.overlap_x = difference_x / length;
352 | 		result.overlap_y = difference_y / length;
353 | 	}
354 | 
355 | 	return true;
356 | }
357 | 
358 | /**
359 |  * Determines if two polygons are separated by an axis
360 |  * @param {Array<Number[]>} a_coords The coordinates of the polygon to test
361 |  * @param {Array<Number[]>} b_coords The coordinates of the polygon to test against
362 |  * @param {Number} x The X direction of the axis
363 |  * @param {Number} y The Y direction of the axis
364 |  * @param {Result} [result = null] A Result object on which to store information about the collision
365 |  * @returns {Boolean}
366 |  */
367 | function separatingAxis(a_coords, b_coords, x, y, result = null) {
368 | 	const a_count = a_coords.length;
369 | 	const b_count = b_coords.length;
370 | 
371 | 	if(!a_count || !b_count) {
372 | 		return true;
373 | 	}
374 | 
375 | 	let a_start = null;
376 | 	let a_end   = null;
377 | 	let b_start = null;
378 | 	let b_end   = null;
379 | 
380 | 	for(let ix = 0, iy = 1; ix < a_count; ix += 2, iy += 2) {
381 | 		const dot = a_coords[ix] * x + a_coords[iy] * y;
382 | 
383 | 		if(a_start === null || a_start > dot) {
384 | 			a_start = dot;
385 | 		}
386 | 
387 | 		if(a_end === null || a_end < dot) {
388 | 			a_end = dot;
389 | 		}
390 | 	}
391 | 
392 | 	for(let ix = 0, iy = 1; ix < b_count; ix += 2, iy += 2) {
393 | 		const dot = b_coords[ix] * x + b_coords[iy] * y;
394 | 
395 | 		if(b_start === null || b_start > dot) {
396 | 			b_start = dot;
397 | 		}
398 | 
399 | 		if(b_end === null || b_end < dot) {
400 | 			b_end = dot;
401 | 		}
402 | 	}
403 | 
404 | 	if(a_start > b_end || a_end < b_start) {
405 | 		return true;
406 | 	}
407 | 
408 | 	if(result) {
409 | 		let overlap = 0;
410 | 
411 | 		if(a_start < b_start) {
412 | 			result.a_in_b = false;
413 | 
414 | 			if(a_end < b_end) {
415 | 				overlap       = a_end - b_start;
416 | 				result.b_in_a = false;
417 | 			}
418 | 			else {
419 | 				const option1 = a_end - b_start;
420 | 				const option2 = b_end - a_start;
421 | 
422 | 				overlap = option1 < option2 ? option1 : -option2;
423 | 			}
424 | 		}
425 | 		else {
426 | 			result.b_in_a = false;
427 | 
428 | 			if(a_end > b_end) {
429 | 				overlap       = a_start - b_end;
430 | 				result.a_in_b = false;
431 | 			}
432 | 			else {
433 | 				const option1 = a_end - b_start;
434 | 				const option2 = b_end - a_start;
435 | 
436 | 				overlap = option1 < option2 ? option1 : -option2;
437 | 			}
438 | 		}
439 | 
440 | 		const current_overlap  = result.overlap;
441 | 		const absolute_overlap = overlap < 0 ? -overlap : overlap;
442 | 
443 | 		if(current_overlap === null || current_overlap > absolute_overlap) {
444 | 			const sign = overlap < 0 ? -1 : 1;
445 | 
446 | 			result.overlap   = absolute_overlap;
447 | 			result.overlap_x = x * sign;
448 | 			result.overlap_y = y * sign;
449 | 		}
450 | 	}
451 | 
452 | 	return false;
453 | }
454 | 
455 | 456 |
457 | 458 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | -------------------------------------------------------------------------------- /docs/identifiers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Reference | collisions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Home 16 | 17 | Reference 18 | Source 19 | 20 | 27 |
28 | 29 | 41 | 42 |

References

43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 | 52 | 53 | 54 | 61 | 77 | 81 | 82 | 83 |
summary
55 | public 56 | 57 | 58 | 59 | 60 | 62 |
63 |

64 | C 65 | 66 | 67 | Collisions 68 |

69 |
70 |
71 | 72 | 73 |

A collision system used to track bodies in order to improve collision detection performance

74 |
75 |
76 |
78 | 79 | 80 |
84 |
85 |
86 |
87 |

modules

88 |
89 | 90 | 91 | 92 | 93 | 100 | 116 | 120 | 121 | 122 | 129 | 145 | 149 | 150 | 151 | 158 | 174 | 178 | 179 | 180 | 187 | 203 | 207 | 208 | 209 | 216 | 232 | 236 | 237 | 238 |
summary
94 | protected 95 | 96 | 97 | 98 | 99 | 101 |
102 |

103 | C 104 | 105 | 106 | Body 107 |

108 |
109 |
110 | 111 | 112 |

The base class for bodies used to detect collisions

113 |
114 |
115 |
117 | 118 | 119 |
123 | public 124 | 125 | 126 | 127 | 128 | 130 |
131 |

132 | C 133 | 134 | 135 | Circle 136 |

137 |
138 |
139 | 140 | 141 |

A circle used to detect collisions

142 |
143 |
144 |
146 | 147 | 148 |
152 | public 153 | 154 | 155 | 156 | 157 | 159 |
160 |

161 | C 162 | 163 | 164 | Point 165 |

166 |
167 |
168 | 169 | 170 |

A point used to detect collisions

171 |
172 |
173 |
175 | 176 | 177 |
181 | public 182 | 183 | 184 | 185 | 186 | 188 |
189 |

190 | C 191 | 192 | 193 | Polygon 194 |

195 |
196 |
197 | 198 | 199 |

A polygon used to detect collisions

200 |
201 |
202 |
204 | 205 | 206 |
210 | public 211 | 212 | 213 | 214 | 215 | 217 |
218 |

219 | C 220 | 221 | 222 | Result 223 |

224 |
225 |
226 | 227 | 228 |

An object used to collect the detailed results of a collision test

229 |
230 |
231 |
233 | 234 | 235 |
239 |
240 |
241 |
242 | 243 |
244 |
Directories
245 | 246 |
247 |
248 |
249 | 250 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /docs/image/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | document 13 | document 14 | @ratio@ 15 | @ratio@ 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/image/esdoc-logo-mini-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sinova/Collisions/f59299c1a333542187db37e99dcf8fa65bfa7ab3/docs/image/esdoc-logo-mini-black.png -------------------------------------------------------------------------------- /docs/image/esdoc-logo-mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sinova/Collisions/f59299c1a333542187db37e99dcf8fa65bfa7ab3/docs/image/esdoc-logo-mini.png -------------------------------------------------------------------------------- /docs/image/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sinova/Collisions/f59299c1a333542187db37e99dcf8fa65bfa7ab3/docs/image/github.png -------------------------------------------------------------------------------- /docs/image/manual-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | manual 13 | manual 14 | @value@ 15 | @value@ 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/image/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sinova/Collisions/f59299c1a333542187db37e99dcf8fa65bfa7ab3/docs/image/search.png -------------------------------------------------------------------------------- /docs/script/inherited-summary.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function toggle(ev) { 3 | var button = ev.target; 4 | var parent = ev.target.parentElement; 5 | while(parent) { 6 | if (parent.tagName === 'TABLE' && parent.classList.contains('summary')) break; 7 | parent = parent.parentElement; 8 | } 9 | 10 | if (!parent) return; 11 | 12 | var tbody = parent.querySelector('tbody'); 13 | if (button.classList.contains('opened')) { 14 | button.classList.remove('opened'); 15 | button.classList.add('closed'); 16 | tbody.style.display = 'none'; 17 | } else { 18 | button.classList.remove('closed'); 19 | button.classList.add('opened'); 20 | tbody.style.display = 'block'; 21 | } 22 | } 23 | 24 | var buttons = document.querySelectorAll('.inherited-summary thead .toggle'); 25 | for (var i = 0; i < buttons.length; i++) { 26 | buttons[i].addEventListener('click', toggle); 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /docs/script/inner-link.js: -------------------------------------------------------------------------------- 1 | // inner link(#foo) can not correctly scroll, because page has fixed header, 2 | // so, I manually scroll. 3 | (function(){ 4 | var matched = location.hash.match(/errorLines=([\d,]+)/); 5 | if (matched) return; 6 | 7 | function adjust() { 8 | window.scrollBy(0, -55); 9 | var el = document.querySelector('.inner-link-active'); 10 | if (el) el.classList.remove('inner-link-active'); 11 | 12 | // ``[ ] . ' " @`` are not valid in DOM id. so must escape these. 13 | var id = location.hash.replace(/([\[\].'"@$])/g, '\\$1'); 14 | var el = document.querySelector(id); 15 | if (el) el.classList.add('inner-link-active'); 16 | } 17 | 18 | window.addEventListener('hashchange', adjust); 19 | 20 | if (location.hash) { 21 | setTimeout(adjust, 0); 22 | } 23 | })(); 24 | 25 | (function(){ 26 | var els = document.querySelectorAll('[href^="#"]'); 27 | var href = location.href.replace(/#.*$/, ''); // remove existed hash 28 | for (var i = 0; i < els.length; i++) { 29 | var el = els[i]; 30 | el.href = href + el.getAttribute('href'); // because el.href is absolute path 31 | } 32 | })(); 33 | -------------------------------------------------------------------------------- /docs/script/manual.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var matched = location.pathname.match(/\/(manual\/.*\.html)$/); 3 | if (!matched) return; 4 | 5 | var currentName = matched[1]; 6 | var cssClass = '.navigation .manual-toc li[data-link="' + currentName + '"]'; 7 | var styleText = cssClass + '{ display: block; }\n'; 8 | styleText += cssClass + '.indent-h1 a { color: #039BE5 }'; 9 | var style = document.createElement('style'); 10 | style.textContent = styleText; 11 | document.querySelector('head').appendChild(style); 12 | })(); 13 | -------------------------------------------------------------------------------- /docs/script/patch-for-local.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | if (location.protocol === 'file:') { 3 | var elms = document.querySelectorAll('a[href="./"]'); 4 | for (var i = 0; i < elms.length; i++) { 5 | elms[i].href = './index.html'; 6 | } 7 | } 8 | })(); 9 | -------------------------------------------------------------------------------- /docs/script/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/script/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | !function(){/* 2 | 3 | Copyright (C) 2006 Google Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | window.PR_SHOULD_USE_CONTINUATION=!0; 18 | (function(){function T(a){function d(e){var b=e.charCodeAt(0);if(92!==b)return b;var a=e.charAt(1);return(b=w[a])?b:"0"<=a&&"7">=a?parseInt(e.substring(1),8):"u"===a||"x"===a?parseInt(e.substring(2),16):e.charCodeAt(1)}function f(e){if(32>e)return(16>e?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return"\\"===e||"-"===e||"]"===e||"^"===e?"\\"+e:e}function b(e){var b=e.substring(1,e.length-1).match(/\\u[0-9A-Fa-f]{4}|\\x[0-9A-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\s\S]|-|[^-\\]/g);e= 19 | [];var a="^"===b[0],c=["["];a&&c.push("^");for(var a=a?1:0,g=b.length;ak||122k||90k||122h[0]&&(h[1]+1>h[0]&&c.push("-"),c.push(f(h[1])));c.push("]");return c.join("")}function v(e){for(var a=e.source.match(/(?:\[(?:[^\x5C\x5D]|\\[\s\S])*\]|\\u[A-Fa-f0-9]{4}|\\x[A-Fa-f0-9]{2}|\\[0-9]+|\\[^ux0-9]|\(\?[:!=]|[\(\)\^]|[^\x5B\x5C\(\)\^]+)/g),c=a.length,d=[],g=0,h=0;g/,null])):d.push(["com",/^#[^\r\n]*/,null,"#"]));a.cStyleComments&&(f.push(["com",/^\/\/[^\r\n]*/,null]),f.push(["com",/^\/\*[\s\S]*?(?:\*\/|$)/,null]));if(b=a.regexLiterals){var v=(b=1|\\/=?|::?|<>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+ 28 | ("/(?=[^/*"+b+"])(?:[^/\\x5B\\x5C"+b+"]|\\x5C"+v+"|\\x5B(?:[^\\x5C\\x5D"+b+"]|\\x5C"+v+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&f.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&f.push(["kwd",new RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),null]);d.push(["pln",/^\s+/,null," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");f.push(["lit",/^@[a-z_$][a-z_$@0-9]*/i,null],["typ",/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],["pln",/^[a-z_$][a-z_$@0-9]*/i, 29 | null],["lit",/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],["pln",/^\\[\s\S]?/,null],["pun",new RegExp(b),null]);return G(d,f)}function L(a,d,f){function b(a){var c=a.nodeType;if(1==c&&!A.test(a.className))if("br"===a.nodeName)v(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((3==c||4==c)&&f){var d=a.nodeValue,q=d.match(n);q&&(c=d.substring(0,q.index),a.nodeValue=c,(d=d.substring(q.index+q[0].length))&& 30 | a.parentNode.insertBefore(l.createTextNode(d),a.nextSibling),v(a),c||a.parentNode.removeChild(a))}}function v(a){function b(a,c){var d=c?a.cloneNode(!1):a,k=a.parentNode;if(k){var k=b(k,1),e=a.nextSibling;k.appendChild(d);for(var f=e;f;f=e)e=f.nextSibling,k.appendChild(f)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;a=b(a.nextSibling,0);for(var d;(d=a.parentNode)&&1===d.nodeType;)a=d;c.push(a)}for(var A=/(?:^|\s)nocode(?:\s|$)/,n=/\r\n?|\n/,l=a.ownerDocument,m=l.createElement("li");a.firstChild;)m.appendChild(a.firstChild); 31 | for(var c=[m],p=0;p=+v[1],d=/\n/g,A=a.a,n=A.length,f=0,l=a.c,m=l.length,b=0,c=a.g,p=c.length,w=0;c[p]=n;var r,e;for(e=r=0;e=h&&(b+=2);f>=k&&(w+=2)}}finally{g&&(g.style.display=a)}}catch(x){E.console&&console.log(x&&x.stack||x)}}var E=window,C=["break,continue,do,else,for,if,return,while"], 34 | F=[[C,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,restrict,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],H=[F,"alignas,alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,noexcept,noreturn,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"], 35 | O=[F,"abstract,assert,boolean,byte,extends,finally,final,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],P=[F,"abstract,add,alias,as,ascending,async,await,base,bool,by,byte,checked,decimal,delegate,descending,dynamic,event,finally,fixed,foreach,from,get,global,group,implicit,in,interface,internal,into,is,join,let,lock,null,object,out,override,orderby,params,partial,readonly,ref,remove,sbyte,sealed,select,set,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,value,var,virtual,where,yield"], 36 | F=[F,"abstract,async,await,constructor,debugger,enum,eval,export,function,get,implements,instanceof,interface,let,null,set,undefined,var,with,yield,Infinity,NaN"],Q=[C,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],R=[C,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],C=[C,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"], 37 | S=/^(DIR|FILE|array|vector|(de|priority_)?queue|(forward_)?list|stack|(const_)?(reverse_)?iterator|(unordered_)?(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,W=/\S/,X=y({keywords:[H,P,O,F,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",Q,R,C],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),I={};t(X,["default-code"]);t(G([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),"default-markup htm html mxml xhtml xml xsl".split(" "));t(G([["pln",/^[\s]+/,null," \t\r\n"],["atv",/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null, 39 | "\"'"]],[["tag",/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],["pun",/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);t(G([],[["atv",/^[\s\S]+/]]),["uq.val"]);t(y({keywords:H, 40 | hashComments:!0,cStyleComments:!0,types:S}),"c cc cpp cxx cyc m".split(" "));t(y({keywords:"null,true,false"}),["json"]);t(y({keywords:P,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:S}),["cs"]);t(y({keywords:O,cStyleComments:!0}),["java"]);t(y({keywords:C,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);t(y({keywords:Q,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);t(y({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END", 41 | hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);t(y({keywords:R,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);t(y({keywords:F,cStyleComments:!0,regexLiterals:!0}),["javascript","js","ts","typescript"]);t(y({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0, 42 | regexLiterals:!0}),["coffee"]);t(G([],[["str",/^[\s\S]+/]]),["regex"]);var Y=E.PR={createSimpleLexer:G,registerLangHandler:t,sourceDecorator:y,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:E.prettyPrintOne=function(a,d,f){f=f||!1;d=d||null;var b=document.createElement("div");b.innerHTML="
"+a+"
"; 43 | b=b.firstChild;f&&L(b,f,!0);M({j:d,m:f,h:b,l:1,a:null,i:null,c:null,g:null});return b.innerHTML},prettyPrint:E.prettyPrint=function(a,d){function f(){for(var b=E.PR_SHOULD_USE_CONTINUATION?c.now()+250:Infinity;p' + pair[2] + ''); 35 | } 36 | } 37 | 38 | var innerHTML = ''; 39 | for (kind in html) { 40 | var list = html[kind]; 41 | if (!list.length) continue; 42 | innerHTML += '
  • ' + kind + '
  • \n' + list.join('\n'); 43 | } 44 | result.innerHTML = innerHTML; 45 | if (innerHTML) result.style.display = 'block'; 46 | selectedIndex = -1; 47 | }); 48 | 49 | // down, up and enter key are pressed, select search result. 50 | input.addEventListener('keydown', function(ev){ 51 | if (ev.keyCode === 40) { 52 | // arrow down 53 | var current = result.children[selectedIndex]; 54 | var selected = result.children[selectedIndex + 1]; 55 | if (selected && selected.classList.contains('search-separator')) { 56 | var selected = result.children[selectedIndex + 2]; 57 | selectedIndex++; 58 | } 59 | 60 | if (selected) { 61 | if (current) current.classList.remove('selected'); 62 | selectedIndex++; 63 | selected.classList.add('selected'); 64 | } 65 | } else if (ev.keyCode === 38) { 66 | // arrow up 67 | var current = result.children[selectedIndex]; 68 | var selected = result.children[selectedIndex - 1]; 69 | if (selected && selected.classList.contains('search-separator')) { 70 | var selected = result.children[selectedIndex - 2]; 71 | selectedIndex--; 72 | } 73 | 74 | if (selected) { 75 | if (current) current.classList.remove('selected'); 76 | selectedIndex--; 77 | selected.classList.add('selected'); 78 | } 79 | } else if (ev.keyCode === 13) { 80 | // enter 81 | var current = result.children[selectedIndex]; 82 | if (current) { 83 | var link = current.querySelector('a'); 84 | if (link) location.href = link.href; 85 | } 86 | } else { 87 | return; 88 | } 89 | 90 | ev.preventDefault(); 91 | }); 92 | 93 | // select search result when search result is mouse over. 94 | result.addEventListener('mousemove', function(ev){ 95 | var current = result.children[selectedIndex]; 96 | if (current) current.classList.remove('selected'); 97 | 98 | var li = ev.target; 99 | while (li) { 100 | if (li.nodeName === 'LI') break; 101 | li = li.parentElement; 102 | } 103 | 104 | if (li) { 105 | selectedIndex = Array.prototype.indexOf.call(result.children, li); 106 | li.classList.add('selected'); 107 | } 108 | }); 109 | 110 | // clear search result when body is clicked. 111 | document.body.addEventListener('click', function(ev){ 112 | selectedIndex = -1; 113 | result.style.display = 'none'; 114 | result.innerHTML = ''; 115 | }); 116 | 117 | })(); 118 | -------------------------------------------------------------------------------- /docs/script/test-summary.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function toggle(ev) { 3 | var button = ev.target; 4 | var parent = ev.target.parentElement; 5 | while(parent) { 6 | if (parent.tagName === 'TR' && parent.classList.contains('test-interface')) break; 7 | parent = parent.parentElement; 8 | } 9 | 10 | if (!parent) return; 11 | 12 | var direction; 13 | if (button.classList.contains('opened')) { 14 | button.classList.remove('opened'); 15 | button.classList.add('closed'); 16 | direction = 'closed'; 17 | } else { 18 | button.classList.remove('closed'); 19 | button.classList.add('opened'); 20 | direction = 'opened'; 21 | } 22 | 23 | var targetDepth = parseInt(parent.dataset.testDepth, 10) + 1; 24 | var nextElement = parent.nextElementSibling; 25 | while (nextElement) { 26 | var depth = parseInt(nextElement.dataset.testDepth, 10); 27 | if (depth >= targetDepth) { 28 | if (direction === 'opened') { 29 | if (depth === targetDepth) nextElement.style.display = ''; 30 | } else if (direction === 'closed') { 31 | nextElement.style.display = 'none'; 32 | var innerButton = nextElement.querySelector('.toggle'); 33 | if (innerButton && innerButton.classList.contains('opened')) { 34 | innerButton.classList.remove('opened'); 35 | innerButton.classList.add('closed'); 36 | } 37 | } 38 | } else { 39 | break; 40 | } 41 | nextElement = nextElement.nextElementSibling; 42 | } 43 | } 44 | 45 | var buttons = document.querySelectorAll('.test-summary tr.test-interface .toggle'); 46 | for (var i = 0; i < buttons.length; i++) { 47 | buttons[i].addEventListener('click', toggle); 48 | } 49 | 50 | var topDescribes = document.querySelectorAll('.test-summary tr[data-test-depth="0"]'); 51 | for (var i = 0; i < topDescribes.length; i++) { 52 | topDescribes[i].style.display = ''; 53 | } 54 | })(); 55 | -------------------------------------------------------------------------------- /docs/source.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Source | collisions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | Home 16 | 17 | Reference 18 | Source 19 | 20 | 27 |
    28 | 29 | 41 | 42 |

    Source 110/110

    43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
    FileIdentifierDocumentSizeLinesUpdated
    src/Collisions.mjsCollisions100 %15/154552 byte1632017-12-01 05:47:46 (UTC)
    src/modules/BVH.mjs-100 %11/1111371 byte4102017-12-01 05:48:46 (UTC)
    src/modules/BVHBranch.mjs-100 %15/151205 byte732017-12-01 05:28:16 (UTC)
    src/modules/Body.mjsBody100 %21/212444 byte1182017-11-02 18:10:13 (UTC)
    src/modules/Circle.mjsCircle100 %5/51036 byte442017-11-03 05:42:08 (UTC)
    src/modules/Point.mjsPoint100 %3/3555 byte222017-12-01 05:27:54 (UTC)
    src/modules/Polygon.mjsPolygon100 %25/255619 byte2452017-12-01 05:28:06 (UTC)
    src/modules/Result.mjsResult100 %9/91158 byte602017-12-01 05:27:26 (UTC)
    src/modules/SAT.mjs-100 %6/611274 byte4112017-12-01 05:27:21 (UTC)
    131 |
    132 | 133 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collisions", 3 | "version": "2.0.14", 4 | "description": "Collision detection for circles, polygons, and points", 5 | "module": "src/Collisions.mjs", 6 | "types": "collisions.d.ts", 7 | "scripts": { 8 | "build": "rm -rf ./docs/* && esdoc && webpack" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Sinova/Collisions.git" 13 | }, 14 | "keywords": [ 15 | "Collision", 16 | "Separating Axis Theorem", 17 | "Bounding Volume Hierarchy", 18 | "SAT", 19 | "BVH", 20 | "Circle", 21 | "Polygon", 22 | "Line", 23 | "Shape", 24 | "Separating", 25 | "Axis", 26 | "Theorem", 27 | "Bounding", 28 | "Volume", 29 | "Hierarchy" 30 | ], 31 | "author": "Samuel Hodge", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/Sinova/Collisions/issues" 35 | }, 36 | "homepage": "https://github.com/Sinova/Collisions#readme", 37 | "devDependencies": { 38 | "esdoc": "^1.0.4", 39 | "esdoc-standard-plugin": "^1.0.0", 40 | "html-webpack-plugin": "^2.30.1", 41 | "npm": "^5.5.1", 42 | "webpack": "^3.9.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Collisions.mjs: -------------------------------------------------------------------------------- 1 | import BVH from './modules/BVH.mjs'; 2 | import Circle from './modules/Circle.mjs'; 3 | import Polygon from './modules/Polygon.mjs'; 4 | import Point from './modules/Point.mjs'; 5 | import Result from './modules/Result.mjs'; 6 | import SAT from './modules/SAT.mjs'; 7 | 8 | /** 9 | * A collision system used to track bodies in order to improve collision detection performance 10 | * @class 11 | */ 12 | class Collisions { 13 | /** 14 | * @constructor 15 | */ 16 | constructor() { 17 | /** @private */ 18 | this._bvh = new BVH(); 19 | } 20 | 21 | /** 22 | * Creates a {@link Circle} and inserts it into the collision system 23 | * @param {Number} [x = 0] The starting X coordinate 24 | * @param {Number} [y = 0] The starting Y coordinate 25 | * @param {Number} [radius = 0] The radius 26 | * @param {Number} [scale = 1] The scale 27 | * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 28 | * @returns {Circle} 29 | */ 30 | createCircle(x = 0, y = 0, radius = 0, scale = 1, padding = 0) { 31 | const body = new Circle(x, y, radius, scale, padding); 32 | 33 | this._bvh.insert(body); 34 | 35 | return body; 36 | } 37 | 38 | /** 39 | * Creates a {@link Polygon} and inserts it into the collision system 40 | * @param {Number} [x = 0] The starting X coordinate 41 | * @param {Number} [y = 0] The starting Y coordinate 42 | * @param {Array} [points = []] An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...] 43 | * @param {Number} [angle = 0] The starting rotation in radians 44 | * @param {Number} [scale_x = 1] The starting scale along the X axis 45 | * @param {Number} [scale_y = 1] The starting scale long the Y axis 46 | * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 47 | * @returns {Polygon} 48 | */ 49 | createPolygon(x = 0, y = 0, points = [[0, 0]], angle = 0, scale_x = 1, scale_y = 1, padding = 0) { 50 | const body = new Polygon(x, y, points, angle, scale_x, scale_y, padding); 51 | 52 | this._bvh.insert(body); 53 | 54 | return body; 55 | } 56 | 57 | /** 58 | * Creates a {@link Point} and inserts it into the collision system 59 | * @param {Number} [x = 0] The starting X coordinate 60 | * @param {Number} [y = 0] The starting Y coordinate 61 | * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 62 | * @returns {Point} 63 | */ 64 | createPoint(x = 0, y = 0, padding = 0) { 65 | const body = new Point(x, y, padding); 66 | 67 | this._bvh.insert(body); 68 | 69 | return body; 70 | } 71 | 72 | /** 73 | * Creates a {@link Result} used to collect the detailed results of a collision test 74 | */ 75 | createResult() { 76 | return new Result(); 77 | } 78 | 79 | /** 80 | * Creates a Result used to collect the detailed results of a collision test 81 | */ 82 | static createResult() { 83 | return new Result(); 84 | } 85 | 86 | /** 87 | * Inserts bodies into the collision system 88 | * @param {...Circle|...Polygon|...Point} bodies 89 | */ 90 | insert(...bodies) { 91 | for(const body of bodies) { 92 | this._bvh.insert(body, false); 93 | } 94 | 95 | return this; 96 | } 97 | 98 | /** 99 | * Removes bodies from the collision system 100 | * @param {...Circle|...Polygon|...Point} bodies 101 | */ 102 | remove(...bodies) { 103 | for(const body of bodies) { 104 | this._bvh.remove(body, false); 105 | } 106 | 107 | return this; 108 | } 109 | 110 | /** 111 | * Updates the collision system. This should be called before any collisions are tested. 112 | */ 113 | update() { 114 | this._bvh.update(); 115 | 116 | return this; 117 | } 118 | 119 | /** 120 | * Draws the bodies within the system to a CanvasRenderingContext2D's current path 121 | * @param {CanvasRenderingContext2D} context The context to draw to 122 | */ 123 | draw(context) { 124 | return this._bvh.draw(context); 125 | } 126 | 127 | /** 128 | * Draws the system's BVH to a CanvasRenderingContext2D's current path. This is useful for testing out different padding values for bodies. 129 | * @param {CanvasRenderingContext2D} context The context to draw to 130 | */ 131 | drawBVH(context) { 132 | return this._bvh.drawBVH(context); 133 | } 134 | 135 | /** 136 | * Returns a list of potential collisions for a body 137 | * @param {Circle|Polygon|Point} body The body to test for potential collisions against 138 | * @returns {Array} 139 | */ 140 | potentials(body) { 141 | return this._bvh.potentials(body); 142 | } 143 | 144 | /** 145 | * Determines if two bodies are colliding 146 | * @param {Circle|Polygon|Point} target The target body to test against 147 | * @param {Result} [result = null] A Result object on which to store information about the collision 148 | * @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own potential collision heuristic) 149 | * @returns {Boolean} 150 | */ 151 | collides(source, target, result = null, aabb = true) { 152 | return SAT(source, target, result, aabb); 153 | } 154 | }; 155 | 156 | export { 157 | Collisions as default, 158 | Collisions, 159 | Result, 160 | Circle, 161 | Polygon, 162 | Point, 163 | }; 164 | -------------------------------------------------------------------------------- /src/modules/BVH.mjs: -------------------------------------------------------------------------------- 1 | import BVHBranch from './BVHBranch.mjs'; 2 | 3 | /** 4 | * A Bounding Volume Hierarchy (BVH) used to find potential collisions quickly 5 | * @class 6 | * @private 7 | */ 8 | export default class BVH { 9 | /** 10 | * @constructor 11 | */ 12 | constructor() { 13 | /** @private */ 14 | this._hierarchy = null; 15 | 16 | /** @private */ 17 | this._bodies = []; 18 | 19 | /** @private */ 20 | this._dirty_branches = []; 21 | } 22 | 23 | /** 24 | * Inserts a body into the BVH 25 | * @param {Circle|Polygon|Point} body The body to insert 26 | * @param {Boolean} [updating = false] Set to true if the body already exists in the BVH (used internally when updating the body's position) 27 | */ 28 | insert(body, updating = false) { 29 | if(!updating) { 30 | const bvh = body._bvh; 31 | 32 | if(bvh && bvh !== this) { 33 | throw new Error('Body belongs to another collision system'); 34 | } 35 | 36 | body._bvh = this; 37 | this._bodies.push(body); 38 | } 39 | 40 | const polygon = body._polygon; 41 | const body_x = body.x; 42 | const body_y = body.y; 43 | 44 | if(polygon) { 45 | if( 46 | body._dirty_coords || 47 | body.x !== body._x || 48 | body.y !== body._y || 49 | body.angle !== body._angle || 50 | body.scale_x !== body._scale_x || 51 | body.scale_y !== body._scale_y 52 | ) { 53 | body._calculateCoords(); 54 | } 55 | } 56 | 57 | const padding = body._bvh_padding; 58 | const radius = polygon ? 0 : body.radius * body.scale; 59 | const body_min_x = (polygon ? body._min_x : body_x - radius) - padding; 60 | const body_min_y = (polygon ? body._min_y : body_y - radius) - padding; 61 | const body_max_x = (polygon ? body._max_x : body_x + radius) + padding; 62 | const body_max_y = (polygon ? body._max_y : body_y + radius) + padding; 63 | 64 | body._bvh_min_x = body_min_x; 65 | body._bvh_min_y = body_min_y; 66 | body._bvh_max_x = body_max_x; 67 | body._bvh_max_y = body_max_y; 68 | 69 | let current = this._hierarchy; 70 | let sort = 0; 71 | 72 | if(!current) { 73 | this._hierarchy = body; 74 | } 75 | else { 76 | while(true) { 77 | // Branch 78 | if(current._bvh_branch) { 79 | const left = current._bvh_left; 80 | const left_min_y = left._bvh_min_y; 81 | const left_max_x = left._bvh_max_x; 82 | const left_max_y = left._bvh_max_y; 83 | const left_new_min_x = body_min_x < left._bvh_min_x ? body_min_x : left._bvh_min_x; 84 | const left_new_min_y = body_min_y < left_min_y ? body_min_y : left_min_y; 85 | const left_new_max_x = body_max_x > left_max_x ? body_max_x : left_max_x; 86 | const left_new_max_y = body_max_y > left_max_y ? body_max_y : left_max_y; 87 | const left_volume = (left_max_x - left._bvh_min_x) * (left_max_y - left_min_y); 88 | const left_new_volume = (left_new_max_x - left_new_min_x) * (left_new_max_y - left_new_min_y); 89 | const left_difference = left_new_volume - left_volume; 90 | 91 | const right = current._bvh_right; 92 | const right_min_x = right._bvh_min_x; 93 | const right_min_y = right._bvh_min_y; 94 | const right_max_x = right._bvh_max_x; 95 | const right_max_y = right._bvh_max_y; 96 | const right_new_min_x = body_min_x < right_min_x ? body_min_x : right_min_x; 97 | const right_new_min_y = body_min_y < right_min_y ? body_min_y : right_min_y; 98 | const right_new_max_x = body_max_x > right_max_x ? body_max_x : right_max_x; 99 | const right_new_max_y = body_max_y > right_max_y ? body_max_y : right_max_y; 100 | const right_volume = (right_max_x - right_min_x) * (right_max_y - right_min_y); 101 | const right_new_volume = (right_new_max_x - right_new_min_x) * (right_new_max_y - right_new_min_y); 102 | const right_difference = right_new_volume - right_volume; 103 | 104 | current._bvh_sort = sort++; 105 | current._bvh_min_x = left_new_min_x < right_new_min_x ? left_new_min_x : right_new_min_x; 106 | current._bvh_min_y = left_new_min_y < right_new_min_y ? left_new_min_y : right_new_min_y; 107 | current._bvh_max_x = left_new_max_x > right_new_max_x ? left_new_max_x : right_new_max_x; 108 | current._bvh_max_y = left_new_max_y > right_new_max_y ? left_new_max_y : right_new_max_y; 109 | 110 | current = left_difference <= right_difference ? left : right; 111 | } 112 | // Leaf 113 | else { 114 | const grandparent = current._bvh_parent; 115 | const parent_min_x = current._bvh_min_x; 116 | const parent_min_y = current._bvh_min_y; 117 | const parent_max_x = current._bvh_max_x; 118 | const parent_max_y = current._bvh_max_y; 119 | const new_parent = current._bvh_parent = body._bvh_parent = BVHBranch.getBranch(); 120 | 121 | new_parent._bvh_parent = grandparent; 122 | new_parent._bvh_left = current; 123 | new_parent._bvh_right = body; 124 | new_parent._bvh_sort = sort++; 125 | new_parent._bvh_min_x = body_min_x < parent_min_x ? body_min_x : parent_min_x; 126 | new_parent._bvh_min_y = body_min_y < parent_min_y ? body_min_y : parent_min_y; 127 | new_parent._bvh_max_x = body_max_x > parent_max_x ? body_max_x : parent_max_x; 128 | new_parent._bvh_max_y = body_max_y > parent_max_y ? body_max_y : parent_max_y; 129 | 130 | if(!grandparent) { 131 | this._hierarchy = new_parent; 132 | } 133 | else if(grandparent._bvh_left === current) { 134 | grandparent._bvh_left = new_parent; 135 | } 136 | else { 137 | grandparent._bvh_right = new_parent; 138 | } 139 | 140 | break; 141 | } 142 | } 143 | } 144 | } 145 | 146 | /** 147 | * Removes a body from the BVH 148 | * @param {Circle|Polygon|Point} body The body to remove 149 | * @param {Boolean} [updating = false] Set to true if this is a temporary removal (used internally when updating the body's position) 150 | */ 151 | remove(body, updating = false) { 152 | if(!updating) { 153 | const bvh = body._bvh; 154 | 155 | if(bvh && bvh !== this) { 156 | throw new Error('Body belongs to another collision system'); 157 | } 158 | 159 | body._bvh = null; 160 | this._bodies.splice(this._bodies.indexOf(body), 1); 161 | } 162 | 163 | if(this._hierarchy === body) { 164 | this._hierarchy = null; 165 | 166 | return; 167 | } 168 | 169 | const parent = body._bvh_parent; 170 | const grandparent = parent._bvh_parent; 171 | const parent_left = parent._bvh_left; 172 | const sibling = parent_left === body ? parent._bvh_right : parent_left; 173 | 174 | sibling._bvh_parent = grandparent; 175 | 176 | if(sibling._bvh_branch) { 177 | sibling._bvh_sort = parent._bvh_sort; 178 | } 179 | 180 | if(grandparent) { 181 | if(grandparent._bvh_left === parent) { 182 | grandparent._bvh_left = sibling; 183 | } 184 | else { 185 | grandparent._bvh_right = sibling; 186 | } 187 | 188 | let branch = grandparent; 189 | 190 | while(branch) { 191 | const left = branch._bvh_left; 192 | const left_min_x = left._bvh_min_x; 193 | const left_min_y = left._bvh_min_y; 194 | const left_max_x = left._bvh_max_x; 195 | const left_max_y = left._bvh_max_y; 196 | 197 | const right = branch._bvh_right; 198 | const right_min_x = right._bvh_min_x; 199 | const right_min_y = right._bvh_min_y; 200 | const right_max_x = right._bvh_max_x; 201 | const right_max_y = right._bvh_max_y; 202 | 203 | branch._bvh_min_x = left_min_x < right_min_x ? left_min_x : right_min_x; 204 | branch._bvh_min_y = left_min_y < right_min_y ? left_min_y : right_min_y; 205 | branch._bvh_max_x = left_max_x > right_max_x ? left_max_x : right_max_x; 206 | branch._bvh_max_y = left_max_y > right_max_y ? left_max_y : right_max_y; 207 | 208 | branch = branch._bvh_parent; 209 | } 210 | } 211 | else { 212 | this._hierarchy = sibling; 213 | } 214 | 215 | BVHBranch.releaseBranch(parent); 216 | } 217 | 218 | /** 219 | * Updates the BVH. Moved bodies are removed/inserted. 220 | */ 221 | update() { 222 | const bodies = this._bodies; 223 | const count = bodies.length; 224 | 225 | for(let i = 0; i < count; ++i) { 226 | const body = bodies[i]; 227 | 228 | let update = false; 229 | 230 | if(!update && body.padding !== body._bvh_padding) { 231 | body._bvh_padding = body.padding; 232 | update = true; 233 | } 234 | 235 | if(!update) { 236 | const polygon = body._polygon; 237 | 238 | if(polygon) { 239 | if( 240 | body._dirty_coords || 241 | body.x !== body._x || 242 | body.y !== body._y || 243 | body.angle !== body._angle || 244 | body.scale_x !== body._scale_x || 245 | body.scale_y !== body._scale_y 246 | ) { 247 | body._calculateCoords(); 248 | } 249 | } 250 | 251 | const x = body.x; 252 | const y = body.y; 253 | const radius = polygon ? 0 : body.radius * body.scale; 254 | const min_x = polygon ? body._min_x : x - radius; 255 | const min_y = polygon ? body._min_y : y - radius; 256 | const max_x = polygon ? body._max_x : x + radius; 257 | const max_y = polygon ? body._max_y : y + radius; 258 | 259 | update = min_x < body._bvh_min_x || min_y < body._bvh_min_y || max_x > body._bvh_max_x || max_y > body._bvh_max_y; 260 | } 261 | 262 | if(update) { 263 | this.remove(body, true); 264 | this.insert(body, true); 265 | } 266 | } 267 | } 268 | 269 | /** 270 | * Returns a list of potential collisions for a body 271 | * @param {Circle|Polygon|Point} body The body to test 272 | * @returns {Array} 273 | */ 274 | potentials(body) { 275 | const results = []; 276 | const min_x = body._bvh_min_x; 277 | const min_y = body._bvh_min_y; 278 | const max_x = body._bvh_max_x; 279 | const max_y = body._bvh_max_y; 280 | 281 | let current = this._hierarchy; 282 | let traverse_left = true; 283 | 284 | if(!current || !current._bvh_branch) { 285 | return results; 286 | } 287 | 288 | while(current) { 289 | if(traverse_left) { 290 | traverse_left = false; 291 | 292 | let left = current._bvh_branch ? current._bvh_left : null; 293 | 294 | while( 295 | left && 296 | left._bvh_max_x >= min_x && 297 | left._bvh_max_y >= min_y && 298 | left._bvh_min_x <= max_x && 299 | left._bvh_min_y <= max_y 300 | ) { 301 | current = left; 302 | left = current._bvh_branch ? current._bvh_left : null; 303 | } 304 | } 305 | 306 | const branch = current._bvh_branch; 307 | const right = branch ? current._bvh_right : null; 308 | 309 | if( 310 | right && 311 | right._bvh_max_x > min_x && 312 | right._bvh_max_y > min_y && 313 | right._bvh_min_x < max_x && 314 | right._bvh_min_y < max_y 315 | ) { 316 | current = right; 317 | traverse_left = true; 318 | } 319 | else { 320 | if(!branch && current !== body) { 321 | results.push(current); 322 | } 323 | 324 | let parent = current._bvh_parent; 325 | 326 | if(parent) { 327 | while(parent && parent._bvh_right === current) { 328 | current = parent; 329 | parent = current._bvh_parent; 330 | } 331 | 332 | current = parent; 333 | } 334 | else { 335 | break; 336 | } 337 | } 338 | } 339 | 340 | return results; 341 | } 342 | 343 | /** 344 | * Draws the bodies within the BVH to a CanvasRenderingContext2D's current path 345 | * @param {CanvasRenderingContext2D} context The context to draw to 346 | */ 347 | draw(context) { 348 | const bodies = this._bodies; 349 | const count = bodies.length; 350 | 351 | for(let i = 0; i < count; ++i) { 352 | bodies[i].draw(context); 353 | } 354 | } 355 | 356 | /** 357 | * Draws the BVH to a CanvasRenderingContext2D's current path. This is useful for testing out different padding values for bodies. 358 | * @param {CanvasRenderingContext2D} context The context to draw to 359 | */ 360 | drawBVH(context) { 361 | let current = this._hierarchy; 362 | let traverse_left = true; 363 | 364 | while(current) { 365 | if(traverse_left) { 366 | traverse_left = false; 367 | 368 | let left = current._bvh_branch ? current._bvh_left : null; 369 | 370 | while(left) { 371 | current = left; 372 | left = current._bvh_branch ? current._bvh_left : null; 373 | } 374 | } 375 | 376 | const branch = current._bvh_branch; 377 | const min_x = current._bvh_min_x; 378 | const min_y = current._bvh_min_y; 379 | const max_x = current._bvh_max_x; 380 | const max_y = current._bvh_max_y; 381 | const right = branch ? current._bvh_right : null; 382 | 383 | context.moveTo(min_x, min_y); 384 | context.lineTo(max_x, min_y); 385 | context.lineTo(max_x, max_y); 386 | context.lineTo(min_x, max_y); 387 | context.lineTo(min_x, min_y); 388 | 389 | if(right) { 390 | current = right; 391 | traverse_left = true; 392 | } 393 | else { 394 | let parent = current._bvh_parent; 395 | 396 | if(parent) { 397 | while(parent && parent._bvh_right === current) { 398 | current = parent; 399 | parent = current._bvh_parent; 400 | } 401 | 402 | current = parent; 403 | } 404 | else { 405 | break; 406 | } 407 | } 408 | } 409 | } 410 | }; 411 | -------------------------------------------------------------------------------- /src/modules/BVHBranch.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @private 3 | */ 4 | const branch_pool = []; 5 | 6 | /** 7 | * A branch within a BVH 8 | * @class 9 | * @private 10 | */ 11 | export default class BVHBranch { 12 | /** 13 | * @constructor 14 | */ 15 | constructor() { 16 | /** @private */ 17 | this._bvh_parent = null; 18 | 19 | /** @private */ 20 | this._bvh_branch = true; 21 | 22 | /** @private */ 23 | this._bvh_left = null; 24 | 25 | /** @private */ 26 | this._bvh_right = null; 27 | 28 | /** @private */ 29 | this._bvh_sort = 0; 30 | 31 | /** @private */ 32 | this._bvh_min_x = 0; 33 | 34 | /** @private */ 35 | this._bvh_min_y = 0; 36 | 37 | /** @private */ 38 | this._bvh_max_x = 0; 39 | 40 | /** @private */ 41 | this._bvh_max_y = 0; 42 | } 43 | 44 | /** 45 | * Returns a branch from the branch pool or creates a new branch 46 | * @returns {BVHBranch} 47 | */ 48 | static getBranch() { 49 | if(branch_pool.length) { 50 | return branch_pool.pop(); 51 | } 52 | 53 | return new BVHBranch(); 54 | } 55 | 56 | /** 57 | * Releases a branch back into the branch pool 58 | * @param {BVHBranch} branch The branch to release 59 | */ 60 | static releaseBranch(branch) { 61 | branch_pool.push(branch); 62 | } 63 | 64 | /** 65 | * Sorting callback used to sort branches by deepest first 66 | * @param {BVHBranch} a The first branch 67 | * @param {BVHBranch} b The second branch 68 | * @returns {Number} 69 | */ 70 | static sortBranches(a, b) { 71 | return a.sort > b.sort ? -1 : 1; 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/modules/Body.mjs: -------------------------------------------------------------------------------- 1 | import Result from './Result.mjs'; 2 | import SAT from './SAT.mjs'; 3 | 4 | /** 5 | * The base class for bodies used to detect collisions 6 | * @class 7 | * @protected 8 | */ 9 | export default class Body { 10 | /** 11 | * @constructor 12 | * @param {Number} [x = 0] The starting X coordinate 13 | * @param {Number} [y = 0] The starting Y coordinate 14 | * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 15 | */ 16 | constructor(x = 0, y = 0, padding = 0) { 17 | /** 18 | * @desc The X coordinate of the body 19 | * @type {Number} 20 | */ 21 | this.x = x; 22 | 23 | /** 24 | * @desc The Y coordinate of the body 25 | * @type {Number} 26 | */ 27 | this.y = y; 28 | 29 | /** 30 | * @desc The amount to pad the bounding volume when testing for potential collisions 31 | * @type {Number} 32 | */ 33 | this.padding = padding; 34 | 35 | /** @private */ 36 | this._circle = false; 37 | 38 | /** @private */ 39 | this._polygon = false; 40 | 41 | /** @private */ 42 | this._point = false; 43 | 44 | /** @private */ 45 | this._bvh = null; 46 | 47 | /** @private */ 48 | this._bvh_parent = null; 49 | 50 | /** @private */ 51 | this._bvh_branch = false; 52 | 53 | /** @private */ 54 | this._bvh_padding = padding; 55 | 56 | /** @private */ 57 | this._bvh_min_x = 0; 58 | 59 | /** @private */ 60 | this._bvh_min_y = 0; 61 | 62 | /** @private */ 63 | this._bvh_max_x = 0; 64 | 65 | /** @private */ 66 | this._bvh_max_y = 0; 67 | } 68 | 69 | /** 70 | * Determines if the body is colliding with another body 71 | * @param {Circle|Polygon|Point} target The target body to test against 72 | * @param {Result} [result = null] A Result object on which to store information about the collision 73 | * @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own potential collision heuristic) 74 | * @returns {Boolean} 75 | */ 76 | collides(target, result = null, aabb = true) { 77 | return SAT(this, target, result, aabb); 78 | } 79 | 80 | /** 81 | * Returns a list of potential collisions 82 | * @returns {Array} 83 | */ 84 | potentials() { 85 | const bvh = this._bvh; 86 | 87 | if(bvh === null) { 88 | throw new Error('Body does not belong to a collision system'); 89 | } 90 | 91 | return bvh.potentials(this); 92 | } 93 | 94 | /** 95 | * Removes the body from its current collision system 96 | */ 97 | remove() { 98 | const bvh = this._bvh; 99 | 100 | if(bvh) { 101 | bvh.remove(this, false); 102 | } 103 | } 104 | 105 | /** 106 | * Creates a {@link Result} used to collect the detailed results of a collision test 107 | */ 108 | createResult() { 109 | return new Result(); 110 | } 111 | 112 | /** 113 | * Creates a Result used to collect the detailed results of a collision test 114 | */ 115 | static createResult() { 116 | return new Result(); 117 | } 118 | }; 119 | -------------------------------------------------------------------------------- /src/modules/Circle.mjs: -------------------------------------------------------------------------------- 1 | import Body from './Body.mjs'; 2 | 3 | /** 4 | * A circle used to detect collisions 5 | * @class 6 | */ 7 | export default class Circle extends Body { 8 | /** 9 | * @constructor 10 | * @param {Number} [x = 0] The starting X coordinate 11 | * @param {Number} [y = 0] The starting Y coordinate 12 | * @param {Number} [radius = 0] The radius 13 | * @param {Number} [scale = 1] The scale 14 | * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 15 | */ 16 | constructor(x = 0, y = 0, radius = 0, scale = 1, padding = 0) { 17 | super(x, y, padding); 18 | 19 | /** 20 | * @desc 21 | * @type {Number} 22 | */ 23 | this.radius = radius; 24 | 25 | /** 26 | * @desc 27 | * @type {Number} 28 | */ 29 | this.scale = scale; 30 | } 31 | 32 | /** 33 | * Draws the circle to a CanvasRenderingContext2D's current path 34 | * @param {CanvasRenderingContext2D} context The context to add the arc to 35 | */ 36 | draw(context) { 37 | const x = this.x; 38 | const y = this.y; 39 | const radius = this.radius * this.scale; 40 | 41 | context.moveTo(x + radius, y); 42 | context.arc(x, y, radius, 0, Math.PI * 2); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/modules/Point.mjs: -------------------------------------------------------------------------------- 1 | import Polygon from './Polygon.mjs'; 2 | 3 | /** 4 | * A point used to detect collisions 5 | * @class 6 | */ 7 | export default class Point extends Polygon { 8 | /** 9 | * @constructor 10 | * @param {Number} [x = 0] The starting X coordinate 11 | * @param {Number} [y = 0] The starting Y coordinate 12 | * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 13 | */ 14 | constructor(x = 0, y = 0, padding = 0) { 15 | super(x, y, [[0, 0]], 0, 1, 1, padding); 16 | 17 | /** @private */ 18 | this._point = true; 19 | } 20 | }; 21 | 22 | Point.prototype.setPoints = undefined; 23 | -------------------------------------------------------------------------------- /src/modules/Polygon.mjs: -------------------------------------------------------------------------------- 1 | import Body from './Body.mjs'; 2 | 3 | /** 4 | * A polygon used to detect collisions 5 | * @class 6 | */ 7 | export default class Polygon extends Body { 8 | /** 9 | * @constructor 10 | * @param {Number} [x = 0] The starting X coordinate 11 | * @param {Number} [y = 0] The starting Y coordinate 12 | * @param {Array} [points = []] An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...] 13 | * @param {Number} [angle = 0] The starting rotation in radians 14 | * @param {Number} [scale_x = 1] The starting scale along the X axis 15 | * @param {Number} [scale_y = 1] The starting scale long the Y axis 16 | * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 17 | */ 18 | constructor(x = 0, y = 0, points = [], angle = 0, scale_x = 1, scale_y = 1, padding = 0) { 19 | super(x, y, padding); 20 | 21 | /** 22 | * @desc The angle of the body in radians 23 | * @type {Number} 24 | */ 25 | this.angle = angle; 26 | 27 | /** 28 | * @desc The scale of the body along the X axis 29 | * @type {Number} 30 | */ 31 | this.scale_x = scale_x; 32 | 33 | /** 34 | * @desc The scale of the body along the Y axis 35 | * @type {Number} 36 | */ 37 | this.scale_y = scale_y; 38 | 39 | 40 | /** @private */ 41 | this._polygon = true; 42 | 43 | /** @private */ 44 | this._x = x; 45 | 46 | /** @private */ 47 | this._y = y; 48 | 49 | /** @private */ 50 | this._angle = angle; 51 | 52 | /** @private */ 53 | this._scale_x = scale_x; 54 | 55 | /** @private */ 56 | this._scale_y = scale_y; 57 | 58 | /** @private */ 59 | this._min_x = 0; 60 | 61 | /** @private */ 62 | this._min_y = 0; 63 | 64 | /** @private */ 65 | this._max_x = 0; 66 | 67 | /** @private */ 68 | this._max_y = 0; 69 | 70 | /** @private */ 71 | this._points = null; 72 | 73 | /** @private */ 74 | this._coords = null; 75 | 76 | /** @private */ 77 | this._edges = null; 78 | 79 | /** @private */ 80 | this._normals = null; 81 | 82 | /** @private */ 83 | this._dirty_coords = true; 84 | 85 | /** @private */ 86 | this._dirty_normals = true; 87 | 88 | Polygon.prototype.setPoints.call(this, points); 89 | } 90 | 91 | /** 92 | * Draws the polygon to a CanvasRenderingContext2D's current path 93 | * @param {CanvasRenderingContext2D} context The context to add the shape to 94 | */ 95 | draw(context) { 96 | if( 97 | this._dirty_coords || 98 | this.x !== this._x || 99 | this.y !== this._y || 100 | this.angle !== this._angle || 101 | this.scale_x !== this._scale_x || 102 | this.scale_y !== this._scale_y 103 | ) { 104 | this._calculateCoords(); 105 | } 106 | 107 | const coords = this._coords; 108 | 109 | if(coords.length === 2) { 110 | context.moveTo(coords[0], coords[1]); 111 | context.arc(coords[0], coords[1], 1, 0, Math.PI * 2); 112 | } 113 | else { 114 | context.moveTo(coords[0], coords[1]); 115 | 116 | for(let i = 2; i < coords.length; i += 2) { 117 | context.lineTo(coords[i], coords[i + 1]); 118 | } 119 | 120 | if(coords.length > 4) { 121 | context.lineTo(coords[0], coords[1]); 122 | } 123 | } 124 | } 125 | 126 | /** 127 | * Sets the points making up the polygon. It's important to use this function when changing the polygon's shape to ensure internal data is also updated. 128 | * @param {Array} new_points An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...] 129 | */ 130 | setPoints(new_points) { 131 | const count = new_points.length; 132 | 133 | this._points = new Float64Array(count * 2); 134 | this._coords = new Float64Array(count * 2); 135 | this._edges = new Float64Array(count * 2); 136 | this._normals = new Float64Array(count * 2); 137 | 138 | const points = this._points; 139 | 140 | for(let i = 0, ix = 0, iy = 1; i < count; ++i, ix += 2, iy += 2) { 141 | const new_point = new_points[i]; 142 | 143 | points[ix] = new_point[0]; 144 | points[iy] = new_point[1]; 145 | } 146 | 147 | this._dirty_coords = true; 148 | } 149 | 150 | /** 151 | * Calculates and caches the polygon's world coordinates based on its points, angle, and scale 152 | */ 153 | _calculateCoords() { 154 | const x = this.x; 155 | const y = this.y; 156 | const angle = this.angle; 157 | const scale_x = this.scale_x; 158 | const scale_y = this.scale_y; 159 | const points = this._points; 160 | const coords = this._coords; 161 | const count = points.length; 162 | 163 | let min_x; 164 | let max_x; 165 | let min_y; 166 | let max_y; 167 | 168 | for(let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) { 169 | let coord_x = points[ix] * scale_x; 170 | let coord_y = points[iy] * scale_y; 171 | 172 | if(angle) { 173 | const cos = Math.cos(angle); 174 | const sin = Math.sin(angle); 175 | const tmp_x = coord_x; 176 | const tmp_y = coord_y; 177 | 178 | coord_x = tmp_x * cos - tmp_y * sin; 179 | coord_y = tmp_x * sin + tmp_y * cos; 180 | } 181 | 182 | coord_x += x; 183 | coord_y += y; 184 | 185 | coords[ix] = coord_x; 186 | coords[iy] = coord_y; 187 | 188 | if(ix === 0) { 189 | min_x = max_x = coord_x; 190 | min_y = max_y = coord_y; 191 | } 192 | else { 193 | if(coord_x < min_x) { 194 | min_x = coord_x; 195 | } 196 | else if(coord_x > max_x) { 197 | max_x = coord_x; 198 | } 199 | 200 | if(coord_y < min_y) { 201 | min_y = coord_y; 202 | } 203 | else if(coord_y > max_y) { 204 | max_y = coord_y; 205 | } 206 | } 207 | } 208 | 209 | this._x = x; 210 | this._y = y; 211 | this._angle = angle; 212 | this._scale_x = scale_x; 213 | this._scale_y = scale_y; 214 | this._min_x = min_x; 215 | this._min_y = min_y; 216 | this._max_x = max_x; 217 | this._max_y = max_y; 218 | this._dirty_coords = false; 219 | this._dirty_normals = true; 220 | } 221 | 222 | /** 223 | * Calculates the normals and edges of the polygon's sides 224 | */ 225 | _calculateNormals() { 226 | const coords = this._coords; 227 | const edges = this._edges; 228 | const normals = this._normals; 229 | const count = coords.length; 230 | 231 | for(let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) { 232 | const next = ix + 2 < count ? ix + 2 : 0; 233 | const x = coords[next] - coords[ix]; 234 | const y = coords[next + 1] - coords[iy]; 235 | const length = x || y ? Math.sqrt(x * x + y * y) : 0; 236 | 237 | edges[ix] = x; 238 | edges[iy] = y; 239 | normals[ix] = length ? y / length : 0; 240 | normals[iy] = length ? -x / length : 0; 241 | } 242 | 243 | this._dirty_normals = false; 244 | } 245 | }; 246 | -------------------------------------------------------------------------------- /src/modules/Result.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * An object used to collect the detailed results of a collision test 3 | * 4 | * > **Note:** It is highly recommended you recycle the same Result object if possible in order to avoid wasting memory 5 | * @class 6 | */ 7 | export default class Result { 8 | /** 9 | * @constructor 10 | */ 11 | constructor() { 12 | /** 13 | * @desc True if a collision was detected 14 | * @type {Boolean} 15 | */ 16 | this.collision = false; 17 | 18 | /** 19 | * @desc The source body tested 20 | * @type {Circle|Polygon|Point} 21 | */ 22 | this.a = null; 23 | 24 | /** 25 | * @desc The target body tested against 26 | * @type {Circle|Polygon|Point} 27 | */ 28 | this.b = null; 29 | 30 | /** 31 | * @desc True if A is completely contained within B 32 | * @type {Boolean} 33 | */ 34 | this.a_in_b = false; 35 | 36 | /** 37 | * @desc True if B is completely contained within A 38 | * @type {Boolean} 39 | */ 40 | this.b_in_a = false; 41 | 42 | /** 43 | * @desc The magnitude of the shortest axis of overlap 44 | * @type {Number} 45 | */ 46 | this.overlap = 0; 47 | 48 | /** 49 | * @desc The X direction of the shortest axis of overlap 50 | * @type {Number} 51 | */ 52 | this.overlap_x = 0; 53 | 54 | /** 55 | * @desc The Y direction of the shortest axis of overlap 56 | * @type {Number} 57 | */ 58 | this.overlap_y = 0; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/modules/SAT.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Determines if two bodies are colliding using the Separating Axis Theorem 3 | * @private 4 | * @param {Circle|Polygon|Point} a The source body to test 5 | * @param {Circle|Polygon|Point} b The target body to test against 6 | * @param {Result} [result = null] A Result object on which to store information about the collision 7 | * @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own collision heuristic) 8 | * @returns {Boolean} 9 | */ 10 | export default function SAT(a, b, result = null, aabb = true) { 11 | const a_polygon = a._polygon; 12 | const b_polygon = b._polygon; 13 | 14 | let collision = false; 15 | 16 | if(result) { 17 | result.a = a; 18 | result.b = b; 19 | result.a_in_b = true; 20 | result.b_in_a = true; 21 | result.overlap = null; 22 | result.overlap_x = 0; 23 | result.overlap_y = 0; 24 | } 25 | 26 | if(a_polygon) { 27 | if( 28 | a._dirty_coords || 29 | a.x !== a._x || 30 | a.y !== a._y || 31 | a.angle !== a._angle || 32 | a.scale_x !== a._scale_x || 33 | a.scale_y !== a._scale_y 34 | ) { 35 | a._calculateCoords(); 36 | } 37 | } 38 | 39 | if(b_polygon) { 40 | if( 41 | b._dirty_coords || 42 | b.x !== b._x || 43 | b.y !== b._y || 44 | b.angle !== b._angle || 45 | b.scale_x !== b._scale_x || 46 | b.scale_y !== b._scale_y 47 | ) { 48 | b._calculateCoords(); 49 | } 50 | } 51 | 52 | if(!aabb || aabbAABB(a, b)) { 53 | if(a_polygon && a._dirty_normals) { 54 | a._calculateNormals(); 55 | } 56 | 57 | if(b_polygon && b._dirty_normals) { 58 | b._calculateNormals(); 59 | } 60 | 61 | collision = ( 62 | a_polygon && b_polygon ? polygonPolygon(a, b, result) : 63 | a_polygon ? polygonCircle(a, b, result, false) : 64 | b_polygon ? polygonCircle(b, a, result, true) : 65 | circleCircle(a, b, result) 66 | ); 67 | } 68 | 69 | if(result) { 70 | result.collision = collision; 71 | } 72 | 73 | return collision; 74 | }; 75 | 76 | /** 77 | * Determines if two bodies' axis aligned bounding boxes are colliding 78 | * @param {Circle|Polygon|Point} a The source body to test 79 | * @param {Circle|Polygon|Point} b The target body to test against 80 | */ 81 | function aabbAABB(a, b) { 82 | const a_polygon = a._polygon; 83 | const a_x = a_polygon ? 0 : a.x; 84 | const a_y = a_polygon ? 0 : a.y; 85 | const a_radius = a_polygon ? 0 : a.radius * a.scale; 86 | const a_min_x = a_polygon ? a._min_x : a_x - a_radius; 87 | const a_min_y = a_polygon ? a._min_y : a_y - a_radius; 88 | const a_max_x = a_polygon ? a._max_x : a_x + a_radius; 89 | const a_max_y = a_polygon ? a._max_y : a_y + a_radius; 90 | 91 | const b_polygon = b._polygon; 92 | const b_x = b_polygon ? 0 : b.x; 93 | const b_y = b_polygon ? 0 : b.y; 94 | const b_radius = b_polygon ? 0 : b.radius * b.scale; 95 | const b_min_x = b_polygon ? b._min_x : b_x - b_radius; 96 | const b_min_y = b_polygon ? b._min_y : b_y - b_radius; 97 | const b_max_x = b_polygon ? b._max_x : b_x + b_radius; 98 | const b_max_y = b_polygon ? b._max_y : b_y + b_radius; 99 | 100 | return a_min_x < b_max_x && a_min_y < b_max_y && a_max_x > b_min_x && a_max_y > b_min_y; 101 | } 102 | 103 | /** 104 | * Determines if two polygons are colliding 105 | * @param {Polygon} a The source polygon to test 106 | * @param {Polygon} b The target polygon to test against 107 | * @param {Result} [result = null] A Result object on which to store information about the collision 108 | * @returns {Boolean} 109 | */ 110 | function polygonPolygon(a, b, result = null) { 111 | const a_count = a._coords.length; 112 | const b_count = b._coords.length; 113 | 114 | // Handle points specially 115 | if(a_count === 2 && b_count === 2) { 116 | const a_coords = a._coords; 117 | const b_coords = b._coords; 118 | 119 | if(result) { 120 | result.overlap = 0; 121 | } 122 | 123 | return a_coords[0] === b_coords[0] && a_coords[1] === b_coords[1]; 124 | } 125 | 126 | const a_coords = a._coords; 127 | const b_coords = b._coords; 128 | const a_normals = a._normals; 129 | const b_normals = b._normals; 130 | 131 | if(a_count > 2) { 132 | for(let ix = 0, iy = 1; ix < a_count; ix += 2, iy += 2) { 133 | if(separatingAxis(a_coords, b_coords, a_normals[ix], a_normals[iy], result)) { 134 | return false; 135 | } 136 | } 137 | } 138 | 139 | if(b_count > 2) { 140 | for(let ix = 0, iy = 1; ix < b_count; ix += 2, iy += 2) { 141 | if(separatingAxis(a_coords, b_coords, b_normals[ix], b_normals[iy], result)) { 142 | return false; 143 | } 144 | } 145 | } 146 | 147 | return true; 148 | } 149 | 150 | /** 151 | * Determines if a polygon and a circle are colliding 152 | * @param {Polygon} a The source polygon to test 153 | * @param {Circle} b The target circle to test against 154 | * @param {Result} [result = null] A Result object on which to store information about the collision 155 | * @param {Boolean} [reverse = false] Set to true to reverse a and b in the result parameter when testing circle->polygon instead of polygon->circle 156 | * @returns {Boolean} 157 | */ 158 | function polygonCircle(a, b, result = null, reverse = false) { 159 | const a_coords = a._coords; 160 | const a_edges = a._edges; 161 | const a_normals = a._normals; 162 | const b_x = b.x; 163 | const b_y = b.y; 164 | const b_radius = b.radius * b.scale; 165 | const b_radius2 = b_radius * 2; 166 | const radius_squared = b_radius * b_radius; 167 | const count = a_coords.length; 168 | 169 | let a_in_b = true; 170 | let b_in_a = true; 171 | let overlap = null; 172 | let overlap_x = 0; 173 | let overlap_y = 0; 174 | 175 | // Handle points specially 176 | if(count === 2) { 177 | const coord_x = b_x - a_coords[0]; 178 | const coord_y = b_y - a_coords[1]; 179 | const length_squared = coord_x * coord_x + coord_y * coord_y; 180 | 181 | if(length_squared > radius_squared) { 182 | return false; 183 | } 184 | 185 | if(result) { 186 | const length = Math.sqrt(length_squared); 187 | 188 | overlap = b_radius - length; 189 | overlap_x = coord_x / length; 190 | overlap_y = coord_y / length; 191 | b_in_a = false; 192 | } 193 | } 194 | else { 195 | for(let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) { 196 | const coord_x = b_x - a_coords[ix]; 197 | const coord_y = b_y - a_coords[iy]; 198 | const edge_x = a_edges[ix]; 199 | const edge_y = a_edges[iy]; 200 | const dot = coord_x * edge_x + coord_y * edge_y; 201 | const region = dot < 0 ? -1 : dot > edge_x * edge_x + edge_y * edge_y ? 1 : 0; 202 | 203 | let tmp_overlapping = false; 204 | let tmp_overlap = 0; 205 | let tmp_overlap_x = 0; 206 | let tmp_overlap_y = 0; 207 | 208 | if(result && a_in_b && coord_x * coord_x + coord_y * coord_y > radius_squared) { 209 | a_in_b = false; 210 | } 211 | 212 | if(region) { 213 | const left = region === -1; 214 | const other_x = left ? (ix === 0 ? count - 2 : ix - 2) : (ix === count - 2 ? 0 : ix + 2); 215 | const other_y = other_x + 1; 216 | const coord2_x = b_x - a_coords[other_x]; 217 | const coord2_y = b_y - a_coords[other_y]; 218 | const edge2_x = a_edges[other_x]; 219 | const edge2_y = a_edges[other_y]; 220 | const dot2 = coord2_x * edge2_x + coord2_y * edge2_y; 221 | const region2 = dot2 < 0 ? -1 : dot2 > edge2_x * edge2_x + edge2_y * edge2_y ? 1 : 0; 222 | 223 | if(region2 === -region) { 224 | const target_x = left ? coord_x : coord2_x; 225 | const target_y = left ? coord_y : coord2_y; 226 | const length_squared = target_x * target_x + target_y * target_y; 227 | 228 | if(length_squared > radius_squared) { 229 | return false; 230 | } 231 | 232 | if(result) { 233 | const length = Math.sqrt(length_squared); 234 | 235 | tmp_overlapping = true; 236 | tmp_overlap = b_radius - length; 237 | tmp_overlap_x = target_x / length; 238 | tmp_overlap_y = target_y / length; 239 | b_in_a = false; 240 | } 241 | } 242 | } 243 | else { 244 | const normal_x = a_normals[ix]; 245 | const normal_y = a_normals[iy]; 246 | const length = coord_x * normal_x + coord_y * normal_y; 247 | const absolute_length = length < 0 ? -length : length; 248 | 249 | if(length > 0 && absolute_length > b_radius) { 250 | return false; 251 | } 252 | 253 | if(result) { 254 | tmp_overlapping = true; 255 | tmp_overlap = b_radius - length; 256 | tmp_overlap_x = normal_x; 257 | tmp_overlap_y = normal_y; 258 | 259 | if(b_in_a && length >= 0 || tmp_overlap < b_radius2) { 260 | b_in_a = false; 261 | } 262 | } 263 | } 264 | 265 | if(tmp_overlapping && (overlap === null || overlap > tmp_overlap)) { 266 | overlap = tmp_overlap; 267 | overlap_x = tmp_overlap_x; 268 | overlap_y = tmp_overlap_y; 269 | } 270 | } 271 | } 272 | 273 | if(result) { 274 | result.a_in_b = reverse ? b_in_a : a_in_b; 275 | result.b_in_a = reverse ? a_in_b : b_in_a; 276 | result.overlap = overlap; 277 | result.overlap_x = reverse ? -overlap_x : overlap_x; 278 | result.overlap_y = reverse ? -overlap_y : overlap_y; 279 | } 280 | 281 | return true; 282 | } 283 | 284 | /** 285 | * Determines if two circles are colliding 286 | * @param {Circle} a The source circle to test 287 | * @param {Circle} b The target circle to test against 288 | * @param {Result} [result = null] A Result object on which to store information about the collision 289 | * @returns {Boolean} 290 | */ 291 | function circleCircle(a, b, result = null) { 292 | const a_radius = a.radius * a.scale; 293 | const b_radius = b.radius * b.scale; 294 | const difference_x = b.x - a.x; 295 | const difference_y = b.y - a.y; 296 | const radius_sum = a_radius + b_radius; 297 | const length_squared = difference_x * difference_x + difference_y * difference_y; 298 | 299 | if(length_squared > radius_sum * radius_sum) { 300 | return false; 301 | } 302 | 303 | if(result) { 304 | const length = Math.sqrt(length_squared); 305 | 306 | result.a_in_b = a_radius <= b_radius && length <= b_radius - a_radius; 307 | result.b_in_a = b_radius <= a_radius && length <= a_radius - b_radius; 308 | result.overlap = radius_sum - length; 309 | result.overlap_x = difference_x / length; 310 | result.overlap_y = difference_y / length; 311 | } 312 | 313 | return true; 314 | } 315 | 316 | /** 317 | * Determines if two polygons are separated by an axis 318 | * @param {Array} a_coords The coordinates of the polygon to test 319 | * @param {Array} b_coords The coordinates of the polygon to test against 320 | * @param {Number} x The X direction of the axis 321 | * @param {Number} y The Y direction of the axis 322 | * @param {Result} [result = null] A Result object on which to store information about the collision 323 | * @returns {Boolean} 324 | */ 325 | function separatingAxis(a_coords, b_coords, x, y, result = null) { 326 | const a_count = a_coords.length; 327 | const b_count = b_coords.length; 328 | 329 | if(!a_count || !b_count) { 330 | return true; 331 | } 332 | 333 | let a_start = null; 334 | let a_end = null; 335 | let b_start = null; 336 | let b_end = null; 337 | 338 | for(let ix = 0, iy = 1; ix < a_count; ix += 2, iy += 2) { 339 | const dot = a_coords[ix] * x + a_coords[iy] * y; 340 | 341 | if(a_start === null || a_start > dot) { 342 | a_start = dot; 343 | } 344 | 345 | if(a_end === null || a_end < dot) { 346 | a_end = dot; 347 | } 348 | } 349 | 350 | for(let ix = 0, iy = 1; ix < b_count; ix += 2, iy += 2) { 351 | const dot = b_coords[ix] * x + b_coords[iy] * y; 352 | 353 | if(b_start === null || b_start > dot) { 354 | b_start = dot; 355 | } 356 | 357 | if(b_end === null || b_end < dot) { 358 | b_end = dot; 359 | } 360 | } 361 | 362 | if(a_start > b_end || a_end < b_start) { 363 | return true; 364 | } 365 | 366 | if(result) { 367 | let overlap = 0; 368 | 369 | if(a_start < b_start) { 370 | result.a_in_b = false; 371 | 372 | if(a_end < b_end) { 373 | overlap = a_end - b_start; 374 | result.b_in_a = false; 375 | } 376 | else { 377 | const option1 = a_end - b_start; 378 | const option2 = b_end - a_start; 379 | 380 | overlap = option1 < option2 ? option1 : -option2; 381 | } 382 | } 383 | else { 384 | result.b_in_a = false; 385 | 386 | if(a_end > b_end) { 387 | overlap = a_start - b_end; 388 | result.a_in_b = false; 389 | } 390 | else { 391 | const option1 = a_end - b_start; 392 | const option2 = b_end - a_start; 393 | 394 | overlap = option1 < option2 ? option1 : -option2; 395 | } 396 | } 397 | 398 | const current_overlap = result.overlap; 399 | const absolute_overlap = overlap < 0 ? -overlap : overlap; 400 | 401 | if(current_overlap === null || current_overlap > absolute_overlap) { 402 | const sign = overlap < 0 ? -1 : 1; 403 | 404 | result.overlap = absolute_overlap; 405 | result.overlap_x = x * sign; 406 | result.overlap_y = y * sign; 407 | } 408 | } 409 | 410 | return false; 411 | } 412 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | 3 | module.exports = { 4 | entry : './demo/index.mjs', 5 | 6 | plugins : [ 7 | new HtmlWebpackPlugin({ 8 | filename : 'index.html', 9 | title : 'Collisions - Collision detection for circles, polygons, and points', 10 | }), 11 | ], 12 | 13 | output : { 14 | path : `${__dirname}/docs/demo/`, 15 | filename : 'index.js', 16 | }, 17 | }; 18 | --------------------------------------------------------------------------------