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

854 lines (750 loc) 21.9 kB
var Rectangle = require("./rectangle.js"), Distance = require("./distance.js"), Segment2 = require("./segment2.js"), segment2$inside = Segment2.$inside, AABB2 = require("./aabb2.js"), Vec2 = require("./vec2.js"), abs = Math.abs, sqrt = Math.sqrt, max = Math.max, min = Math.min, EPS = Math.EPS, aux_vec2 = [0, 0], //cache OUTSIDE = 1, // no collision PARALLEL = 2, // no collision INSIDE = 4, // no collision COLLIDE = 8, // collision COINCIDENT = 16, // collision TANGENT = 32; // collision /** * @param {Number} num * @param {Number} num2 * @return {Boolean} */ function near(num, num2) { return num > num2 - EPS && num < num2 + EPS; } // // helpers // /** * x1 < x3 * * @TODO distance * @TODO segment collision, maybe using segment-segment collision, this could slow down things! * * @param {Number} x1 * @param {Number} y1 * @param {Number} x2 * @param {Number} y2 * @param {Number} x3 * @param {Number} y3 * @param {Number} x4 * @param {Number} y4 * @param {Boolean} collision * @param {Boolean} distance */ function $rectangle_rectangle(x1, y1, x2, y2, x3, y3, x4, y4, collision, distance) { var points, x_inside, y_inside; if (x2 < x3 || y1 > y4 || y2 < y3) { return {reason : OUTSIDE}; } x_inside = x1 < x3 && x2 > x4; y_inside = y1 < y3 && y2 > y4; if (x_inside && y_inside) { return {reason : INSIDE}; } if (collision === false) { return {reason: COLLIDE}; } // complex cases, 4 point collision if (y1 > y3 && (x_inside || y_inside)) { points = [ [max(x1, x3), max(y1, y3)], [min(x2, x4), min(y2, y4)], [min(x2, x4), max(y1, y3)], [max(x1, x3), min(y2, y4)] ]; } else { //base case points = [ [min(x2, x4), max(y1, y3)], [max(x1, x3), min(y2, y4)] ]; } return {reason: COLLIDE, points : points}; } /** * @param {Number} x1 * @param {Number} y1 * @param {Number} x2 * @param {Number} y2 * @param {Number} x3 * @param {Number} y3 * @param {Boolean} collision * @param {Boolean} distance */ function $rectangle_vec2(x1, y1, x2, y2, x3, y3, collision, distance) { if (x1 > x3 || x2 < x3 || y1 > y3 || y2 < y3) { return {reason: OUTSIDE}; // TODO distance: distance ? Distance.rectangle_vec2(rectangle, vec2) : null } //if (bb[0] <= v[0] && bb[2] >= v[0] && bb[1] <= v[1] && bb[3] >= v[1]); if (x1 < x3 && x2 > x3 && y1 < y3 && y2 > y3) { return {reason: INSIDE}; // TODO distance: distance ? Distance.rectangle_vec2(rectangle, vec2) : null } return {reason: COLLIDE, points : [[x3, y3]]}; } /** * @param {Number} cx * @param {Number} cy * @param {Number} r * @param {Number} x1 * @param {Number} y1 * @param {Number} x2 * @param {Number} y2 * @param {Boolean} collision * @param {Boolean} distance */ function $circle_segment2(cx, cy, r, x1, y1, x2, y2, collision, distance) { var cx1 = x1 - cx, cy1 = y1 - cy, cx2 = x2 - cx, cy2 = y2 - cy, dx = cx2 - cx1, dy = cy2 - cy1, a = dx * dx + dy * dy, b = 2 * ((dx * cx1) + (dy * cy1)), c = (cx1 * cx1) + (cy1 * cy1) - (r * r), delta = b * b - (4 * a * c), u, u1, u2, deltasqrt, p, points; if (delta < 0) { // No intersection return {reason: OUTSIDE}; } if (delta === 0) { // One intersection if (collision === false) { return {reason: TANGENT}; } u = -b / (2 * a); p = [x1 + (u * dx), y1 + (u * dy)]; if (segment2$inside(x1, y1, x2, y2, p[0], p[1])) { return {reason: TANGENT, points: [p]}; } return {reason: OUTSIDE}; /* Use LineP1 instead of LocalP1 because we want our answer in global space, not the circle's local space */ } // NOTE do not test collision === false, here, there is no performance gain here. if (delta > 0) { // Two intersections deltasqrt = sqrt(delta); u1 = (-b + deltasqrt) / (2 * a); u2 = (-b - deltasqrt) / (2 * a); points = []; p = [x1 + (u1 * dx), y1 + (u1 * dy)]; if (segment2$inside(x1, y1, x2, y2, p[0], p[1])) { points.push(p); } p = [x1 + (u2 * dx), y1 + (u2 * dy)]; if (segment2$inside(x1, y1, x2, y2, p[0], p[1])) { points.push(p); } if (points.length) { return {reason: COLLIDE, points: points}; } return {reason: OUTSIDE}; } } /** * @param {Number} cx * @param {Number} cy * @param {Number} r * @param {Number} x1 * @param {Number} y1 * @param {Number} x2 * @param {Number} y2 * @param {Boolean} collision * @param {Boolean} distance */ function $circle_rectangle(cx, cy, r, x1, y1, x2, y2, collision, distance) { var points = [], r2, collide = false; // TODO inside test r2 = $circle_segment2(cx, cy, r, x1, y1, x2, y1, collision, distance); if (r2.reason >= COLLIDE) { collide = true; if (collision === true) { points.push(r2.points[0]); if (r2.points.length === 2) { points.push(r2.points[1]); } } } r2 = $circle_segment2(cx, cy, r, x1, y1, x1, y2, collision, distance); if (r2.reason >= COLLIDE) { collide = true; if (collision === true) { points.push(r2.points[0]); if (r2.points.length === 2) { points.push(r2.points[1]); } } } r2 = $circle_segment2(cx, cy, r, x1, y2, x2, y2, collision, distance); if (r2.reason >= COLLIDE) { collide = true; if (collision === true) { points.push(r2.points[0]); if (r2.points.length === 2) { points.push(r2.points[1]); } } } r2 = $circle_segment2(cx, cy, r, x2, y1, x2, y2, collision, distance); if (r2.reason >= COLLIDE) { collide = true; if (collision === true) { points.push(r2.points[0]); if (r2.points.length === 2) { points.push(r2.points[1]); } } } if (collide) { if (points === false) { return {reason: COLLIDE}; } return {reason: COLLIDE, points: points}; } return {reason: OUTSIDE}; } /** * @param {AABB2} bb2_1 * @param {AABB2} bb2_2 * @param {Boolean} collision * @param {Boolean} distance */ function bb2_bb2(bb2_1, bb2_2, collision, distance) { AABB2.normalize(bb2_1, bb2_1); AABB2.normalize(bb2_2, bb2_2); // x1 should be further left! if (bb2_2[0] < bb2_1[0]) { return $rectangle_rectangle( bb2_2[0], bb2_2[1], bb2_2[2], bb2_2[3], bb2_1[0], bb2_1[1], bb2_1[2], bb2_1[3], collision === true, distance === true ); } return $rectangle_rectangle( bb2_1[0], bb2_1[1], bb2_1[2], bb2_1[3], bb2_2[0], bb2_2[1], bb2_2[2], bb2_2[3], collision === true, distance === true ); } /** * @param {AABB2} bb2 * @param {Vec2} vec2 * @param {Boolean} collision * @param {Boolean} distance */ function bb2_vec2(bb2, vec2, collision, distance) { return $rectangle_vec2(bb2[0], bb2[1], bb2[2], bb2[3], vec2[0], vec2[1], collision === true, distance === true); } /** * @param {Vec2} vec2 * @param {AABB2} bb2 * @param {Boolean} collision * @param {Boolean} distance */ function vec2_bb2(vec2, bb2, collision, distance) { return $rectangle_vec2(bb2[0], bb2[1], bb2[2], bb2[3], vec2[0], vec2[1], collision === true, distance === true); } /** * @TODO segments of collision * * @param {Rectangle} rect1 * @param {Rectangle} rect2 * @param {Boolean} collision * @param {Boolean} distance */ function rectangle_rectangle(rect1, rect2, collision, distance) { Rectangle.normalize(rect1, rect1); Rectangle.normalize(rect2, rect2); // r1 should be further left! if (rect2[0][0] < rect1[0][0]) { return $rectangle_rectangle( rect2[0][0], rect2[0][1], rect2[1][0], rect2[1][1], rect1[0][0], rect1[0][1], rect1[1][0], rect1[1][1], collision === true, distance === true ); } return $rectangle_rectangle( rect1[0][0], rect1[0][1], rect1[1][0], rect1[1][1], rect2[0][0], rect2[0][1], rect2[1][0], rect2[1][1], collision === true, distance === true ); } /** * @TODO segments of collision * * @param {AABB2} bb2 * @param {AABB2} rect * @param {Boolean} collision * @param {Boolean} distance */ function bb2_rectangle(bb2, rect, collision, distance) { AABB2.normalize(bb2, bb2); Rectangle.normalize(rect, rect); // r1 should be further left! if (bb2[0] < rect[0][0]) { return $rectangle_rectangle( rect[0][0], rect[0][1], rect[1][0], rect[1][1], bb2[0], bb2[1], bb2[2], bb2[3], collision === true, distance === true ); } return $rectangle_rectangle( bb2[0], bb2[1], bb2[2], bb2[3], rect[0][0], rect[0][1], rect[1][0], rect[1][1], collision === true, distance === true ); } /** * * @param {AABB2} rect * @param {AABB2} bb2 * @param {Boolean} collision * @param {Boolean} distance */ function rectangle_bb2(rect, bb2, collision, distance) { return bb2_rectangle(bb2, rect, collision, distance); } /** * * @param {AABB2} rect * @param {Vec2} vec2 * @param {Boolean} collision * @param {Boolean} distance */ function rectangle_vec2(rect, vec2, collision, distance) { return $rectangle_vec2(rect[0][0], rect[0][1], rect[1][0], rect[1][1], vec2[0], vec2[1], collision === true, distance === true); } /** * * @param {Vec2} vec2 * @param {AABB2} rect * @param {Boolean} collision * @param {Boolean} distance */ function vec2_rectangle(vec2, rect, collision, distance) { return $rectangle_vec2(rect[0][0], rect[0][1], rect[1][0], rect[1][1], vec2[0], vec2[1], collision === true, distance === true); } /** * * @param {Circle} circle * @param {Vec2} vec2 * @param {Boolean} collision * @param {Boolean} distance */ function circle_vec2(circle, vec2, collision, distance) { collision = collision === true; distance = distance === true; var distance_to_center = Vec2.distance(circle[0], vec2); if (near(distance_to_center, circle[1])) { return {reason: COLLIDE, points: [vec2]}; } if (distance_to_center < circle[1]) { return {reason: INSIDE, distance: abs(distance_to_center - circle[1])}; } return {reason: OUTSIDE, distance: abs(distance_to_center - circle[1])}; } /** * * @param {Vec2} vec2 * @param {Circle} circle * @param {Boolean} collision * @param {Boolean} distance */ function vec2_circle(vec2, circle, collision, distance) { circle_vec2(circle, vec2, collision, distance); } /** * * @param {Circle} a_circle * @param {Circle} b_circle * @param {Boolean} collision * @param {Boolean} distance */ function circle_circle(a_circle, b_circle, collision, distance) { collision = collision === true; distance = distance === true; var c1 = a_circle[0], c2 = b_circle[0], r1 = a_circle[1], r2 = b_circle[1], r1sq = r1 * r1, r2sq = r2 * r2, // Determine minimum and maximum radius where circles can intersect r_max = r1 + r2, r_min, // Determine actual distance between circle circles c_dist_sq = Vec2.distanceSq(c1, c2), c_dist, a, h, b, points, z; if (c_dist_sq > r_max * r_max) { return {reason: OUTSIDE}; } r_min = r1 - r2; if (c_dist_sq < r_min * r_min) { return {reason: INSIDE}; } if (collision === false) { return {reason: COLLIDE}; } points = []; c_dist = sqrt(c_dist_sq); a = (r1sq - r2sq + c_dist_sq) / (2 * c_dist); z = r1sq - a * a; h = sqrt(z > 0 ? z : -z); Vec2.lerp(aux_vec2, c1, c2, a / c_dist); b = h / c_dist; points.push([aux_vec2[0] - b * (c2[1] - c1[1]), aux_vec2[1] + b * (c2[0] - c1[0])]); points.push([aux_vec2[0] + b * (c2[1] - c1[1]), aux_vec2[1] - b * (c2[0] - c1[0])]); return {reason: COLLIDE, points: points}; } /** * * @param {Circle} circle * @param {AABB2} bb2 * @param {Boolean} collision * @param {Boolean} distance */ function circle_bb2(circle, bb2, collision, distance) { return $circle_rectangle(circle[0][0], circle[0][1], circle[1], bb2[0], bb2[1], bb2[2], bb2[3], collision === true, distance === true); } /** * * @param {AABB2} bb2 * @param {Circle} circle * @param {Boolean} collision * @param {Boolean} distance */ function bb2_circle(bb2, circle, collision, distance) { return $circle_rectangle(circle[0][0], circle[0][1], circle[1], bb2[0], bb2[1], bb2[2], bb2[3], collision === true, distance === true); } /** * * @param {Circle} circle * @param {Rectangle} rect * @param {Boolean} collision * @param {Boolean} distance */ function circle_rectangle(circle, rect, collision, distance) { return $circle_rectangle(circle[0][0], circle[0][1], circle[1], rect[0][0], rect[0][1], rect[1][0], rect[1][1], collision === true, distance === true); } /** * * @param {Rectangle} rect * @param {Circle} circle * @param {Boolean} collision * @param {Boolean} distance */ function rectangle_circle(rect, circle, collision, distance) { return $circle_rectangle(circle[0][0], circle[0][1], circle[1], rect[0][0], rect[0][1], rect[1][0], rect[1][1], collision === true, distance === true); } /** * * @param {Circle} circle * @param {Segment2} seg2 * @param {Boolean} collision * @param {Boolean} distance */ function circle_segment2(circle, seg2, collision, distance) { return $circle_segment2( circle[0][0], circle[0][1], circle[1], seg2[0], seg2[1], seg2[2], seg2[3], collision === true, distance === true ); } /** * * @param {Segment2} seg2 * @param {Circle} circle * @param {Boolean} collision * @param {Boolean} distance */ function segment2_circle(seg2, circle, collision, distance) { return $circle_segment2( circle[0][0], circle[0][1], circle[1], seg2[0], seg2[1], seg2[2], seg2[3], collision === true, distance === true ); } /** * * @param {Line2} line2_2 * @param {Line2} line2_1 * @param {Boolean} collision * @param {Boolean} distance */ function line2_line2(line2_1, line2_2, collision, distance) { collision = collision === true; distance = distance === true; var a1 = [line2_1[0][0], line2_1[0][1]], a2 = [0, 0], // XXX check! m,1 ?? b1 = [line2_2[0][0], line2_2[0][1]], b2 = [0, 0], ua_t, ub_t, u_b, ua, ub; Vec2.add(a2, a1, [line2_1[1], 1]); Vec2.add(b2, b1, [line2_2[1], 1]); ua_t = (b2[0] - b1[0]) * (a1[1] - b1[1]) - (b2[1] - b1[1]) * (a1[0] - b1[0]); ub_t = (a2[0] - a1[0]) * (a1[1] - b1[1]) - (a2[1] - a1[1]) * (a1[0] - b1[0]); u_b = (b2[1] - b1[1]) * (a2[0] - a1[0]) - (b2[0] - b1[0]) * (a2[1] - a1[1]); if (u_b !== 0) { ua = ua_t / u_b; ub = ub_t / u_b; if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { if (collision === false) { return {reason: COLLIDE}; } return {reason: COLLIDE, points: [[a1[0] + ua * (a2[0] - a1[0]), a1[1] + ua * (a2[1] - a1[1])]]}; } return {reason: OUTSIDE}; } if (ua_t === 0 || ub_t === 0) { return {reason: COINCIDENT}; } return {reason: PARALLEL}; } /** * * @param {Segment2} seg2_2 * @param {Segment2} seg2_1 * @param {Boolean} collision * @param {Boolean} distance */ function segment2_segment2(seg2_1, seg2_2, collision, distance) { collision = collision === true; distance = distance === true; var mua, mub, denom, numera, numerb, points, i, max, minp, maxp, dist; denom = (seg2_2[3] - seg2_2[1]) * (seg2_1[2] - seg2_1[0]) - (seg2_2[2] - seg2_2[0]) * (seg2_1[3] - seg2_1[1]); numera = (seg2_2[2] - seg2_2[0]) * (seg2_1[1] - seg2_2[1]) - (seg2_2[3] - seg2_2[1]) * (seg2_1[0] - seg2_2[0]); numerb = (seg2_1[2] - seg2_1[0]) * (seg2_1[1] - seg2_2[1]) - (seg2_1[3] - seg2_1[1]) * (seg2_1[0] - seg2_2[0]); /* Are the line coincident? */ if (Math.abs(numera) < Math.EPS && Math.abs(numerb) < Math.EPS && Math.abs(denom) < Math.EPS) { if (collision === false) { return { reason : COLLIDE }; } // check if the intersections is a line! points = []; points.push(segment2_vec2(seg2_2, [seg2_1[0], seg2_1[1]])); points.push(segment2_vec2(seg2_2, [seg2_1[2], seg2_1[3]])); points.push(segment2_vec2(seg2_1, [seg2_2[0], seg2_2[1]])); points.push(segment2_vec2(seg2_1, [seg2_2[2], seg2_2[3]])); // now check those intersections, remove no intersections! max = points.length; minp = { distance: false, point: null}; maxp = { distance: false, point: null}; for (i = 0; i < max; ++i) { if (points[i].reason <= COLLIDE) { // no collision points.splice(i, 1); --i; max = points.length; } else { dist = Vec2.lengthSq(points[i].points[0]); if (minp.distance === false || minp.distance > dist) { minp.distance = dist; minp.point = points[i].points[0]; } if (maxp.distance === false || minp.distance < dist) { maxp.distance = dist; maxp.point = points[i].points[0]; } } } if (points.length > 1) { //line intersection! return { reason : COLLIDE, points: [], segments: [[minp.point[0], minp.point[1], maxp.point[0], maxp.point[1]]] }; } return { reason : COINCIDENT }; } /* Are the line parallel */ if (Math.abs(denom) < Math.EPS) { return {reason: PARALLEL}; } /* Is the intersection along the the segments */ mua = numera / denom; mub = numerb / denom; if (mua < 0 || mua > 1 || mub < 0 || mub > 1) { return {reason: OUTSIDE}; } if (collision === false) { return {reason: COLLIDE}; } return {reason: COLLIDE, points: [[seg2_1[0] + mua * (seg2_1[2] - seg2_1[0]), seg2_1[1] + mua * (seg2_1[3] - seg2_1[1])]]}; } /** * * @param {Segment2} seg2 * @param {Vec2} vec2 * @param {Boolean} collision * @param {Boolean} distance */ function segment2_vec2(seg2, vec2) { var dis = Distance.segment2_vec2(seg2, vec2); if (dis === 0) { return { reason : COLLIDE, points: [vec2] }; } return { reason : OUTSIDE, distance: dis }; } /** * * @param {Vec2} vec2 * @param {Segment2} seg2 * @param {Boolean} collision * @param {Boolean} distance */ function vec2_segment2(vec2, seg2) { return segment2_vec2(seg2, vec2); } /** * @TODO this is just a fast-code-version, no optimization no for real-time * * @param {Polygon} a_poly * @param {Polygon} b_poly * @param {Boolean} collision * @param {Boolean} distance */ function polygon_polygon(a_poly, b_poly) { var alen = a_poly.length, blen = b_poly.length, a, a2, b, b2, points = []; for (a = 0; a < alen; ++a) { a2 = a + 1; a2 = a2 == alen ? 0 : a2; for (b = 0; b < blen; ++b) { b2 = b + 1; b2 = b2 == blen ? 0 : b2; r = segment2_segment2( [a_poly[a][0], a_poly[a][1], a_poly[a2][0], a_poly[a2][1]], [b_poly[b][0], b_poly[b][1], b_poly[b2][0], b_poly[b2][1]], true, true ); if (r.points) { points.push(r.points[0]); } } } if (points.length) { return { reason : COLLIDE, points: points, } } return { reason : OUTSIDE } } /** * @class Intersection */ var Intersection = { OUTSIDE: OUTSIDE, PARALLEL: PARALLEL, COLLIDE: COLLIDE, INSIDE: INSIDE, COINCIDENT: COINCIDENT, TANGENT: TANGENT, bb2_bb2: bb2_bb2, bb2_vec2: bb2_vec2, vec2_bb2: vec2_bb2, rectangle_rectangle: rectangle_rectangle, bb2_rectangle: bb2_rectangle, rectangle_bb2: rectangle_bb2, rectangle_vec2: rectangle_vec2, vec2_rectangle: vec2_rectangle, circle_vec2: circle_vec2, vec2_circle: vec2_circle, circle_circle: circle_circle, circle_bb2: circle_bb2, bb2_circle: bb2_circle, circle_rectangle: circle_rectangle, rectangle_circle: rectangle_circle, circle_segment2: circle_segment2, segment2_circle: segment2_circle, line2_line2: line2_line2, segment2_segment2: segment2_segment2, segment2_vec2: segment2_vec2, vec2_segment2: vec2_segment2, polygon_polygon: polygon_polygon, $rectangle_rectangle: $rectangle_rectangle, $rectangle_vec2: $rectangle_vec2, $circle_segment2: $circle_segment2, $circle_rectangle: $circle_rectangle }; module.exports = Intersection;