UNPKG

js-2dmath

Version:

Fast 2d geometry math: Vector2, Rectangle, Circle, Matrix2x3 (2D transformation), Circle, BoundingBox, Line2, Segment2, Intersections, Distances, Transitions (animation/tween), Random numbers, Noise

460 lines (412 loc) 16.6 kB
/**! * @source https://github.com/jriecken/sat-js * @reference http://physics2d.com/content/separation-axis * * Version 0.4.1 - Copyright 2014 - Jim Riecken <jimr@jimr.ca> * Released under the MIT License * Adapted to js-2dmath by Luis Lafuente <llafuente@noboxout.com> * * A simple library for determining intersections of circles and polygons using the Separating Axis Theorem. */ var Vec2 = require("../vec2.js"), Polygon = require("../polygon.js"), Response = require("./response.js"), response_clear = Response.clear, vec2_dot = Vec2.dot, vec2_sub = Vec2.sub, vec2_length = Vec2.length, vec2_lengthSq = Vec2.lengthSq, vec2_copy = Vec2.copy, vec2_normalize = Vec2.normalize, vec2_negate = Vec2.negate, vec2_scale = Vec2.scale, vec2_perp = Vec2.perp, abs = Math.abs, sqrt = Math.sqrt; // Unit square polygon used for polygon hit detection. /** * @type {Polygon} */ var UNIT_SQUARE = [[1, 0], [1, 1], [0, 1], [0, 0]], UNIT_SQUARE_MOVED = [[1, 0], [1, 1], [0, 1], [0, 0]]; // ## Helper Functions /** * Flattens the specified array of points onto a unit vector axis, * resulting in a one dimensional range of the minimum and * maximum value on that axis. * * @param {Array.<Vec2>} points The points to flatten. * @param {Vec2} normal The unit vector axis to flatten on. * @param {Array.<number>} result An array. After calling this function, result[0] will be the minimum value, result[1] will be the maximum value. */ function _flattenPointsOn(points, normal, result) { var min = Number.MAX_VALUE, max = -Number.MAX_VALUE, len = points.length, i = 0, dot; for (; i < len; ++i) { // The magnitude of the projection of the point onto the normal dot = vec2_dot(points[i], normal); if (dot < min) { min = dot; } if (dot > max) { max = dot; } } result[0] = min; result[1] = max; } var rangeA = [0, 0], rangeB = [0, 0], offsetV = [0, 0]; /** * Check whether two convex polygons are separated by the specified * axis (must be a unit vector). * * @param {Response} out_response A Response object which will be populated if the axis is not a separating axis. * @param {Vec2} a_pos The position of the first polygon. * @param {Vec2} b_pos The position of the second polygon. * @param {Array.<Vec2>} a_points The points in the first polygon. * @param {Array.<Vec2>} b_points The points in the second polygon. * @param {Vec2} axis The axis (unit sized) to test against. The points of both polygons will be projected onto this axis. * @return {Boolean} true if it is a separating axis, false otherwise. If false, and a response is passed in, information about how much overlap and the direction of the overlap will be populated. */ function _isSeparatingAxis(out_response, a_pos, b_pos, a_points, b_points, axis) { // The magnitude of the offset between the two polygons vec2_sub(offsetV, b_pos, a_pos); var projectedOffset = vec2_dot(offsetV, axis); // Project the polygons onto the axis. _flattenPointsOn(a_points, axis, rangeA); _flattenPointsOn(b_points, axis, rangeB); // Move B's range to its position relative to A. rangeB[0] += projectedOffset; rangeB[1] += projectedOffset; // Check if there is a gap. If there is, this is a separating axis and we can stop if (rangeA[0] > rangeB[1] || rangeB[0] > rangeA[1]) { return true; } // This is not a separating axis. If we're calculating a response, calculate the overlap. var overlap = 0, option1, option2; // A starts further left than B if (rangeA[0] < rangeB[0]) { out_response.aInB = false; // A ends before B does. We have to pull A out of B if (rangeA[1] < rangeB[1]) { overlap = rangeA[1] - rangeB[0]; out_response.bInA = false; // B is fully inside A. Pick the shortest way out. } else { option1 = rangeA[1] - rangeB[0]; option2 = rangeB[1] - rangeA[0]; overlap = option1 < option2 ? option1 : -option2; } // B starts further left than A } else { out_response.bInA = false; // B ends before A ends. We have to push A out of B if (rangeA[1] > rangeB[1]) { overlap = rangeA[0] - rangeB[1]; out_response.aInB = false; // A is fully inside B. Pick the shortest way out. } else { option1 = rangeA[1] - rangeB[0]; option2 = rangeB[1] - rangeA[0]; overlap = option1 < option2 ? option1 : -option2; } } // If this is the smallest amount of overlap we've seen so far, set it as the minimum overlap. var absOverlap = abs(overlap); if (absOverlap < out_response.depth) { out_response.depth = absOverlap; vec2_copy(out_response.mtv, axis); if (overlap < 0) { vec2_negate(out_response.mtv, out_response.mtv); } } return false; } // Calculates which Vornoi region a point is on a line segment. // It is assumed that both the line and the point are relative to `(0,0)` // // | (0) | // (-1) [S]--------------[E] (1) // | (0) | /** * @param {Vec2} line The line segment. * @param {Vec2} point The point. * @return {Number} LEFT_VORNOI_REGION (-1) if it is the left region, MIDDLE_VORNOI_REGION (0) if it is the middle region, RIGHT_VORNOI_REGION (1) if it is the right region. */ function _vornoiRegion(line, point) { var len2 = vec2_lengthSq(line), dp = vec2_dot(point, line); // If the point is beyond the start of the line, it is in the // left vornoi region. if (dp < 0) { return LEFT_VORNOI_REGION; } // If the point is beyond the end of the line, it is in the // right vornoi region. else if (dp > len2) { return RIGHT_VORNOI_REGION; } // Otherwise, it's in the middle one. else { return MIDDLE_VORNOI_REGION; } } // Constants for Vornoi regions /** * @const */ var LEFT_VORNOI_REGION = -1; /** * @const */ var MIDDLE_VORNOI_REGION = 0; /** * @const */ var RIGHT_VORNOI_REGION = 1; // ## Collision Tests var pic_differenceV = [0, 0]; /** * Check if a point is inside a circle. * * @param {Vec2} vec2 The point to test. * @param {Circle} circle The circle to test. * @return {Boolean} true if the point is inside the circle, false if it is not. */ function getPointInCircle(vec2, circle) { vec2_sub(pic_differenceV, vec2, circle[0]); // If the distance between is smaller than the radius then the point is inside the circle. return vec2_lengthSq(pic_differenceV) <= (circle[1] * circle[1]); } /** * Check if a point is inside a convex polygon. * * @param {Response} out_response * @param {Vec2} vec2 The point to test. * @param {Polygon} poly The polygon to test. * @return {Boolean} true if the point is inside the polygon, false if it is not. */ function getPointInPolygon(out_response, vec2, poly) { Polygon.translate(UNIT_SQUARE_MOVED, UNIT_SQUARE, vec2); response_clear(out_response); var result = getPolygonPolygon(out_response, UNIT_SQUARE_MOVED, poly); if (result) { result = out_response.aInB; } return result; } var cic_differenceV = [0, 0]; /** * Check if two circles collide. * * @param {Response} out_response Response object that will be populated if the circles intersect. * @param {Circle} a_circle The first circle. * @param {Circle} b_circle The second circle. * @return {Boolean} true if the circles intersect, false if they don't. */ function getCircleCircle(out_response, a_circle, b_circle) { response_clear(out_response); // Check if the distance between the centers of the two // circles is greater than their combined radius. vec2_sub(cic_differenceV, b_circle[0], a_circle[0]); var totalRadius = a_circle[1] + b_circle[1], totalRadiusSq = totalRadius * totalRadius, distanceSq = vec2_lengthSq(cic_differenceV), dist; // If the distance is bigger than the combined radius, they don't intersect. if (distanceSq > totalRadiusSq) { return false; } // They intersect. If we're calculating a response, calculate the overlap. dist = sqrt(distanceSq); out_response.a = a_circle; out_response.b = b_circle; out_response.depth = totalRadius - dist; vec2_normalize(out_response.mtv, cic_differenceV); out_response.aInB = a_circle[1] <= b_circle[1] && dist <= b_circle[1] - a_circle[1]; out_response.bInA = b_circle[1] <= a_circle[1] && dist <= a_circle[1] - b_circle[1]; return true; } var pc_circlePos = [0, 0], edge = [0, 0], point = [0, 0], point2 = [0, 0], normal = [0, 0]; /** * Check if a polygon and a circle collide. * * @param {Response} out_response Response object that will be populated if they interset. * @param {Polygon} poly_points The polygon. * @param {Polygon} poly_edges The polygon edges * @param {Vec2} poly_pos The polygon position * @param {Circle} circle The circle. * @return {Boolean} true if they intersect, false if they don't. */ function getPolygonCircle(out_response, poly_points, poly_edges, poly_pos, circle) { response_clear(out_response); // Get the position of the circle relative to the polygon. vec2_sub(pc_circlePos, circle[0], poly_pos); var radius = circle[1], radius2 = radius * radius, len = poly_points.length, dist, i = 0; // For each edge in the polygon: for (; i < len; i++) { var next = i === len - 1 ? 0 : i + 1; var prev = i === 0 ? len - 1 : i - 1; var overlap = 0; var overlapN = null; // Get the edge. vec2_copy(edge, poly_edges[i]); // Calculate the center of the circle relative to the starting point of the edge. vec2_sub(point, pc_circlePos, poly_points[i]); // If the distance between the center of the circle and the point // is bigger than the radius, the polygon is definitely not fully in // the circle. if (vec2_lengthSq(point) > radius2) { out_response.aInB = false; } // Calculate which Vornoi region the center of the circle is in. var region = _vornoiRegion(edge, point); // If it's the left region: if (region === LEFT_VORNOI_REGION) { // We need to make sure we're in the RIGHT_VORNOI_REGION of the previous edge. vec2_copy(edge, poly_edges[prev]); // Calculate the center of the circle relative the starting point of the previous edge vec2_sub(point2, pc_circlePos, poly_points[prev]); region = _vornoiRegion(edge, point2); if (region === RIGHT_VORNOI_REGION) { // It's in the region we want. Check if the circle intersects the point. dist = vec2_length(point); if (dist > radius) { // No intersection return false; } else { // It intersects, calculate the overlap. out_response.bInA = false; overlapN = [0, 0]; vec2_normalize(overlapN, point); overlap = radius - dist; } } // If it's the right region: } else if (region === RIGHT_VORNOI_REGION) { // We need to make sure we're in the left region on the next edge vec2_copy(edge, poly_edges[next]); // Calculate the center of the circle relative to the starting point of the next edge. vec2_sub(point, pc_circlePos, poly_points[next]); region = _vornoiRegion(edge, point); if (region === LEFT_VORNOI_REGION) { // It's in the region we want. Check if the circle intersects the point. dist = vec2_length(point); if (dist > radius) { // No intersection return false; } else { // It intersects, calculate the overlap. out_response.bInA = false; overlapN = [0, 0]; vec2_normalize(overlapN, point); overlap = radius - dist; } } // Otherwise, it's the middle region: } else { // Need to check if the circle is intersecting the edge, // Change the edge into its "edge normal". vec2_normalize(normal, vec2_perp(edge, edge)); // Find the perpendicular distance between the center of the // circle and the edge. dist = vec2_dot(point, normal); var distAbs = abs(dist); // If the circle is on the outside of the edge, there is no intersection. if (dist > 0 && distAbs > radius) { // No intersection return false; } else { // It intersects, calculate the overlap. overlapN = normal; overlap = radius - dist; // If the center of the circle is on the outside of the edge, or part of the // circle is on the outside, the circle is not fully inside the polygon. if (dist >= 0 || overlap < 2 * radius) { out_response.bInA = false; } } } // If this is the smallest overlap we've seen, keep it. // (overlapN may be null if the circle was in the wrong Vornoi region). if (overlapN && abs(overlap) < abs(out_response.depth)) { out_response.depth = overlap; vec2_copy(out_response.mtv, overlapN); } } // Calculate the final overlap vector - based on the smallest overlap. out_response.a = poly_points; out_response.b = circle; return true; } /** * Check if a circle and a polygon collide. * * @note This is slightly less efficient than polygonCircle as it just runs polygonCircle and reverses everything at the end. * @param {Response} out_response Response object that will be populated if they interset. * @param {Circle} circle The circle. * @param {Polygon} poly The polygon. * @return {Boolean} true if they intersect, false if they don't. */ function getCirclePolygon(out_response, circle, poly) { response_clear(out_response); // Test the polygon against the circle. var result = getPolygonCircle(out_response, poly, circle); if (result) { // Swap A and B in the response. var a = out_response.a; var aInB = out_response.aInB; vec2_negate(out_response.mtv, out_response.mtv); out_response.a = out_response.b; out_response.b = a; out_response.aInB = out_response.bInA; out_response.bInA = aInB; } return result; } /** * Checks whether polygons collide. * * @param {Response} out_response Response object that will be populated if they interset. * @param {Polygon} a_points The first polygon points * @param {Polygon} a_normals The first polygon normals * @param {Polygon} a_pos The first polygon position * @param {Polygon} b_points The second polygon points * @param {Polygon} b_normals The second polygon normals * @param {Polygon} b_pos The second polygon position * @return {Boolean} true if they intersect, false if they don't. */ function getPolygonPolygon(out_response, a_points, a_normals, a_pos, b_points, b_normals, b_pos) { response_clear(out_response); var aLen = a_points.length, bLen = b_points.length, i; // If any of the edge normals of A is a separating axis, no intersection. for (i = 0; i < aLen; ++i) { if (_isSeparatingAxis(out_response, a_pos, b_pos, a_points, b_points, a_normals[i])) { return false; } } // If any of the edge normals of B is a separating axis, no intersection. for (i = 0;i < bLen; ++i) { if (_isSeparatingAxis(out_response, a_pos, b_pos, a_points, b_points, b_normals[i])) { return false; } } return true; } var SAT = { Response: Response, getPointInCircle: getPointInCircle, getPointInPolygon: getPointInPolygon, getCircleCircle: getCircleCircle, getPolygonCircle: getPolygonCircle, getCirclePolygon: getCirclePolygon, getPolygonPolygon: getPolygonPolygon }; module.exports = SAT;