// Copyright (c) 2012 Elf M. Sternberg // // Much of the code here I would never have understood if it hadn't // been for the patient work of Caleb Helbling // (http://www.propulsionjs.com/), as well as the Wikipedia pages for // the Separating Axis Theorem. It took me a week to wrap my head // around these ideas. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. class Vector { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } add(v2: Vector) { return new Vector(this.x + v2.x, this.y + v2.y); } scalar(s: number) { return new Vector(this.x * s, this.y * s); } dot(v2: Vector) { return this.x * v2.x + this.y * v2.y; } magnitude2() { return this.x * this.x + this.y * this.y; } magnitude() { return Math.sqrt(this.magnitude2()); } normal() { const mag = this.magnitude(); return new Vector(this.x / mag, this.y / mag); } leftNormal() { return new Vector(-1 * this.y, this.x); } } const vec = (x: number, y: number) => new Vector(x, y); type Shape = Vector[]; type Projection = { min: number; max: number }; function colliding(shape1: Shape, shape2: Shape) { const genAxes = (shape: Shape) => { if (shape.length < 3) { throw new Error("A Shape must be a polygon."); } const axis = (idx: number) => { const p1 = shape[idx]; const p2 = shape[idx === shape.length - 1 ? 0 : idx + 1]; return vec(p1.x - p2.x, p1.y - p2.y) .normal() .leftNormal(); }; return shape.map((_: Vector, idx: number) => axis(idx)); }; const genProjection = (shape: Shape, axis: Vector) => shape.reduce( ({ min, max }: Projection, v: Vector) => { const p = axis.dot(v); return { min: p < min ? p : min, max: p > max ? p : max }; }, { min: axis.dot(shape[0]), max: axis.dot(shape[0]) } ); const axes = [...genAxes(shape1), ...genAxes(shape2)]; // The logic here may be wrong. I may have to invert the return on the whole expression. return axes.some((axis) => { const proj1 = genProjection(shape1, axis); const proj2 = genProjection(shape2, axis); return !( (proj1.min >= proj2.min && proj1.min <= proj2.max) || (proj1.max >= proj2.min && proj1.max <= proj2.max) || (proj2.min >= proj1.min && proj2.min <= proj1.max) || (proj2.max >= proj1.min && proj2.max <= proj1.max) ); }); }