├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SAT.js ├── examples ├── dynamic_collision.html ├── examples.js └── simple_collision.html ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.9.0 (April 4, 2021) 4 | 5 | - Add `getAABBAsBox` methods to `Polygon` and `Circle` that returns a `Box` (`getAABB` returns a `Polygon`) - thanks [getkey](https://github.com/getkey)! 6 | 7 | ## 0.8.0 (Sept 7, 2019) 8 | 9 | - Combine consecutive duplicate points in polygons to remove zero-length edges. (Fixes #55) 10 | - Add the ability to set an offset for circles - thanks [funnisimo](https://github.com/funnisimo)! 11 | 12 | ## 0.7.1 (May 23, 2018) 13 | 14 | - Check explicitly for `undefined` `y` param when scaling vectors. (Fixes #52) 15 | 16 | ## O.7.0 (Feb 17, 2018) 17 | 18 | - Add `getCentroid` method to `Polygon` that computes the [centroid](https://en.wikipedia.org/wiki/Centroid#Centroid_of_a_polygon). (Fixes #50) 19 | - Useful for computing the center of a polygon if you want to rotate around it. 20 | 21 | ## 0.6.0 (Sept 11, 2016) 22 | 23 | - Fix "Vornoi" -> "Voronoi" everywhere. Changes are all in private code, no functional changes. (Fixes #27) 24 | - Exposed isSeparatingAxis() function - thanks [hexus](https://github.com/hexus)! 25 | - Allow pointInPolygon to work with small polygons. (Fixes #41) 26 | 27 | ## 0.5.0 (Dec 26, 2014) 28 | 29 | - **(POTENTIALLY BREAKING CHANGE)** Make `recalc` on `Polygon` more memory efficient. It no longer does any allocations. The `calcPoints`,`edges` and `normals` vector arrays are reused and only created in `setPoints` when the number of new points is different than the current ones. (Fixes #15) 30 | - `points`, `angle` and `offset` can no longer be manually changed. The `setPoints`, `setAngle`, and `setOffset` methods **must** be used. 31 | - As a result of this, the `recalc` method is no longer part of the API. 32 | - Add `getAABB` to `Polygon` and `Circle` that calculate Axis-Aligned Bounding Boxes - thanks [TuurDutoit](https://github.com/TuurDutoit)! (Fixes #17) 33 | 34 | ## 0.4.1 (Mar 23, 2014) 35 | 36 | - Fix missing `T_VECTORS.push()` - thanks [shakiba](https://github.com/shakiba)! (Fixes #8) 37 | - Add `package.json` - released as `npm` module (Fixes #11, Fixes #12) 38 | 39 | ## 0.4 (Mar 2, 2014) 40 | 41 | - Add `clone` method to `Vector` that returns a new vector with the same coordinates. 42 | - Add `angle` and `offset` to `Polygon` that are used to modify the computed collision polygon (Fixes #3, Fixes #4) 43 | - The `rotate` and `translate` methods still exist on `Polygon` but they modify the original `points` of the polygon, wheras `angle` and `offset` do not modify the original points, and are instead applied as computed values. 44 | - Add `setPoints`, `setAngle`, and `setOffset` methods to `Polygon` 45 | 46 | ## 0.3 (Feb 11, 2014) 47 | 48 | - Add `pointInCircle` and `pointInPolygon` functions for performing "hit tests" (Fixes #2) 49 | 50 | ## 0.2 (Dec 8, 2013) 51 | 52 | - Reformat comments so that they can be run through `docco` to create an annotated source file. 53 | - Fix/optimize compilation with the Closure Compiler in advanced mode (previously it was mangling some important properties) 54 | - Wrap the code in a UMD declaration so that it works: 55 | - Just inserting it as a ` 6 | 7 | 8 | 9 | 10 | 11 |

Dynamic Collision Examples

12 | 13 |

Dynamic response

14 |

Drag the shapes around. If they collide they will be moved so that they don't collide. The circle is "heavy" - it will not be moved by other items (but will move other items)

15 |
16 | 31 | 32 |

Lots of circles, all collidable with each other.

33 |

Drag any of the circles and watch them react.

34 |
35 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/examples.js: -------------------------------------------------------------------------------- 1 | // Alias a few things in SAT.js to make the code shorter 2 | var V = function (x, y) { return new SAT.Vector(x, y); }; 3 | var P = function (pos, points) { return new SAT.Polygon(pos, points); }; 4 | var C = function (pos, r) { return new SAT.Circle(pos, r); }; 5 | var B = function (pos, w, h) { return new SAT.Box(pos, w, h); }; 6 | 7 | // Converts a SAT.Polygon into a SVG path string. 8 | function poly2path(polygon) { 9 | var pos = polygon.pos; 10 | var points = polygon.calcPoints; 11 | var result = 'M' + pos.x + ' ' + pos.y; 12 | result += 'M' + (pos.x + points[0].x) + ' ' + (pos.y + points[0].y); 13 | for (var i = 1; i < points.length; i++) { 14 | var point = points[i]; 15 | result += 'L' + (pos.x + point.x) + ' ' + (pos.y + point.y); 16 | } 17 | result += 'Z'; 18 | return result; 19 | } 20 | 21 | // Create a Raphael start drag handler for specified entity 22 | function startDrag(entity) { 23 | return function () { 24 | this.ox = entity.data.pos.x; 25 | this.oy = entity.data.pos.y; 26 | }; 27 | } 28 | // Create a Raphael move drag handler for specified entity 29 | function moveDrag(entity, world) { 30 | return function (dx, dy) { 31 | // This position updating is fairly naive - it lets objects tunnel through each other, but it suffices for these examples. 32 | entity.data.pos.x = this.ox + dx; 33 | entity.data.pos.y = this.oy + dy; 34 | world.simulate(); 35 | }; 36 | } 37 | // Create a Raphael end drag handler for specified entity 38 | function endDrag(entity) { 39 | return function () { 40 | entity.updateDisplay(); 41 | }; 42 | } 43 | 44 | var idCounter = 0; 45 | 46 | function noop() {} 47 | 48 | function Entity(data, display, options) { 49 | options = _.defaults(options || {}, { 50 | solid: false, // Whether this object is "solid" and therefore should participate in responses. 51 | heavy: false, // Whether this object is "heavy" and can't be moved by other objects. 52 | displayAttrs: {}, // Raphael attrs to set on the display object 53 | onCollide: noop, // Function to execute when this entity collides with another - arguments are (otherEntity, response) 54 | onTick: noop // Function called at the start of every simulation tick - no arguments 55 | }); 56 | this.id = idCounter++; 57 | this.data = data; 58 | this.display = display; 59 | this.displayAttrs = _.extend({ 60 | fill: '#CCC', 61 | stroke: '#000' 62 | }, options.displayAttrs); 63 | this.isSolid = options.solid; 64 | this.isHeavy = options.heavy; 65 | this.onCollide = options.onCollide; 66 | this.onTick = options.onTick; 67 | } 68 | Entity.prototype = { 69 | remove: function () { 70 | this.display.remove(); 71 | }, 72 | // Call this to update the display after changing the underlying data. 73 | updateDisplay: function () { 74 | if (this.data instanceof SAT.Circle) { 75 | this.displayAttrs.cx = this.data.pos.x; 76 | this.displayAttrs.cy = this.data.pos.y; 77 | this.displayAttrs.r = this.data.r; 78 | } else { 79 | this.displayAttrs.path = poly2path(this.data); 80 | } 81 | this.display.attr(this.displayAttrs); 82 | }, 83 | tick: function () { 84 | this.onTick(); 85 | }, 86 | respondToCollision: function (other, response) { 87 | this.onCollide(other, response); 88 | // Collisions between "ghostly" objects don't matter, and 89 | // two "heavy" objects will just remain where they are. 90 | if (this.isSolid && other.isSolid && 91 | !(this.isHeavy && other.isHeavy)) { 92 | if (this.isHeavy) { 93 | // Move the other object out of us 94 | other.data.pos.add(response.overlapV); 95 | } else if (other.isHeavy) { 96 | // Move us out of the other object 97 | this.data.pos.sub(response.overlapV); 98 | } else { 99 | // Move equally out of each other 100 | response.overlapV.scale(0.5); 101 | this.data.pos.sub(response.overlapV); 102 | other.data.pos.add(response.overlapV); 103 | } 104 | } 105 | } 106 | }; 107 | 108 | function World(canvas, options) { 109 | options = _.defaults(options || {}, { 110 | loopCount: 1 // number of loops to do each time simulation is called. The higher the more accurate the simulation, but slowers. 111 | }); 112 | this.canvas = canvas; // A raphael.js canvas 113 | this.response = new SAT.Response(); // Response reused for collisions 114 | this.loopCount = options.loopCount; 115 | this.entities = {}; 116 | } 117 | World.prototype = { 118 | addEntity: function(data, options) { 119 | var entity = new Entity( 120 | data, 121 | data instanceof SAT.Circle ? this.canvas.circle() : this.canvas.path(), 122 | options 123 | ); 124 | // Make the display item draggable if requested. 125 | if (options.draggable) { 126 | entity.display.drag(moveDrag(entity, this), startDrag(entity), endDrag(entity)); 127 | } 128 | entity.updateDisplay(); 129 | this.entities[entity.id] = entity; 130 | return entity; 131 | }, 132 | removeEntity: function (entity) { 133 | entity.remove(); 134 | delete this.entities[entity.id]; 135 | }, 136 | simulate: function () { 137 | var entities = _.values(this.entities); 138 | var entitiesLen = entities.length; 139 | // Let the entity do something every simulation tick 140 | _.each(entities, function (entity) { 141 | entity.tick(); 142 | }); 143 | // Handle collisions - loop a configurable number of times to let things "settle" 144 | var loopCount = this.loopCount; 145 | for (var i = 0; i < loopCount; i++) { 146 | // Naively check for collision between all pairs of entities 147 | // E.g if there are 4 entities: (0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3) 148 | for (var aCount = 0; aCount < entitiesLen; aCount++) { 149 | var a = entities[aCount]; 150 | for (var bCount = aCount + 1; bCount < entitiesLen; bCount++) { 151 | var b = entities[bCount]; 152 | this.response.clear(); 153 | var collided; 154 | var aData = a.data; 155 | var bData = b.data; 156 | if (aData instanceof SAT.Circle) { 157 | if (bData instanceof SAT.Circle) { 158 | collided = SAT.testCircleCircle(aData, bData, this.response); 159 | } else { 160 | collided = SAT.testCirclePolygon(aData, bData, this.response); 161 | } 162 | } else { 163 | if (bData instanceof SAT.Circle) { 164 | collided = SAT.testPolygonCircle(aData, bData, this.response); 165 | } else { 166 | collided = SAT.testPolygonPolygon(aData, bData, this.response); 167 | } 168 | } 169 | if (collided) { 170 | a.respondToCollision(b, this.response); 171 | } 172 | } 173 | } 174 | } 175 | // Finally, update the display of each entity now that the simulation step is done. 176 | _.each(entities, function (entity) { 177 | entity.updateDisplay(); 178 | }); 179 | } 180 | }; -------------------------------------------------------------------------------- /examples/simple_collision.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SAT.js - Simple Collision Examples 5 | 6 | 7 | 8 | 9 | 27 | 28 | 29 |

Simple Collision Examples

30 |

Drag the shapes around. The shapes will turn red when they are colliding. The smaller shape will turn green if it is completely inside the larger one.

31 |

Circle-Circle collision

32 |
33 | 41 | 42 |

Circle-Polygon Collision

43 |
44 | 52 | 53 |

Polygon-Polygon Collision

54 |
55 | 63 | 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sat", 3 | "description": "Library for performing 2D collision detection", 4 | "version": "0.9.0", 5 | "author": "Jim Riecken ", 6 | "keywords": [ 7 | "collision detection", 8 | "sat", 9 | "game" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/jriecken/sat-js.git" 15 | }, 16 | "bugs": { 17 | "url": "http://github.com/jriecken/sat-js/issues" 18 | }, 19 | "scripts": { 20 | "test": "mocha" 21 | }, 22 | "main": "SAT.js", 23 | "devDependencies": { 24 | "mocha": "^2.1.0" 25 | } 26 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var SAT = require('..'); 2 | var assert = require('assert'); 3 | 4 | describe('Vector.scale', function() { 5 | it('should scale by zero properly', function() { 6 | var V = SAT.Vector; 7 | var v1 = new V(5, 5); 8 | v1.scale(10, 10); 9 | assert(v1.x === 50); 10 | assert(v1.y === 50); 11 | 12 | v1.scale(0, 1); 13 | assert(v1.x === 0); 14 | assert(v1.y === 50); 15 | 16 | v1.scale(1, 0); 17 | assert(v1.x === 0); 18 | assert(v1.y === 0); 19 | }); 20 | }); 21 | 22 | describe("Polygon.getCentroid", function() { 23 | it("should calculate the correct value for a square", function() { 24 | var V = SAT.Vector; 25 | var P = SAT.Polygon; 26 | 27 | // A square 28 | var polygon = new P(new V(0,0), [ 29 | new V(0,0), new V(40,0), new V(40,40), new V(0,40) 30 | ]); 31 | var c = polygon.getCentroid(); 32 | assert( c.x === 20 ); 33 | assert( c.y === 20 ); 34 | }); 35 | 36 | it("should calculate the correct value for a triangle", function() { 37 | var V = SAT.Vector; 38 | var P = SAT.Polygon; 39 | 40 | // A triangle 41 | var polygon = new P(new V(0,0), [ 42 | new V(0,0), new V(100,0), new V(50,99) 43 | ]); 44 | var c = polygon.getCentroid(); 45 | assert( c.x === 50 ); 46 | assert( c.y === 33 ); 47 | }); 48 | }); 49 | 50 | describe("Collision", function() { 51 | it("testCircleCircle", function() { 52 | var V = SAT.Vector; 53 | var C = SAT.Circle; 54 | 55 | var circle1 = new C(new V(0,0), 20); 56 | var circle2 = new C(new V(30,0), 20); 57 | var response = new SAT.Response(); 58 | var collided = SAT.testCircleCircle(circle1, circle2, response); 59 | 60 | assert( collided ); 61 | assert( response.overlap == 10 ); 62 | assert( response.overlapV.x == 10 && response.overlapV.y === 0); 63 | 64 | circle1.offset = new V(-10, -10); 65 | collided = SAT.testCircleCircle(circle1, circle2, response); 66 | assert( !collided ); 67 | }); 68 | 69 | it("testPolygonCircle", function() { 70 | 71 | var V = SAT.Vector; 72 | var C = SAT.Circle; 73 | var P = SAT.Polygon; 74 | 75 | var circle = new C(new V(50,50), 20); 76 | // A square 77 | var polygon = new P(new V(0,0), [ 78 | new V(0,0), new V(40,0), new V(40,40), new V(0,40) 79 | ]); 80 | var response = new SAT.Response(); 81 | var collided = SAT.testPolygonCircle(polygon, circle, response); 82 | 83 | assert(collided); 84 | assert(response.overlap.toFixed(2) == "5.86"); 85 | assert( 86 | response.overlapV.x.toFixed(2) == "4.14" && 87 | response.overlapV.y.toFixed(2) == "4.14" 88 | ); 89 | 90 | circle.offset = new V(10, 10); 91 | collided = SAT.testPolygonCircle(polygon, circle, response); 92 | assert(!collided); 93 | }); 94 | 95 | it('testPolygonCircle - line - not collide', function () { 96 | var V = SAT.Vector; 97 | var C = SAT.Circle; 98 | var B = SAT.Box; 99 | 100 | var circle = new C(new V(50,50), 20); 101 | var polygon = new B(new V(1000,1000), 100, 0).toPolygon(); 102 | var response = new SAT.Response(); 103 | var collided = SAT.testPolygonCircle(polygon, circle, response); 104 | assert(!collided); 105 | }) 106 | 107 | it('testPolygonCircle - line - collide', function () { 108 | var V = SAT.Vector; 109 | var C = SAT.Circle; 110 | var B = SAT.Box; 111 | 112 | var circle = new C(new V(50,50), 20); 113 | var polygon = new B(new V(50,50), 100, 0).toPolygon(); 114 | var response = new SAT.Response(); 115 | var collided = SAT.testPolygonCircle(polygon, circle, response); 116 | 117 | assert(collided); 118 | assert(response.overlap.toFixed(2) == "20.00"); 119 | }) 120 | 121 | it("testPolygonPolygon", function() { 122 | var V = SAT.Vector; 123 | var P = SAT.Polygon; 124 | 125 | // A square 126 | var polygon1 = new P(new V(0,0), [ 127 | new V(0,0), new V(40,0), new V(40,40), new V(0,40) 128 | ]); 129 | // A triangle 130 | var polygon2 = new P(new V(30,0), [ 131 | new V(0,0), new V(30, 0), new V(0, 30) 132 | ]); 133 | var response = new SAT.Response(); 134 | var collided = SAT.testPolygonPolygon(polygon1, polygon2, response); 135 | 136 | assert( collided ); 137 | assert( response.overlap == 10 ); 138 | assert( response.overlapV.x == 10 && response.overlapV.y === 0); 139 | }); 140 | }); 141 | 142 | describe("No collision", function() { 143 | it("testPolygonPolygon", function(){ 144 | var V = SAT.Vector; 145 | var B = SAT.Box; 146 | 147 | var box1 = new B(new V(0,0), 20, 20).toPolygon(); 148 | var box2 = new B(new V(100,100), 20, 20).toPolygon(); 149 | var collided = SAT.testPolygonPolygon(box1, box2); 150 | }); 151 | }); 152 | 153 | describe("Point testing", function() { 154 | it("pointInCircle", function(){ 155 | var V = SAT.Vector; 156 | var C = SAT.Circle; 157 | 158 | var circle = new C(new V(100,100), 20); 159 | 160 | assert(!SAT.pointInCircle(new V(0,0), circle)); // false 161 | assert(SAT.pointInCircle(new V(110,110), circle)); // true 162 | 163 | circle.offset = new V(-10, -10); 164 | assert(!SAT.pointInCircle(new V(110,110), circle)); // false 165 | }); 166 | 167 | it("pointInPolygon", function() { 168 | var V = SAT.Vector; 169 | var C = SAT.Circle; 170 | var P = SAT.Polygon; 171 | 172 | var triangle = new P(new V(30,0), [ 173 | new V(0,0), new V(30, 0), new V(0, 30) 174 | ]); 175 | assert(!SAT.pointInPolygon(new V(0,0), triangle)); // false 176 | assert(SAT.pointInPolygon(new V(35, 5), triangle)); // true 177 | }); 178 | 179 | it("pointInPolygon (small)", function () { 180 | var V = SAT.Vector; 181 | var C = SAT.Circle; 182 | var P = SAT.Polygon; 183 | 184 | var v1 = new V(1, 1.1); 185 | var p1 = new P(new V(0,0),[new V(2,1), new V(2,2), new V(1,3), new V(0,2),new V(0,1),new V(1,0)]); 186 | assert(SAT.pointInPolygon(v1, p1)); 187 | }); 188 | }); 189 | --------------------------------------------------------------------------------