} bPoints The points in the second polygon.
+ * @param {Vector} axis The axis (unit sized) to test against. The points of both polygons
+ * will be projected onto this axis.
+ * @param {Response=} response A Response object (optional) which will be populated
+ * if the axis is not a separating 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(aPos, bPos, aPoints, bPoints, axis, response) {
+ var rangeA = T_ARRAYS.pop();
+ var rangeB = T_ARRAYS.pop();
+ // The magnitude of the offset between the two polygons
+ var offsetV = T_VECTORS.pop().copy(bPos).sub(aPos);
+ var projectedOffset = offsetV.dot(axis);
+ // Project the polygons onto the axis.
+ flattenPointsOn(aPoints, axis, rangeA);
+ flattenPointsOn(bPoints, 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]) {
+ T_VECTORS.push(offsetV);
+ T_ARRAYS.push(rangeA);
+ T_ARRAYS.push(rangeB);
+ return true;
+ }
+ // This is not a separating axis. If we're calculating a response, calculate the overlap.
+ if (response) {
+ var overlap = 0;
+ // A starts further left than B
+ if (rangeA[0] < rangeB[0]) {
+ 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];
+ response['bInA'] = false;
+ // B is fully inside A. Pick the shortest way out.
+ } else {
+ var option1 = rangeA[1] - rangeB[0];
+ var option2 = rangeB[1] - rangeA[0];
+ overlap = option1 < option2 ? option1 : -option2;
+ }
+ // B starts further left than A
+ } else {
+ 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];
+ response['aInB'] = false;
+ // A is fully inside B. Pick the shortest way out.
+ } else {
+ var option1 = rangeA[1] - rangeB[0];
+ var 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 = Math.abs(overlap);
+ if (absOverlap < response['overlap']) {
+ response['overlap'] = absOverlap;
+ response['overlapN'].copy(axis);
+ if (overlap < 0) {
+ response['overlapN'].reverse();
+ }
+ }
+ }
+ T_VECTORS.push(offsetV);
+ T_ARRAYS.push(rangeA);
+ T_ARRAYS.push(rangeB);
+ return false;
+ }
+ SAT['isSeparatingAxis'] = isSeparatingAxis;
+
+ // Calculates which Voronoi 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 {Vector} line The line segment.
+ * @param {Vector} point The point.
+ * @return {number} LEFT_VORONOI_REGION (-1) if it is the left region,
+ * MIDDLE_VORONOI_REGION (0) if it is the middle region,
+ * RIGHT_VORONOI_REGION (1) if it is the right region.
+ */
+ function voronoiRegion(line, point) {
+ var len2 = line.len2();
+ var dp = point.dot(line);
+ // If the point is beyond the start of the line, it is in the
+ // left voronoi region.
+ if (dp < 0) { return LEFT_VORONOI_REGION; }
+ // If the point is beyond the end of the line, it is in the
+ // right voronoi region.
+ else if (dp > len2) { return RIGHT_VORONOI_REGION; }
+ // Otherwise, it's in the middle one.
+ else { return MIDDLE_VORONOI_REGION; }
+ }
+ // Constants for Voronoi regions
+ /**
+ * @const
+ */
+ var LEFT_VORONOI_REGION = -1;
+ /**
+ * @const
+ */
+ var MIDDLE_VORONOI_REGION = 0;
+ /**
+ * @const
+ */
+ var RIGHT_VORONOI_REGION = 1;
+
+ // ## Collision Tests
+
+ // Check if a point is inside a circle.
+ /**
+ * @param {Vector} p The point to test.
+ * @param {Circle} c The circle to test.
+ * @return {boolean} true if the point is inside the circle, false if it is not.
+ */
+ function pointInCircle(p, c) {
+ var differenceV = T_VECTORS.pop().copy(p).sub(c['pos']).sub(c['offset']);
+ var radiusSq = c['r'] * c['r'];
+ var distanceSq = differenceV.len2();
+ T_VECTORS.push(differenceV);
+ // If the distance between is smaller than the radius then the point is inside the circle.
+ return distanceSq <= radiusSq;
+ }
+ SAT['pointInCircle'] = pointInCircle;
+
+ // Check if a point is inside a convex polygon.
+ /**
+ * @param {Vector} p 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 pointInPolygon(p, poly) {
+ TEST_POINT['pos'].copy(p);
+ T_RESPONSE.clear();
+ var result = testPolygonPolygon(TEST_POINT, poly, T_RESPONSE);
+ if (result) {
+ result = T_RESPONSE['aInB'];
+ }
+ return result;
+ }
+ SAT['pointInPolygon'] = pointInPolygon;
+
+ // Check if two circles collide.
+ /**
+ * @param {Circle} a The first circle.
+ * @param {Circle} b The second circle.
+ * @param {Response=} response Response object (optional) that will be populated if
+ * the circles intersect.
+ * @return {boolean} true if the circles intersect, false if they don't.
+ */
+ function testCircleCircle(a, b, response) {
+ // Check if the distance between the centers of the two
+ // circles is greater than their combined radius.
+ var differenceV = T_VECTORS.pop().copy(b['pos']).add(b['offset']).sub(a['pos']).sub(a['offset']);
+ var totalRadius = a['r'] + b['r'];
+ var totalRadiusSq = totalRadius * totalRadius;
+ var distanceSq = differenceV.len2();
+ // If the distance is bigger than the combined radius, they don't intersect.
+ if (distanceSq > totalRadiusSq) {
+ T_VECTORS.push(differenceV);
+ return false;
+ }
+ // They intersect. If we're calculating a response, calculate the overlap.
+ if (response) {
+ var dist = Math.sqrt(distanceSq);
+ response['a'] = a;
+ response['b'] = b;
+ response['overlap'] = totalRadius - dist;
+ response['overlapN'].copy(differenceV.normalize());
+ response['overlapV'].copy(differenceV).scale(response['overlap']);
+ response['aInB'] = a['r'] <= b['r'] && dist <= b['r'] - a['r'];
+ response['bInA'] = b['r'] <= a['r'] && dist <= a['r'] - b['r'];
+ }
+ T_VECTORS.push(differenceV);
+ return true;
+ }
+ SAT['testCircleCircle'] = testCircleCircle;
+
+ // Check if a polygon and a circle collide.
+ /**
+ * @param {Polygon} polygon The polygon.
+ * @param {Circle} circle The circle.
+ * @param {Response=} response Response object (optional) that will be populated if
+ * they interset.
+ * @return {boolean} true if they intersect, false if they don't.
+ */
+ function testPolygonCircle(polygon, circle, response) {
+ // Get the position of the circle relative to the polygon.
+ var circlePos = T_VECTORS.pop().copy(circle['pos']).add(circle['offset']).sub(polygon['pos']);
+ var radius = circle['r'];
+ var radius2 = radius * radius;
+ var points = polygon['calcPoints'];
+ var len = points.length;
+ var edge = T_VECTORS.pop();
+ var point = T_VECTORS.pop();
+
+ // For each edge in the polygon:
+ for (var i = 0; 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.
+ edge.copy(polygon['edges'][i]);
+ // Calculate the center of the circle relative to the starting point of the edge.
+ point.copy(circlePos).sub(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 (response && point.len2() > radius2) {
+ response['aInB'] = false;
+ }
+
+ // Calculate which Voronoi region the center of the circle is in.
+ var region = voronoiRegion(edge, point);
+ // If it's the left region:
+ if (region === LEFT_VORONOI_REGION) {
+ // We need to make sure we're in the RIGHT_VORONOI_REGION of the previous edge.
+ edge.copy(polygon['edges'][prev]);
+ // Calculate the center of the circle relative the starting point of the previous edge
+ var point2 = T_VECTORS.pop().copy(circlePos).sub(points[prev]);
+ region = voronoiRegion(edge, point2);
+ if (region === RIGHT_VORONOI_REGION) {
+ // It's in the region we want. Check if the circle intersects the point.
+ var dist = point.len();
+ if (dist > radius) {
+ // No intersection
T_VECTORS.push(circlePos);
T_VECTORS.push(edge);
T_VECTORS.push(point);
- return true;
+ T_VECTORS.push(point2);
+ return false;
+ } else if (response) {
+ // It intersects, calculate the overlap.
+ response['bInA'] = false;
+ overlapN = point.normalize();
+ overlap = radius - dist;
}
- SAT["testPolygonCircle"] = testPolygonCircle;
-
- // 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 {Circle} circle The circle.
- * @param {Polygon} polygon The polygon.
- * @param {Response=} response Response object (optional) that will be populated if
- * they interset.
- * @return {boolean} true if they intersect, false if they don't.
- */
- function testCirclePolygon(circle, polygon, response) {
- // Test the polygon against the circle.
- var result = testPolygonCircle(polygon, circle, response);
- if (result && response) {
- // Swap A and B in the response.
- var a = response["a"];
- var aInB = response["aInB"];
- response["overlapN"].reverse();
- response["overlapV"].reverse();
- response["a"] = response["b"];
- response["b"] = a;
- response["aInB"] = response["bInA"];
- response["bInA"] = aInB;
- }
- return result;
+ }
+ T_VECTORS.push(point2);
+ // If it's the right region:
+ } else if (region === RIGHT_VORONOI_REGION) {
+ // We need to make sure we're in the left region on the next edge
+ edge.copy(polygon['edges'][next]);
+ // Calculate the center of the circle relative to the starting point of the next edge.
+ point.copy(circlePos).sub(points[next]);
+ region = voronoiRegion(edge, point);
+ if (region === LEFT_VORONOI_REGION) {
+ // It's in the region we want. Check if the circle intersects the point.
+ var dist = point.len();
+ if (dist > radius) {
+ // No intersection
+ T_VECTORS.push(circlePos);
+ T_VECTORS.push(edge);
+ T_VECTORS.push(point);
+ return false;
+ } else if (response) {
+ // It intersects, calculate the overlap.
+ response['bInA'] = false;
+ overlapN = point.normalize();
+ overlap = radius - dist;
}
- SAT["testCirclePolygon"] = testCirclePolygon;
-
- // Checks whether polygons collide.
- /**
- * @param {Polygon} a The first polygon.
- * @param {Polygon} b The second polygon.
- * @param {Response=} response Response object (optional) that will be populated if
- * they interset.
- * @return {boolean} true if they intersect, false if they don't.
- */
- function testPolygonPolygon(a, b, response) {
- var aPoints = a["calcPoints"];
- var aLen = aPoints.length;
- var bPoints = b["calcPoints"];
- var bLen = bPoints.length;
- // If any of the edge normals of A is a separating axis, no intersection.
- for (var i = 0; i < aLen; i++) {
- if (
- isSeparatingAxis(
- a["pos"],
- b["pos"],
- aPoints,
- bPoints,
- a["normals"][i],
- response,
- )
- ) {
- return false;
- }
- }
- // If any of the edge normals of B is a separating axis, no intersection.
- for (var i = 0; i < bLen; i++) {
- if (
- isSeparatingAxis(
- a["pos"],
- b["pos"],
- aPoints,
- bPoints,
- b["normals"][i],
- response,
- )
- ) {
- return false;
- }
- }
- // Since none of the edge normals of A or B are a separating axis, there is an intersection
- // and we've already calculated the smallest overlap (in isSeparatingAxis). Calculate the
- // final overlap vector.
- if (response) {
- response["a"] = a;
- response["b"] = b;
- response["overlapV"]
- .copy(response["overlapN"])
- .scale(response["overlap"]);
- }
- return true;
+ }
+ // Otherwise, it's the middle region:
+ } else {
+ // Need to check if the circle is intersecting the edge,
+ // Change the edge into its "edge normal".
+ var normal = edge.perp().normalize();
+ // Find the perpendicular distance between the center of the
+ // circle and the edge.
+ var dist = point.dot(normal);
+ var distAbs = Math.abs(dist);
+ // If the circle is on the outside of the edge, there is no intersection.
+ if (dist > 0 && distAbs > radius) {
+ // No intersection
+ T_VECTORS.push(circlePos);
+ T_VECTORS.push(normal);
+ T_VECTORS.push(point);
+ return false;
+ } else if (response) {
+ // 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) {
+ response['bInA'] = false;
}
- SAT["testPolygonPolygon"] = testPolygonPolygon;
+ }
+ }
- return SAT;
- });
+ // If this is the smallest overlap we've seen, keep it.
+ // (overlapN may be null if the circle was in the wrong Voronoi region).
+ if (overlapN && response && Math.abs(overlap) < Math.abs(response['overlap'])) {
+ response['overlap'] = overlap;
+ response['overlapN'].copy(overlapN);
+ }
+ }
+
+ // Calculate the final overlap vector - based on the smallest overlap.
+ if (response) {
+ response['a'] = polygon;
+ response['b'] = circle;
+ response['overlapV'].copy(response['overlapN']).scale(response['overlap']);
+ }
+ T_VECTORS.push(circlePos);
+ T_VECTORS.push(edge);
+ T_VECTORS.push(point);
+ return true;
+ }
+ SAT['testPolygonCircle'] = testPolygonCircle;
+
+ // 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 {Circle} circle The circle.
+ * @param {Polygon} polygon The polygon.
+ * @param {Response=} response Response object (optional) that will be populated if
+ * they interset.
+ * @return {boolean} true if they intersect, false if they don't.
+ */
+ function testCirclePolygon(circle, polygon, response) {
+ // Test the polygon against the circle.
+ var result = testPolygonCircle(polygon, circle, response);
+ if (result && response) {
+ // Swap A and B in the response.
+ var a = response['a'];
+ var aInB = response['aInB'];
+ response['overlapN'].reverse();
+ response['overlapV'].reverse();
+ response['a'] = response['b'];
+ response['b'] = a;
+ response['aInB'] = response['bInA'];
+ response['bInA'] = aInB;
+ }
+ return result;
+ }
+ SAT['testCirclePolygon'] = testCirclePolygon;
+
+ // Checks whether polygons collide.
+ /**
+ * @param {Polygon} a The first polygon.
+ * @param {Polygon} b The second polygon.
+ * @param {Response=} response Response object (optional) that will be populated if
+ * they interset.
+ * @return {boolean} true if they intersect, false if they don't.
+ */
+ function testPolygonPolygon(a, b, response) {
+ var aPoints = a['calcPoints'];
+ var aLen = aPoints.length;
+ var bPoints = b['calcPoints'];
+ var bLen = bPoints.length;
+ // If any of the edge normals of A is a separating axis, no intersection.
+ for (var i = 0; i < aLen; i++) {
+ if (isSeparatingAxis(a['pos'], b['pos'], aPoints, bPoints, a['normals'][i], response)) {
+ return false;
+ }
+ }
+ // If any of the edge normals of B is a separating axis, no intersection.
+ for (var i = 0; i < bLen; i++) {
+ if (isSeparatingAxis(a['pos'], b['pos'], aPoints, bPoints, b['normals'][i], response)) {
+ return false;
+ }
+ }
+ // Since none of the edge normals of A or B are a separating axis, there is an intersection
+ // and we've already calculated the smallest overlap (in isSeparatingAxis). Calculate the
+ // final overlap vector.
+ if (response) {
+ response['a'] = a;
+ response['b'] = b;
+ response['overlapV'].copy(response['overlapN']).scale(response['overlap']);
+ }
+ return true;
+ }
+ SAT['testPolygonPolygon'] = testPolygonPolygon;
+
+ return SAT;
+}));
- /***/
- },
- /***/ "./src/base-system.ts":
- /*!****************************!*\
+/***/ }),
+
+/***/ "./src/base-system.ts":
+/*!****************************!*\
!*** ./src/base-system.ts ***!
\****************************/
- /***/ (__unused_webpack_module, exports, __webpack_require__) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.BaseSystem = void 0;
- const box_1 = __webpack_require__(
- /*! ./bodies/box */ "./src/bodies/box.ts",
- );
- const circle_1 = __webpack_require__(
- /*! ./bodies/circle */ "./src/bodies/circle.ts",
- );
- const ellipse_1 = __webpack_require__(
- /*! ./bodies/ellipse */ "./src/bodies/ellipse.ts",
- );
- const line_1 = __webpack_require__(
- /*! ./bodies/line */ "./src/bodies/line.ts",
- );
- const point_1 = __webpack_require__(
- /*! ./bodies/point */ "./src/bodies/point.ts",
- );
- const polygon_1 = __webpack_require__(
- /*! ./bodies/polygon */ "./src/bodies/polygon.ts",
- );
- const model_1 = __webpack_require__(/*! ./model */ "./src/model.ts");
- const optimized_1 = __webpack_require__(
- /*! ./optimized */ "./src/optimized.ts",
- );
- const utils_1 = __webpack_require__(/*! ./utils */ "./src/utils.ts");
- /**
- * very base collision system (create, insert, update, draw, remove)
- */
- class BaseSystem extends model_1.RBush {
- /**
- * create point at position with options and add to system
- */
- createPoint(position, options) {
- const point = new point_1.Point(position, options);
- this.insert(point);
- return point;
- }
- /**
- * create line at position with options and add to system
- */
- createLine(start, end, options) {
- const line = new line_1.Line(start, end, options);
- this.insert(line);
- return line;
- }
- /**
- * create circle at position with options and add to system
- */
- createCircle(position, radius, options) {
- const circle = new circle_1.Circle(position, radius, options);
- this.insert(circle);
- return circle;
- }
- /**
- * create box at position with options and add to system
- */
- createBox(position, width, height, options) {
- const box = new box_1.Box(position, width, height, options);
- this.insert(box);
- return box;
- }
- /**
- * create ellipse at position with options and add to system
- */
- createEllipse(position, radiusX, radiusY = radiusX, step, options) {
- const ellipse = new ellipse_1.Ellipse(
- position,
- radiusX,
- radiusY,
- step,
- options,
- );
- this.insert(ellipse);
- return ellipse;
- }
- /**
- * create polygon at position with options and add to system
- */
- createPolygon(position, points, options) {
- const polygon = new polygon_1.Polygon(position, points, options);
- this.insert(polygon);
- return polygon;
- }
- /**
- * re-insert body into collision tree and update its bbox
- * every body can be part of only one system
- */
- insert(body) {
- body.bbox = body.getAABBAsBBox();
- if (body.system) {
- // allow end if body inserted and not moved
- if (!(0, utils_1.bodyMoved)(body)) {
+/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.BaseSystem = void 0;
+const model_1 = __webpack_require__(/*! ./model */ "./src/model.ts");
+const utils_1 = __webpack_require__(/*! ./utils */ "./src/utils.ts");
+const optimized_1 = __webpack_require__(/*! ./optimized */ "./src/optimized.ts");
+const box_1 = __webpack_require__(/*! ./bodies/box */ "./src/bodies/box.ts");
+const circle_1 = __webpack_require__(/*! ./bodies/circle */ "./src/bodies/circle.ts");
+const ellipse_1 = __webpack_require__(/*! ./bodies/ellipse */ "./src/bodies/ellipse.ts");
+const line_1 = __webpack_require__(/*! ./bodies/line */ "./src/bodies/line.ts");
+const point_1 = __webpack_require__(/*! ./bodies/point */ "./src/bodies/point.ts");
+const polygon_1 = __webpack_require__(/*! ./bodies/polygon */ "./src/bodies/polygon.ts");
+/**
+ * very base collision system (create, insert, update, draw, remove)
+ */
+class BaseSystem extends model_1.RBush {
+ /**
+ * create point at position with options and add to system
+ */
+ createPoint(position, options) {
+ const point = new point_1.Point(position, options);
+ this.insert(point);
+ return point;
+ }
+ /**
+ * create line at position with options and add to system
+ */
+ createLine(start, end, options) {
+ const line = new line_1.Line(start, end, options);
+ this.insert(line);
+ return line;
+ }
+ /**
+ * create circle at position with options and add to system
+ */
+ createCircle(position, radius, options) {
+ const circle = new circle_1.Circle(position, radius, options);
+ this.insert(circle);
+ return circle;
+ }
+ /**
+ * create box at position with options and add to system
+ */
+ createBox(position, width, height, options) {
+ const box = new box_1.Box(position, width, height, options);
+ this.insert(box);
+ return box;
+ }
+ /**
+ * create ellipse at position with options and add to system
+ */
+ createEllipse(position, radiusX, radiusY = radiusX, step, options) {
+ const ellipse = new ellipse_1.Ellipse(position, radiusX, radiusY, step, options);
+ this.insert(ellipse);
+ return ellipse;
+ }
+ /**
+ * create polygon at position with options and add to system
+ */
+ createPolygon(position, points, options) {
+ const polygon = new polygon_1.Polygon(position, points, options);
+ this.insert(polygon);
+ return polygon;
+ }
+ /**
+ * re-insert body into collision tree and update its bbox
+ * every body can be part of only one system
+ */
+ insert(body) {
+ body.bbox = body.getAABBAsBBox();
+ if (body.system) {
+ // allow end if body inserted and not moved
+ if (!(0, utils_1.bodyMoved)(body)) {
return this;
- }
- // old bounding box *needs* to be removed
- body.system.remove(body);
}
- // only then we update min, max
- body.minX = body.bbox.minX - body.padding;
- body.minY = body.bbox.minY - body.padding;
- body.maxX = body.bbox.maxX + body.padding;
- body.maxY = body.bbox.maxY + body.padding;
- // reinsert bounding box to collision tree
- return super.insert(body);
- }
- /**
- * updates body in collision tree
- */
- updateBody(body) {
- body.updateBody();
- }
- /**
- * update all bodies aabb
- */
- update() {
- (0, optimized_1.forEach)(this.all(), (body) => {
- this.updateBody(body);
- });
- }
- /**
- * draw exact bodies colliders outline
- */
- draw(context) {
- (0, optimized_1.forEach)(this.all(), (body) => {
- body.draw(context);
- });
- }
- /**
- * draw bounding boxes hierarchy outline
- */
- drawBVH(context) {
- const drawChildren = (body) => {
- (0, utils_1.drawBVH)(context, body);
- if (body.children) {
- (0, optimized_1.forEach)(body.children, drawChildren);
- }
- };
- (0, optimized_1.forEach)(this.data.children, drawChildren);
- }
- /**
- * remove body aabb from collision tree
- */
- remove(body, equals) {
- body.system = undefined;
- return super.remove(body, equals);
- }
- /**
- * get object potential colliders
- * @deprecated because it's slower to use than checkOne() or checkAll()
- */
- getPotentials(body) {
- // filter here is required as collides with self
- return (0, optimized_1.filter)(
- this.search(body),
- (candidate) => candidate !== body,
- );
- }
- /**
- * used to find body deep inside data with finder function returning boolean found or not
- */
- traverse(traverseFunction, { children } = this.data) {
- return children === null || children === void 0
- ? void 0
- : children.find((body, index) => {
- if (!body) {
- return false;
- }
- if (
- body.typeGroup &&
- traverseFunction(body, children, index)
- ) {
- return true;
- }
- // if callback returns true, ends forEach
- if (body.children) {
- this.traverse(traverseFunction, body);
- }
- });
- }
+ // old bounding box *needs* to be removed
+ body.system.remove(body);
}
- exports.BaseSystem = BaseSystem;
+ // only then we update min, max
+ body.minX = body.bbox.minX - body.padding;
+ body.minY = body.bbox.minY - body.padding;
+ body.maxX = body.bbox.maxX + body.padding;
+ body.maxY = body.bbox.maxY + body.padding;
+ // reinsert bounding box to collision tree
+ return super.insert(body);
+ }
+ /**
+ * updates body in collision tree
+ */
+ updateBody(body) {
+ body.updateBody();
+ }
+ /**
+ * update all bodies aabb
+ */
+ update() {
+ (0, optimized_1.forEach)(this.all(), (body) => {
+ this.updateBody(body);
+ });
+ }
+ /**
+ * draw exact bodies colliders outline
+ */
+ draw(context) {
+ (0, optimized_1.forEach)(this.all(), (body) => {
+ body.draw(context);
+ });
+ }
+ /**
+ * draw bounding boxes hierarchy outline
+ */
+ drawBVH(context) {
+ const drawChildren = (body) => {
+ (0, utils_1.drawBVH)(context, body);
+ if (body.children) {
+ (0, optimized_1.forEach)(body.children, drawChildren);
+ }
+ };
+ (0, optimized_1.forEach)(this.data.children, drawChildren);
+ }
+ /**
+ * remove body aabb from collision tree
+ */
+ remove(body, equals) {
+ body.system = undefined;
+ return super.remove(body, equals);
+ }
+ /**
+ * get object potential colliders
+ * @deprecated because it's slower to use than checkOne() or checkAll()
+ */
+ getPotentials(body) {
+ // filter here is required as collides with self
+ return (0, optimized_1.filter)(this.search(body), (candidate) => candidate !== body);
+ }
+ /**
+ * used to find body deep inside data with finder function returning boolean found or not
+ */
+ traverse(traverseFunction, { children } = this.data) {
+ return children === null || children === void 0 ? void 0 : children.find((body, index) => {
+ if (!body) {
+ return false;
+ }
+ if (body.typeGroup && traverseFunction(body, children, index)) {
+ return true;
+ }
+ // if callback returns true, ends forEach
+ if (body.children) {
+ this.traverse(traverseFunction, body);
+ }
+ });
+ }
+}
+exports.BaseSystem = BaseSystem;
+
- /***/
- },
+/***/ }),
- /***/ "./src/bodies/box.ts":
- /*!***************************!*\
+/***/ "./src/bodies/box.ts":
+/*!***************************!*\
!*** ./src/bodies/box.ts ***!
\***************************/
- /***/ (__unused_webpack_module, exports, __webpack_require__) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Box = void 0;
- const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
- const utils_1 = __webpack_require__(/*! ../utils */ "./src/utils.ts");
- const polygon_1 = __webpack_require__(
- /*! ./polygon */ "./src/bodies/polygon.ts",
- );
+/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.Box = void 0;
+const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
+const polygon_1 = __webpack_require__(/*! ./polygon */ "./src/bodies/polygon.ts");
+const utils_1 = __webpack_require__(/*! ../utils */ "./src/utils.ts");
+/**
+ * collider - box
+ */
+class Box extends polygon_1.Polygon {
+ /**
+ * collider - box
+ */
+ constructor(position, width, height, options) {
+ super(position, (0, utils_1.createBox)(width, height), options);
/**
- * collider - box
+ * type of body
*/
- class Box extends polygon_1.Polygon {
- /**
- * collider - box
- */
- constructor(position, width, height, options) {
- super(position, (0, utils_1.createBox)(width, height), options);
- /**
- * type of body
- */
- this.type = model_1.BodyType.Box;
- /**
- * faster than type
- */
- this.typeGroup = model_1.BodyGroup.Box;
- /**
- * boxes are convex
- */
- this.isConvex = true;
- this._width = width;
- this._height = height;
- }
- /**
- * get box width
- */
- get width() {
- return this._width;
- }
- /**
- * set box width, update points
- */
- set width(width) {
- this._width = width;
- this.afterUpdateSize();
- }
- /**
- * get box height
- */
- get height() {
- return this._height;
- }
- /**
- * set box height, update points
- */
- set height(height) {
- this._height = height;
- this.afterUpdateSize();
- }
- /**
- * after setting width/height update translate
- * see https://github.com/Prozi/detect-collisions/issues/70
- */
- afterUpdateSize() {
- if (this.isCentered) {
- this.retranslate(false);
- }
- this.setPoints((0, utils_1.createBox)(this._width, this._height));
- if (this.isCentered) {
- this.retranslate();
- }
- }
- /**
- * do not attempt to use Polygon.updateIsConvex()
- */
- updateIsConvex() {
- return;
- }
- }
- exports.Box = Box;
-
- /***/
- },
-
- /***/ "./src/bodies/circle.ts":
- /*!******************************!*\
- !*** ./src/bodies/circle.ts ***!
- \******************************/
- /***/ (__unused_webpack_module, exports, __webpack_require__) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Circle = void 0;
- const sat_1 = __webpack_require__(
- /*! sat */ "./node_modules/.pnpm/sat@0.9.0/node_modules/sat/SAT.js",
- );
- const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
- const utils_1 = __webpack_require__(/*! ../utils */ "./src/utils.ts");
+ this.type = model_1.BodyType.Box;
/**
- * collider - circle
+ * faster than type
*/
- class Circle extends sat_1.Circle {
- /**
- * collider - circle
- */
- constructor(position, radius, options) {
- super((0, utils_1.ensureVectorPoint)(position), radius);
- /**
- * offset copy without angle applied
- */
- this.offsetCopy = { x: 0, y: 0 };
- /**
- * was the polygon modified and needs update in the next checkCollision
- */
- this.dirty = false;
- /*
- * circles are convex
- */
- this.isConvex = true;
- /**
- * circle type
- */
- this.type = model_1.BodyType.Circle;
- /**
- * faster than type
- */
- this.typeGroup = model_1.BodyGroup.Circle;
- /**
- * always centered
- */
- this.isCentered = true;
- (0, utils_1.extendBody)(this, options);
- this.unscaledRadius = radius;
- }
- /**
- * get this.pos.x
- */
- get x() {
- return this.pos.x;
- }
- /**
- * updating this.pos.x by this.x = x updates AABB
- */
- set x(x) {
- this.pos.x = x;
- this.markAsDirty();
- }
- /**
- * get this.pos.y
- */
- get y() {
- return this.pos.y;
- }
- /**
- * updating this.pos.y by this.y = y updates AABB
- */
- set y(y) {
- this.pos.y = y;
- this.markAsDirty();
- }
- /**
- * allow get scale
- */
- get scale() {
- return this.r / this.unscaledRadius;
- }
- /**
- * shorthand for setScale()
- */
- set scale(scale) {
- this.setScale(scale);
- }
- /**
- * scaleX = scale in case of Circles
- */
- get scaleX() {
- return this.scale;
- }
- /**
- * scaleY = scale in case of Circles
- */
- get scaleY() {
- return this.scale;
- }
- /**
- * group for collision filtering
- */
- get group() {
- return this._group;
- }
- set group(group) {
- this._group = (0, utils_1.getGroup)(group);
- }
- /**
- * update position
- */
- setPosition(x, y, update = true) {
- this.pos.x = x;
- this.pos.y = y;
- this.markAsDirty(update);
- return this;
- }
- /**
- * update scale
- */
- setScale(scaleX, _scaleY = scaleX, update = true) {
- this.r = this.unscaledRadius * Math.abs(scaleX);
- this.markAsDirty(update);
- return this;
- }
- /**
- * set rotation
- */
- setAngle(angle, update = true) {
- this.angle = angle;
- const { x, y } = this.getOffsetWithAngle();
- this.offset.x = x;
- this.offset.y = y;
- this.markAsDirty(update);
- return this;
- }
- /**
- * set offset from center
- */
- setOffset(offset, update = true) {
- this.offsetCopy.x = offset.x;
- this.offsetCopy.y = offset.y;
- const { x, y } = this.getOffsetWithAngle();
- this.offset.x = x;
- this.offset.y = y;
- this.markAsDirty(update);
- return this;
- }
- /**
- * get body bounding box, without padding
- */
- getAABBAsBBox() {
- const x = this.pos.x + this.offset.x;
- const y = this.pos.y + this.offset.y;
- return {
- minX: x - this.r,
- maxX: x + this.r,
- minY: y - this.r,
- maxY: y + this.r,
- };
- }
- /**
- * Draws collider on a CanvasRenderingContext2D's current path
- */
- draw(context) {
- const x = this.pos.x + this.offset.x;
- const y = this.pos.y + this.offset.y;
- const r = Math.abs(this.r);
- if (this.isTrigger) {
- const max = Math.max(8, this.r);
- for (let i = 0; i < max; i++) {
+ this.typeGroup = model_1.BodyGroup.Box;
+ /**
+ * boxes are convex
+ */
+ this.isConvex = true;
+ this._width = width;
+ this._height = height;
+ }
+ /**
+ * get box width
+ */
+ get width() {
+ return this._width;
+ }
+ /**
+ * set box width, update points
+ */
+ set width(width) {
+ this._width = width;
+ this.afterUpdateSize();
+ }
+ /**
+ * get box height
+ */
+ get height() {
+ return this._height;
+ }
+ /**
+ * set box height, update points
+ */
+ set height(height) {
+ this._height = height;
+ this.afterUpdateSize();
+ }
+ /**
+ * after setting width/height update translate
+ * see https://github.com/Prozi/detect-collisions/issues/70
+ */
+ afterUpdateSize() {
+ if (this.isCentered) {
+ this.retranslate(false);
+ }
+ this.setPoints((0, utils_1.createBox)(this._width, this._height));
+ if (this.isCentered) {
+ this.retranslate();
+ }
+ }
+ /**
+ * do not attempt to use Polygon.updateIsConvex()
+ */
+ updateIsConvex() {
+ return;
+ }
+}
+exports.Box = Box;
+
+
+/***/ }),
+
+/***/ "./src/bodies/circle.ts":
+/*!******************************!*\
+ !*** ./src/bodies/circle.ts ***!
+ \******************************/
+/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.Circle = void 0;
+const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
+const utils_1 = __webpack_require__(/*! ../utils */ "./src/utils.ts");
+const sat_1 = __webpack_require__(/*! sat */ "./node_modules/sat/SAT.js");
+/**
+ * collider - circle
+ */
+class Circle extends sat_1.Circle {
+ /**
+ * collider - circle
+ */
+ constructor(position, radius, options) {
+ super((0, utils_1.ensureVectorPoint)(position), radius);
+ /**
+ * offset copy without angle applied
+ */
+ this.offsetCopy = { x: 0, y: 0 };
+ /**
+ * was the polygon modified and needs update in the next checkCollision
+ */
+ this.dirty = false;
+ /*
+ * circles are convex
+ */
+ this.isConvex = true;
+ /**
+ * circle type
+ */
+ this.type = model_1.BodyType.Circle;
+ /**
+ * faster than type
+ */
+ this.typeGroup = model_1.BodyGroup.Circle;
+ /**
+ * always centered
+ */
+ this.isCentered = true;
+ (0, utils_1.extendBody)(this, options);
+ this.unscaledRadius = radius;
+ }
+ /**
+ * get this.pos.x
+ */
+ get x() {
+ return this.pos.x;
+ }
+ /**
+ * updating this.pos.x by this.x = x updates AABB
+ */
+ set x(x) {
+ this.pos.x = x;
+ this.markAsDirty();
+ }
+ /**
+ * get this.pos.y
+ */
+ get y() {
+ return this.pos.y;
+ }
+ /**
+ * updating this.pos.y by this.y = y updates AABB
+ */
+ set y(y) {
+ this.pos.y = y;
+ this.markAsDirty();
+ }
+ /**
+ * allow get scale
+ */
+ get scale() {
+ return this.r / this.unscaledRadius;
+ }
+ /**
+ * shorthand for setScale()
+ */
+ set scale(scale) {
+ this.setScale(scale);
+ }
+ /**
+ * scaleX = scale in case of Circles
+ */
+ get scaleX() {
+ return this.scale;
+ }
+ /**
+ * scaleY = scale in case of Circles
+ */
+ get scaleY() {
+ return this.scale;
+ }
+ /**
+ * group for collision filtering
+ */
+ get group() {
+ return this._group;
+ }
+ set group(group) {
+ this._group = (0, utils_1.getGroup)(group);
+ }
+ /**
+ * update position BY MOVING FORWARD IN ANGLE DIRECTION
+ */
+ move(speed = 1, updateNow = true) {
+ (0, utils_1.move)(this, speed, updateNow);
+ return this;
+ }
+ /**
+ * update position BY TELEPORTING
+ */
+ setPosition(x, y, updateNow = true) {
+ this.pos.x = x;
+ this.pos.y = y;
+ this.markAsDirty(updateNow);
+ return this;
+ }
+ /**
+ * update scale
+ */
+ setScale(scaleX, _scaleY = scaleX, updateNow = true) {
+ this.r = this.unscaledRadius * Math.abs(scaleX);
+ this.markAsDirty(updateNow);
+ return this;
+ }
+ /**
+ * set rotation
+ */
+ setAngle(angle, updateNow = true) {
+ this.angle = angle;
+ const { x, y } = this.getOffsetWithAngle();
+ this.offset.x = x;
+ this.offset.y = y;
+ this.markAsDirty(updateNow);
+ return this;
+ }
+ /**
+ * set offset from center
+ */
+ setOffset(offset, updateNow = true) {
+ this.offsetCopy.x = offset.x;
+ this.offsetCopy.y = offset.y;
+ const { x, y } = this.getOffsetWithAngle();
+ this.offset.x = x;
+ this.offset.y = y;
+ this.markAsDirty(updateNow);
+ return this;
+ }
+ /**
+ * get body bounding box, without padding
+ */
+ getAABBAsBBox() {
+ const x = this.pos.x + this.offset.x;
+ const y = this.pos.y + this.offset.y;
+ return {
+ minX: x - this.r,
+ maxX: x + this.r,
+ minY: y - this.r,
+ maxY: y + this.r
+ };
+ }
+ /**
+ * Draws collider on a CanvasRenderingContext2D's current path
+ */
+ draw(context) {
+ const x = this.pos.x + this.offset.x;
+ const y = this.pos.y + this.offset.y;
+ const r = Math.abs(this.r);
+ if (this.isTrigger) {
+ const max = Math.max(8, this.r);
+ for (let i = 0; i < max; i++) {
const arc = (i / max) * 2 * Math.PI;
const arcPrev = ((i - 1) / max) * 2 * Math.PI;
const fromX = x + Math.cos(arcPrev) * this.r;
@@ -3182,2676 +2548,2868 @@ which is good. See: http://baagoe.com/en/RandomMusings/hash/avalanche.xhtml
const toX = x + Math.cos(arc) * this.r;
const toY = y + Math.sin(arc) * this.r;
(0, utils_1.dashLineTo)(context, fromX, fromY, toX, toY);
- }
- } else {
- context.moveTo(x + r, y);
- context.arc(x, y, r, 0, Math.PI * 2);
- }
- }
- /**
- * Draws Bounding Box on canvas context
- */
- drawBVH(context) {
- (0, utils_1.drawBVH)(context, this);
- }
- /**
- * inner function for after position change update aabb in system
- */
- updateBody(update = this.dirty) {
- var _a;
- if (update) {
- (_a = this.system) === null || _a === void 0
- ? void 0
- : _a.insert(this);
- this.dirty = false;
- }
- }
- /**
- * update instantly or mark as dirty
- */
- markAsDirty(update = false) {
- if (update) {
- this.updateBody(true);
- } else {
- this.dirty = true;
}
- }
- /**
- * internal for getting offset with applied angle
- */
- getOffsetWithAngle() {
- if ((!this.offsetCopy.x && !this.offsetCopy.y) || !this.angle) {
- return this.offsetCopy;
- }
- const sin = Math.sin(this.angle);
- const cos = Math.cos(this.angle);
- const x = this.offsetCopy.x * cos - this.offsetCopy.y * sin;
- const y = this.offsetCopy.x * sin + this.offsetCopy.y * cos;
- return { x, y };
- }
}
- exports.Circle = Circle;
+ else {
+ context.moveTo(x + r, y);
+ context.arc(x, y, r, 0, Math.PI * 2);
+ }
+ }
+ /**
+ * Draws Bounding Box on canvas context
+ */
+ drawBVH(context) {
+ (0, utils_1.drawBVH)(context, this);
+ }
+ /**
+ * inner function for after position change update aabb in system
+ */
+ updateBody(updateNow = this.dirty) {
+ var _a;
+ if (updateNow) {
+ (_a = this.system) === null || _a === void 0 ? void 0 : _a.insert(this);
+ this.dirty = false;
+ }
+ }
+ /**
+ * update instantly or mark as dirty
+ */
+ markAsDirty(updateNow = false) {
+ if (updateNow) {
+ this.updateBody(true);
+ }
+ else {
+ this.dirty = true;
+ }
+ }
+ /**
+ * internal for getting offset with applied angle
+ */
+ getOffsetWithAngle() {
+ if ((!this.offsetCopy.x && !this.offsetCopy.y) || !this.angle) {
+ return this.offsetCopy;
+ }
+ const sin = Math.sin(this.angle);
+ const cos = Math.cos(this.angle);
+ const x = this.offsetCopy.x * cos - this.offsetCopy.y * sin;
+ const y = this.offsetCopy.x * sin + this.offsetCopy.y * cos;
+ return { x, y };
+ }
+}
+exports.Circle = Circle;
- /***/
- },
- /***/ "./src/bodies/ellipse.ts":
- /*!*******************************!*\
+/***/ }),
+
+/***/ "./src/bodies/ellipse.ts":
+/*!*******************************!*\
!*** ./src/bodies/ellipse.ts ***!
\*******************************/
- /***/ (__unused_webpack_module, exports, __webpack_require__) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Ellipse = void 0;
- const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
- const utils_1 = __webpack_require__(/*! ../utils */ "./src/utils.ts");
- const polygon_1 = __webpack_require__(
- /*! ./polygon */ "./src/bodies/polygon.ts",
- );
+/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.Ellipse = void 0;
+const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
+const polygon_1 = __webpack_require__(/*! ./polygon */ "./src/bodies/polygon.ts");
+const utils_1 = __webpack_require__(/*! ../utils */ "./src/utils.ts");
+/**
+ * collider - ellipse
+ */
+class Ellipse extends polygon_1.Polygon {
+ /**
+ * collider - ellipse
+ */
+ constructor(position, radiusX, radiusY = radiusX, step = (radiusX + radiusY) / Math.PI, options) {
+ super(position, (0, utils_1.createEllipse)(radiusX, radiusY, step), options);
/**
- * collider - ellipse
+ * ellipse type
*/
- class Ellipse extends polygon_1.Polygon {
- /**
- * collider - ellipse
- */
- constructor(
- position,
- radiusX,
- radiusY = radiusX,
- step = (radiusX + radiusY) / Math.PI,
- options,
- ) {
- super(
- position,
- (0, utils_1.createEllipse)(radiusX, radiusY, step),
- options,
- );
- /**
- * ellipse type
- */
- this.type = model_1.BodyType.Ellipse;
- /**
- * faster than type
- */
- this.typeGroup = model_1.BodyGroup.Ellipse;
- /**
- * ellipses are convex
- */
- this.isConvex = true;
- this._radiusX = radiusX;
- this._radiusY = radiusY;
- this._step = step;
- }
- /**
- * flag to set is body centered
- */
- set isCentered(_isCentered) {}
- /**
- * is body centered?
- */
- get isCentered() {
- return true;
- }
- /**
- * get ellipse step number
- */
- get step() {
- return this._step;
- }
- /**
- * set ellipse step number
- */
- set step(step) {
- this._step = step;
- this.setPoints(
- (0, utils_1.createEllipse)(
- this._radiusX,
- this._radiusY,
- this._step,
- ),
- );
- }
- /**
- * get ellipse radiusX
- */
- get radiusX() {
- return this._radiusX;
- }
- /**
- * set ellipse radiusX, update points
- */
- set radiusX(radiusX) {
- this._radiusX = radiusX;
- this.setPoints(
- (0, utils_1.createEllipse)(
- this._radiusX,
- this._radiusY,
- this._step,
- ),
- );
- }
- /**
- * get ellipse radiusY
- */
- get radiusY() {
- return this._radiusY;
- }
- /**
- * set ellipse radiusY, update points
- */
- set radiusY(radiusY) {
- this._radiusY = radiusY;
- this.setPoints(
- (0, utils_1.createEllipse)(
- this._radiusX,
- this._radiusY,
- this._step,
- ),
- );
- }
- /**
- * do not attempt to use Polygon.center()
- */
- center() {
- return;
- }
- /**
- * do not attempt to use Polygon.updateIsConvex()
- */
- updateIsConvex() {
- return;
- }
- }
- exports.Ellipse = Ellipse;
+ this.type = model_1.BodyType.Ellipse;
+ /**
+ * faster than type
+ */
+ this.typeGroup = model_1.BodyGroup.Ellipse;
+ /**
+ * ellipses are convex
+ */
+ this.isConvex = true;
+ this._radiusX = radiusX;
+ this._radiusY = radiusY;
+ this._step = step;
+ }
+ /**
+ * flag to set is body centered
+ */
+ set isCentered(_isCentered) { }
+ /**
+ * is body centered?
+ */
+ get isCentered() {
+ return true;
+ }
+ /**
+ * get ellipse step number
+ */
+ get step() {
+ return this._step;
+ }
+ /**
+ * set ellipse step number
+ */
+ set step(step) {
+ this._step = step;
+ this.setPoints((0, utils_1.createEllipse)(this._radiusX, this._radiusY, this._step));
+ }
+ /**
+ * get ellipse radiusX
+ */
+ get radiusX() {
+ return this._radiusX;
+ }
+ /**
+ * set ellipse radiusX, update points
+ */
+ set radiusX(radiusX) {
+ this._radiusX = radiusX;
+ this.setPoints((0, utils_1.createEllipse)(this._radiusX, this._radiusY, this._step));
+ }
+ /**
+ * get ellipse radiusY
+ */
+ get radiusY() {
+ return this._radiusY;
+ }
+ /**
+ * set ellipse radiusY, update points
+ */
+ set radiusY(radiusY) {
+ this._radiusY = radiusY;
+ this.setPoints((0, utils_1.createEllipse)(this._radiusX, this._radiusY, this._step));
+ }
+ /**
+ * do not attempt to use Polygon.center()
+ */
+ center() {
+ return;
+ }
+ /**
+ * do not attempt to use Polygon.updateIsConvex()
+ */
+ updateIsConvex() {
+ return;
+ }
+}
+exports.Ellipse = Ellipse;
- /***/
- },
- /***/ "./src/bodies/line.ts":
- /*!****************************!*\
+/***/ }),
+
+/***/ "./src/bodies/line.ts":
+/*!****************************!*\
!*** ./src/bodies/line.ts ***!
\****************************/
- /***/ (__unused_webpack_module, exports, __webpack_require__) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Line = void 0;
- const sat_1 = __webpack_require__(
- /*! sat */ "./node_modules/.pnpm/sat@0.9.0/node_modules/sat/SAT.js",
- );
- const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
- const polygon_1 = __webpack_require__(
- /*! ./polygon */ "./src/bodies/polygon.ts",
- );
+/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.Line = void 0;
+const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
+const polygon_1 = __webpack_require__(/*! ./polygon */ "./src/bodies/polygon.ts");
+const sat_1 = __webpack_require__(/*! sat */ "./node_modules/sat/SAT.js");
+/**
+ * collider - line
+ */
+class Line extends polygon_1.Polygon {
+ /**
+ * collider - line from start to end
+ */
+ constructor(start, end, options) {
+ super(start, [
+ { x: 0, y: 0 },
+ { x: end.x - start.x, y: end.y - start.y }
+ ], options);
/**
- * collider - line
+ * line type
*/
- class Line extends polygon_1.Polygon {
- /**
- * collider - line from start to end
- */
- constructor(start, end, options) {
- super(
- start,
- [
- { x: 0, y: 0 },
- { x: end.x - start.x, y: end.y - start.y },
- ],
- options,
- );
- /**
- * line type
- */
- this.type = model_1.BodyType.Line;
- /**
- * faster than type
- */
- this.typeGroup = model_1.BodyGroup.Line;
- /**
- * line is convex
- */
- this.isConvex = true;
- if (this.calcPoints.length === 1 || !end) {
- console.error({ start, end });
- throw new Error("No end point for line provided");
- }
- }
- get start() {
- return {
- x: this.x + this.calcPoints[0].x,
- y: this.y + this.calcPoints[0].y,
- };
- }
- set start({ x, y }) {
- this.x = x;
- this.y = y;
- }
- get end() {
- return {
- x: this.x + this.calcPoints[1].x,
- y: this.y + this.calcPoints[1].y,
- };
- }
- set end({ x, y }) {
- this.points[1].x = x - this.start.x;
- this.points[1].y = y - this.start.y;
- this.setPoints(this.points);
- }
- getCentroid() {
- return new sat_1.Vector(
- (this.end.x - this.start.x) / 2,
- (this.end.y - this.start.y) / 2,
- );
- }
- /**
- * do not attempt to use Polygon.updateIsConvex()
- */
- updateIsConvex() {
- return;
- }
+ this.type = model_1.BodyType.Line;
+ /**
+ * faster than type
+ */
+ this.typeGroup = model_1.BodyGroup.Line;
+ /**
+ * line is convex
+ */
+ this.isConvex = true;
+ if (this.calcPoints.length === 1 || !end) {
+ console.error({ start, end });
+ throw new Error("No end point for line provided");
}
- exports.Line = Line;
+ }
+ get start() {
+ return {
+ x: this.x + this.calcPoints[0].x,
+ y: this.y + this.calcPoints[0].y
+ };
+ }
+ set start({ x, y }) {
+ this.x = x;
+ this.y = y;
+ }
+ get end() {
+ return {
+ x: this.x + this.calcPoints[1].x,
+ y: this.y + this.calcPoints[1].y
+ };
+ }
+ set end({ x, y }) {
+ this.points[1].x = x - this.start.x;
+ this.points[1].y = y - this.start.y;
+ this.setPoints(this.points);
+ }
+ getCentroid() {
+ return new sat_1.Vector((this.end.x - this.start.x) / 2, (this.end.y - this.start.y) / 2);
+ }
+ /**
+ * do not attempt to use Polygon.updateIsConvex()
+ */
+ updateIsConvex() {
+ return;
+ }
+}
+exports.Line = Line;
- /***/
- },
- /***/ "./src/bodies/point.ts":
- /*!*****************************!*\
+/***/ }),
+
+/***/ "./src/bodies/point.ts":
+/*!*****************************!*\
!*** ./src/bodies/point.ts ***!
\*****************************/
- /***/ (__unused_webpack_module, exports, __webpack_require__) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Point = void 0;
- const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
- const utils_1 = __webpack_require__(/*! ../utils */ "./src/utils.ts");
- const box_1 = __webpack_require__(/*! ./box */ "./src/bodies/box.ts");
+/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.Point = void 0;
+const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
+const box_1 = __webpack_require__(/*! ./box */ "./src/bodies/box.ts");
+const utils_1 = __webpack_require__(/*! ../utils */ "./src/utils.ts");
+/**
+ * collider - point (very tiny box)
+ */
+class Point extends box_1.Box {
+ /**
+ * collider - point (very tiny box)
+ */
+ constructor(position, options) {
+ super((0, utils_1.ensureVectorPoint)(position), 0.001, 0.001, options);
/**
- * collider - point (very tiny box)
+ * point type
*/
- class Point extends box_1.Box {
- /**
- * collider - point (very tiny box)
- */
- constructor(position, options) {
- super(
- (0, utils_1.ensureVectorPoint)(position),
- 0.001,
- 0.001,
- options,
- );
- /**
- * point type
- */
- this.type = model_1.BodyType.Point;
- /**
- * faster than type
- */
- this.typeGroup = model_1.BodyGroup.Point;
- }
- }
- exports.Point = Point;
+ this.type = model_1.BodyType.Point;
+ /**
+ * faster than type
+ */
+ this.typeGroup = model_1.BodyGroup.Point;
+ }
+}
+exports.Point = Point;
- /***/
- },
- /***/ "./src/bodies/polygon.ts":
- /*!*******************************!*\
+/***/ }),
+
+/***/ "./src/bodies/polygon.ts":
+/*!*******************************!*\
!*** ./src/bodies/polygon.ts ***!
\*******************************/
- /***/ (__unused_webpack_module, exports, __webpack_require__) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Polygon = exports.isSimple = void 0;
- const poly_decomp_es_1 = __webpack_require__(
- /*! poly-decomp-es */ "./node_modules/.pnpm/poly-decomp-es@0.4.2/node_modules/poly-decomp-es/dist/poly-decomp-es.js",
- );
- Object.defineProperty(exports, "isSimple", {
- enumerable: true,
- get: function () {
- return poly_decomp_es_1.isSimple;
- },
- });
- const sat_1 = __webpack_require__(
- /*! sat */ "./node_modules/.pnpm/sat@0.9.0/node_modules/sat/SAT.js",
- );
- const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
- const optimized_1 = __webpack_require__(
- /*! ../optimized */ "./src/optimized.ts",
- );
- const utils_1 = __webpack_require__(/*! ../utils */ "./src/utils.ts");
+/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.Polygon = exports.isSimple = void 0;
+const model_1 = __webpack_require__(/*! ../model */ "./src/model.ts");
+const utils_1 = __webpack_require__(/*! ../utils */ "./src/utils.ts");
+const optimized_1 = __webpack_require__(/*! ../optimized */ "./src/optimized.ts");
+const poly_decomp_es_1 = __webpack_require__(/*! poly-decomp-es */ "./node_modules/poly-decomp-es/dist/poly-decomp-es.js");
+Object.defineProperty(exports, "isSimple", ({ enumerable: true, get: function () { return poly_decomp_es_1.isSimple; } }));
+const sat_1 = __webpack_require__(/*! sat */ "./node_modules/sat/SAT.js");
+/**
+ * collider - polygon
+ */
+class Polygon extends sat_1.Polygon {
+ /**
+ * collider - polygon
+ */
+ constructor(position, points, options) {
+ super((0, utils_1.ensureVectorPoint)(position), (0, utils_1.ensurePolygonPoints)(points));
/**
- * collider - polygon
+ * was the polygon modified and needs update in the next checkCollision
*/
- class Polygon extends sat_1.Polygon {
- /**
- * collider - polygon
- */
- constructor(position, points, options) {
- super(
- (0, utils_1.ensureVectorPoint)(position),
- (0, utils_1.ensurePolygonPoints)(points),
- );
- /**
- * was the polygon modified and needs update in the next checkCollision
- */
- this.dirty = false;
- /**
- * type of body
- */
- this.type = model_1.BodyType.Polygon;
- /**
- * faster than type
- */
- this.typeGroup = model_1.BodyGroup.Polygon;
- /**
- * is body centered
- */
- this.centered = false;
- /**
- * scale Vector of body
- */
- this.scaleVector = { x: 1, y: 1 };
- if (!points.length) {
- throw new Error("No points in polygon");
- }
- (0, utils_1.extendBody)(this, options);
- }
- /**
- * flag to set is polygon centered
- */
- set isCentered(isCentered) {
- if (this.centered === isCentered) {
- return;
- }
- const centroid = this.getCentroidWithoutRotation();
- if (centroid.x || centroid.y) {
- const x = centroid.x * (isCentered ? 1 : -1);
- const y = centroid.y * (isCentered ? 1 : -1);
- this.translate(-x, -y);
- }
- this.centered = isCentered;
- }
- /**
- * is polygon centered?
- */
- get isCentered() {
- return this.centered;
- }
- get x() {
- return this.pos.x;
- }
- /**
- * updating this.pos.x by this.x = x updates AABB
- */
- set x(x) {
- this.pos.x = x;
- this.markAsDirty();
- }
- get y() {
- return this.pos.y;
- }
- /**
- * updating this.pos.y by this.y = y updates AABB
- */
- set y(y) {
- this.pos.y = y;
- this.markAsDirty();
- }
- /**
- * allow exact getting of scale x - use setScale(x, y) to set
- */
- get scaleX() {
- return this.scaleVector.x;
- }
- /**
- * allow exact getting of scale y - use setScale(x, y) to set
- */
- get scaleY() {
- return this.scaleVector.y;
- }
- /**
- * allow approx getting of scale
- */
- get scale() {
- return (this.scaleVector.x + this.scaleVector.y) / 2;
- }
- /**
- * allow easier setting of scale
- */
- set scale(scale) {
- this.setScale(scale);
- }
- /**
- * group for collision filtering
- */
- get group() {
- return this._group;
- }
- set group(group) {
- this._group = (0, utils_1.getGroup)(group);
- }
- /**
- * update position
- */
- setPosition(x, y, update = true) {
- this.pos.x = x;
- this.pos.y = y;
- this.markAsDirty(update);
- return this;
- }
- /**
- * update scale
- */
- setScale(x, y = x, update = true) {
- this.scaleVector.x = Math.abs(x);
- this.scaleVector.y = Math.abs(y);
- super.setPoints(
- (0, optimized_1.map)(this.points, (point, index) => {
- point.x = this.pointsBackup[index].x * this.scaleVector.x;
- point.y = this.pointsBackup[index].y * this.scaleVector.y;
- return point;
- }),
- );
- this.markAsDirty(update);
- return this;
- }
- setAngle(angle, update = true) {
- super.setAngle(angle);
- this.markAsDirty(update);
- return this;
- }
- setOffset(offset, update = true) {
- super.setOffset(offset);
- this.markAsDirty(update);
- return this;
- }
- /**
- * get body bounding box, without padding
- */
- getAABBAsBBox() {
- const { pos, w, h } = this.getAABBAsBox();
- return {
- minX: pos.x,
- minY: pos.y,
- maxX: pos.x + w,
- maxY: pos.y + h,
- };
- }
- /**
- * Draws exact collider on canvas context
- */
- draw(context) {
- (0, utils_1.drawPolygon)(context, this, this.isTrigger);
- }
- /**
- * Draws Bounding Box on canvas context
- */
- drawBVH(context) {
- (0, utils_1.drawBVH)(context, this);
- }
- /**
- * get body centroid without applied angle
- */
- getCentroidWithoutRotation() {
- // keep angle copy
- const angle = this.angle;
- if (angle) {
- // reset angle for get centroid
- this.setAngle(0);
- // get centroid
- const centroid = this.getCentroid();
- // revert angle change
- this.setAngle(angle);
- return centroid;
- }
- return this.getCentroid();
- }
- /**
- * sets polygon points to new array of vectors
- */
- setPoints(points) {
- super.setPoints(points);
- this.updateIsConvex();
- this.pointsBackup = (0, utils_1.clonePointsArray)(points);
- return this;
- }
- /**
- * translates polygon points in x, y direction
- */
- translate(x, y) {
- super.translate(x, y);
- this.pointsBackup = (0, utils_1.clonePointsArray)(this.points);
- return this;
- }
- /**
- * rotates polygon points by angle, in radians
- */
- rotate(angle) {
- super.rotate(angle);
- this.pointsBackup = (0, utils_1.clonePointsArray)(this.points);
- return this;
- }
- /**
- * if true, polygon is not an invalid, self-crossing polygon
- */
- isSimple() {
- return (0, poly_decomp_es_1.isSimple)(
- this.calcPoints.map(utils_1.mapVectorToArray),
- );
- }
- /**
- * inner function for after position change update aabb in system and convex inner polygons
- */
- updateBody(update = this.dirty) {
- var _a;
- if (update) {
- this.updateConvexPolygonPositions();
- (_a = this.system) === null || _a === void 0
- ? void 0
- : _a.insert(this);
- this.dirty = false;
- }
- }
- retranslate(isCentered = this.isCentered) {
- const centroid = this.getCentroidWithoutRotation();
- if (centroid.x || centroid.y) {
- const x = centroid.x * (isCentered ? 1 : -1);
- const y = centroid.y * (isCentered ? 1 : -1);
- this.translate(-x, -y);
- }
- }
- /**
- * update instantly or mark as dirty
- */
- markAsDirty(update = false) {
- if (update) {
- this.updateBody(true);
- } else {
- this.dirty = true;
- }
- }
- /**
- * update the position of the decomposed convex polygons (if any), called
- * after the position of the body has changed
- */
- updateConvexPolygonPositions() {
- if (this.isConvex || !this.convexPolygons) {
- return;
- }
- (0, optimized_1.forEach)(this.convexPolygons, (polygon) => {
- polygon.pos.x = this.pos.x;
- polygon.pos.y = this.pos.y;
- if (polygon.angle !== this.angle) {
- // Must use setAngle to recalculate the points of the Polygon
- polygon.setAngle(this.angle);
- }
- });
- }
- /**
- * returns body split into convex polygons, or empty array for convex bodies
- */
- getConvex() {
- if (
- (this.typeGroup &&
- this.typeGroup !== model_1.BodyGroup.Polygon) ||
- this.points.length < 4
- ) {
- return [];
- }
- const points = (0, optimized_1.map)(
- this.calcPoints,
- utils_1.mapVectorToArray,
- );
- return (0, poly_decomp_es_1.quickDecomp)(points);
- }
- /**
- * updates convex polygons cache in body
- */
- updateConvexPolygons(convex = this.getConvex()) {
- if (this.isConvex) {
- return;
- }
- if (!this.convexPolygons) {
- this.convexPolygons = [];
- }
- (0, optimized_1.forEach)(convex, (points, index) => {
- // lazy create
- if (!this.convexPolygons[index]) {
- this.convexPolygons[index] = new sat_1.Polygon();
- }
- this.convexPolygons[index].pos.x = this.pos.x;
- this.convexPolygons[index].pos.y = this.pos.y;
- this.convexPolygons[index].angle = this.angle;
- this.convexPolygons[index].setPoints(
- (0, utils_1.ensurePolygonPoints)(
- (0, optimized_1.map)(points, utils_1.mapArrayToVector),
- ),
- );
- });
- // trim array length
- this.convexPolygons.length = convex.length;
- }
- /**
- * after points update set is convex
- */
- updateIsConvex() {
- // all other types other than polygon are always convex
- const convex = this.getConvex();
- // everything with empty array or one element array
- this.isConvex = convex.length <= 1;
- this.updateConvexPolygons(convex);
- }
- }
- exports.Polygon = Polygon;
-
- /***/
- },
-
- /***/ "./src/intersect.ts":
- /*!**************************!*\
- !*** ./src/intersect.ts ***!
- \**************************/
- /***/ (__unused_webpack_module, exports, __webpack_require__) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.intersectLinePolygon =
- exports.intersectLineLine =
- exports.intersectLineLineFast =
- exports.intersectLineCircle =
- exports.circleOutsidePolygon =
- exports.circleInPolygon =
- exports.circleInCircle =
- exports.pointOnCircle =
- exports.polygonInPolygon =
- exports.pointInPolygon =
- exports.polygonInCircle =
- exports.ensureConvex =
- void 0;
- const sat_1 = __webpack_require__(
- /*! sat */ "./node_modules/.pnpm/sat@0.9.0/node_modules/sat/SAT.js",
- );
- const model_1 = __webpack_require__(/*! ./model */ "./src/model.ts");
- const optimized_1 = __webpack_require__(
- /*! ./optimized */ "./src/optimized.ts",
- );
+ this.dirty = false;
/**
- * replace body with array of related convex polygons
+ * type of body
*/
- function ensureConvex(body) {
- if (body.isConvex || body.typeGroup !== model_1.BodyGroup.Polygon) {
- return [body];
- }
- return body.convexPolygons;
- }
- exports.ensureConvex = ensureConvex;
- function polygonInCircle(polygon, circle) {
- return (0, optimized_1.every)(polygon.calcPoints, (p) =>
- (0, sat_1.pointInCircle)(
- { x: p.x + polygon.pos.x, y: p.y + polygon.pos.y },
- circle,
- ),
- );
- }
- exports.polygonInCircle = polygonInCircle;
- function pointInPolygon(point, polygon) {
- return (0, optimized_1.some)(ensureConvex(polygon), (convex) =>
- (0, sat_1.pointInPolygon)(point, convex),
- );
- }
- exports.pointInPolygon = pointInPolygon;
- function polygonInPolygon(polygonA, polygonB) {
- return (0, optimized_1.every)(polygonA.calcPoints, (point) =>
- pointInPolygon(
- { x: point.x + polygonA.pos.x, y: point.y + polygonA.pos.y },
- polygonB,
- ),
- );
- }
- exports.polygonInPolygon = polygonInPolygon;
+ this.type = model_1.BodyType.Polygon;
/**
- * https://stackoverflow.com/a/68197894/1749528
+ * faster than type
*/
- function pointOnCircle(point, circle) {
- return (
- (point.x - circle.pos.x) * (point.x - circle.pos.x) +
- (point.y - circle.pos.y) * (point.y - circle.pos.y) ===
- circle.r * circle.r
- );
- }
- exports.pointOnCircle = pointOnCircle;
+ this.typeGroup = model_1.BodyGroup.Polygon;
/**
- * https://stackoverflow.com/a/68197894/1749528
+ * is body centered
*/
- function circleInCircle(bodyA, bodyB) {
- const x1 = bodyA.pos.x;
- const y1 = bodyA.pos.y;
- const x2 = bodyB.pos.x;
- const y2 = bodyB.pos.y;
- const r1 = bodyA.r;
- const r2 = bodyB.r;
- const distSq = Math.sqrt(
- (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2),
- );
- return distSq + r2 === r1 || distSq + r2 < r1;
- }
- exports.circleInCircle = circleInCircle;
+ this.centered = false;
/**
- * https://stackoverflow.com/a/68197894/1749528
+ * scale Vector of body
*/
- function circleInPolygon(circle, polygon) {
- // Circle with radius 0 isn't a circle
- if (circle.r === 0) {
- return false;
- }
- // If the center of the circle is not within the polygon,
- // then the circle may overlap, but it'll never be "contained"
- // so return false
- if (!pointInPolygon(circle.pos, polygon)) {
- return false;
- }
- // Necessary add polygon pos to points
- const points = (0, optimized_1.map)(
- polygon.calcPoints,
- ({ x, y }) => ({
- x: x + polygon.pos.x,
- y: y + polygon.pos.y,
- }),
- );
- // If the center of the circle is within the polygon,
- // the circle is not outside of the polygon completely.
- // so return false.
- if (
- (0, optimized_1.some)(points, (point) =>
- (0, sat_1.pointInCircle)(point, circle),
- )
- ) {
- return false;
- }
- // If any line-segment of the polygon intersects the circle,
- // the circle is not "contained"
- // so return false
- if (
- (0, optimized_1.some)(points, (end, index) => {
- const start = index
- ? points[index - 1]
- : points[points.length - 1];
- return intersectLineCircle({ start, end }, circle).length > 0;
- })
- ) {
- return false;
- }
- return true;
+ this.scaleVector = { x: 1, y: 1 };
+ if (!points.length) {
+ throw new Error("No points in polygon");
}
- exports.circleInPolygon = circleInPolygon;
- /**
- * https://stackoverflow.com/a/68197894/1749528
- */
- function circleOutsidePolygon(circle, polygon) {
- // Circle with radius 0 isn't a circle
- if (circle.r === 0) {
- return false;
- }
- // If the center of the circle is within the polygon,
- // the circle is not outside of the polygon completely.
- // so return false.
- if (pointInPolygon(circle.pos, polygon)) {
- return false;
- }
- // Necessary add polygon pos to points
- const points = (0, optimized_1.map)(
- polygon.calcPoints,
- ({ x, y }) => ({
- x: x + polygon.pos.x,
- y: y + polygon.pos.y,
- }),
- );
- // If the center of the circle is within the polygon,
- // the circle is not outside of the polygon completely.
- // so return false.
- if (
- (0, optimized_1.some)(
- points,
- (point) =>
- (0, sat_1.pointInCircle)(point, circle) ||
- pointOnCircle(point, circle),
- )
- ) {
- return false;
- }
- // If any line-segment of the polygon intersects the circle,
- // the circle is not "contained"
- // so return false
- if (
- (0, optimized_1.some)(points, (end, index) => {
- const start = index
- ? points[index - 1]
- : points[points.length - 1];
- return intersectLineCircle({ start, end }, circle).length > 0;
- })
- ) {
- return false;
- }
- return true;
+ (0, utils_1.extendBody)(this, options);
+ }
+ /**
+ * flag to set is polygon centered
+ */
+ set isCentered(isCentered) {
+ if (this.centered === isCentered) {
+ return;
}
- exports.circleOutsidePolygon = circleOutsidePolygon;
- /**
- * https://stackoverflow.com/a/37225895/1749528
- */
- function intersectLineCircle(line, { pos, r }) {
- const v1 = {
- x: line.end.x - line.start.x,
- y: line.end.y - line.start.y,
- };
- const v2 = { x: line.start.x - pos.x, y: line.start.y - pos.y };
- const b = (v1.x * v2.x + v1.y * v2.y) * -2;
- const c = (v1.x * v1.x + v1.y * v1.y) * 2;
- const d = Math.sqrt(
- b * b - (v2.x * v2.x + v2.y * v2.y - r * r) * c * 2,
- );
- if (isNaN(d)) {
- // no intercept
- return [];
- }
- const u1 = (b - d) / c; // these represent the unit distance of point one and two on the line
- const u2 = (b + d) / c;
- const results = []; // return array
- if (u1 <= 1 && u1 >= 0) {
- // add point if on the line segment
- results.push({
- x: line.start.x + v1.x * u1,
- y: line.start.y + v1.y * u1,
- });
- }
- if (u2 <= 1 && u2 >= 0) {
- // second add point if on the line segment
- results.push({
- x: line.start.x + v1.x * u2,
- y: line.start.y + v1.y * u2,
- });
- }
- return results;
+ const centroid = this.getCentroidWithoutRotation();
+ if (centroid.x || centroid.y) {
+ const x = centroid.x * (isCentered ? 1 : -1);
+ const y = centroid.y * (isCentered ? 1 : -1);
+ this.translate(-x, -y);
}
- exports.intersectLineCircle = intersectLineCircle;
- /**
- * helper for intersectLineLineFast
- */
- function isTurn(point1, point2, point3) {
- const A = (point3.x - point1.x) * (point2.y - point1.y);
- const B = (point2.x - point1.x) * (point3.y - point1.y);
- return A > B + Number.EPSILON ? 1 : A + Number.EPSILON < B ? -1 : 0;
+ this.centered = isCentered;
+ }
+ /**
+ * is polygon centered?
+ */
+ get isCentered() {
+ return this.centered;
+ }
+ get x() {
+ return this.pos.x;
+ }
+ /**
+ * updating this.pos.x by this.x = x updates AABB
+ */
+ set x(x) {
+ this.pos.x = x;
+ this.markAsDirty();
+ }
+ get y() {
+ return this.pos.y;
+ }
+ /**
+ * updating this.pos.y by this.y = y updates AABB
+ */
+ set y(y) {
+ this.pos.y = y;
+ this.markAsDirty();
+ }
+ /**
+ * allow exact getting of scale x - use setScale(x, y) to set
+ */
+ get scaleX() {
+ return this.scaleVector.x;
+ }
+ /**
+ * allow exact getting of scale y - use setScale(x, y) to set
+ */
+ get scaleY() {
+ return this.scaleVector.y;
+ }
+ /**
+ * allow approx getting of scale
+ */
+ get scale() {
+ return (this.scaleVector.x + this.scaleVector.y) / 2;
+ }
+ /**
+ * allow easier setting of scale
+ */
+ set scale(scale) {
+ this.setScale(scale);
+ }
+ /**
+ * group for collision filtering
+ */
+ get group() {
+ return this._group;
+ }
+ set group(group) {
+ this._group = (0, utils_1.getGroup)(group);
+ }
+ /**
+ * update position BY MOVING FORWARD IN ANGLE DIRECTION
+ */
+ move(speed = 1, updateNow = true) {
+ (0, utils_1.move)(this, speed, updateNow);
+ return this;
+ }
+ /**
+ * update position BY TELEPORTING
+ */
+ setPosition(x, y, updateNow = true) {
+ this.pos.x = x;
+ this.pos.y = y;
+ this.markAsDirty(updateNow);
+ return this;
+ }
+ /**
+ * update scale
+ */
+ setScale(x, y = x, updateNow = true) {
+ this.scaleVector.x = Math.abs(x);
+ this.scaleVector.y = Math.abs(y);
+ super.setPoints((0, optimized_1.map)(this.points, (point, index) => {
+ point.x = this.pointsBackup[index].x * this.scaleVector.x;
+ point.y = this.pointsBackup[index].y * this.scaleVector.y;
+ return point;
+ }));
+ this.markAsDirty(updateNow);
+ return this;
+ }
+ setAngle(angle, updateNow = true) {
+ super.setAngle(angle);
+ this.markAsDirty(updateNow);
+ return this;
+ }
+ setOffset(offset, updateNow = true) {
+ super.setOffset(offset);
+ this.markAsDirty(updateNow);
+ return this;
+ }
+ /**
+ * get body bounding box, without padding
+ */
+ getAABBAsBBox() {
+ const { pos, w, h } = this.getAABBAsBox();
+ return {
+ minX: pos.x,
+ minY: pos.y,
+ maxX: pos.x + w,
+ maxY: pos.y + h
+ };
+ }
+ /**
+ * Draws exact collider on canvas context
+ */
+ draw(context) {
+ (0, utils_1.drawPolygon)(context, this, this.isTrigger);
+ }
+ /**
+ * Draws Bounding Box on canvas context
+ */
+ drawBVH(context) {
+ (0, utils_1.drawBVH)(context, this);
+ }
+ /**
+ * get body centroid without applied angle
+ */
+ getCentroidWithoutRotation() {
+ // keep angle copy
+ const angle = this.angle;
+ if (angle) {
+ // reset angle for get centroid
+ this.setAngle(0);
+ // get centroid
+ const centroid = this.getCentroid();
+ // revert angle change
+ this.setAngle(angle);
+ return centroid;
}
- /**
- * faster implementation of intersectLineLine
- * https://stackoverflow.com/a/16725715/1749528
- */
- function intersectLineLineFast(line1, line2) {
- return (
- isTurn(line1.start, line2.start, line2.end) !==
- isTurn(line1.end, line2.start, line2.end) &&
- isTurn(line1.start, line1.end, line2.start) !==
- isTurn(line1.start, line1.end, line2.end)
- );
+ return this.getCentroid();
+ }
+ /**
+ * sets polygon points to new array of vectors
+ */
+ setPoints(points) {
+ super.setPoints(points);
+ this.updateIsConvex();
+ this.pointsBackup = (0, utils_1.clonePointsArray)(points);
+ return this;
+ }
+ /**
+ * translates polygon points in x, y direction
+ */
+ translate(x, y) {
+ super.translate(x, y);
+ this.pointsBackup = (0, utils_1.clonePointsArray)(this.points);
+ return this;
+ }
+ /**
+ * rotates polygon points by angle, in radians
+ */
+ rotate(angle) {
+ super.rotate(angle);
+ this.pointsBackup = (0, utils_1.clonePointsArray)(this.points);
+ return this;
+ }
+ /**
+ * if true, polygon is not an invalid, self-crossing polygon
+ */
+ isSimple() {
+ return (0, poly_decomp_es_1.isSimple)(this.calcPoints.map(utils_1.mapVectorToArray));
+ }
+ /**
+ * inner function for after position change update aabb in system and convex inner polygons
+ */
+ updateBody(updateNow = this.dirty) {
+ var _a;
+ if (updateNow) {
+ this.updateConvexPolygonPositions();
+ (_a = this.system) === null || _a === void 0 ? void 0 : _a.insert(this);
+ this.dirty = false;
}
- exports.intersectLineLineFast = intersectLineLineFast;
- /**
- * returns the point of intersection
- * https://stackoverflow.com/a/24392281/1749528
- */
- function intersectLineLine(line1, line2) {
- const dX = line1.end.x - line1.start.x;
- const dY = line1.end.y - line1.start.y;
- const determinant =
- dX * (line2.end.y - line2.start.y) -
- (line2.end.x - line2.start.x) * dY;
- if (determinant === 0) {
- return null;
- }
- const lambda =
- ((line2.end.y - line2.start.y) * (line2.end.x - line1.start.x) +
- (line2.start.x - line2.end.x) * (line2.end.y - line1.start.y)) /
- determinant;
- const gamma =
- ((line1.start.y - line1.end.y) * (line2.end.x - line1.start.x) +
- dX * (line2.end.y - line1.start.y)) /
- determinant;
- // check if there is an intersection
- if (!(lambda >= 0 && lambda <= 1) || !(gamma >= 0 && gamma <= 1)) {
- return null;
- }
- return {
- x: line1.start.x + lambda * dX,
- y: line1.start.y + lambda * dY,
- };
+ }
+ retranslate(isCentered = this.isCentered) {
+ const centroid = this.getCentroidWithoutRotation();
+ if (centroid.x || centroid.y) {
+ const x = centroid.x * (isCentered ? 1 : -1);
+ const y = centroid.y * (isCentered ? 1 : -1);
+ this.translate(-x, -y);
+ }
+ }
+ /**
+ * update instantly or mark as dirty
+ */
+ markAsDirty(updateNow = false) {
+ if (updateNow) {
+ this.updateBody(true);
+ }
+ else {
+ this.dirty = true;
+ }
+ }
+ /**
+ * update the position of the decomposed convex polygons (if any), called
+ * after the position of the body has changed
+ */
+ updateConvexPolygonPositions() {
+ if (this.isConvex || !this.convexPolygons) {
+ return;
+ }
+ (0, optimized_1.forEach)(this.convexPolygons, (polygon) => {
+ polygon.pos.x = this.pos.x;
+ polygon.pos.y = this.pos.y;
+ if (polygon.angle !== this.angle) {
+ // Must use setAngle to recalculate the points of the Polygon
+ polygon.setAngle(this.angle);
+ }
+ });
+ }
+ /**
+ * returns body split into convex polygons, or empty array for convex bodies
+ */
+ getConvex() {
+ if ((this.typeGroup && this.typeGroup !== model_1.BodyGroup.Polygon) ||
+ this.points.length < 4) {
+ return [];
+ }
+ const points = (0, optimized_1.map)(this.calcPoints, utils_1.mapVectorToArray);
+ return (0, poly_decomp_es_1.quickDecomp)(points);
+ }
+ /**
+ * updates convex polygons cache in body
+ */
+ updateConvexPolygons(convex = this.getConvex()) {
+ if (this.isConvex) {
+ return;
}
- exports.intersectLineLine = intersectLineLine;
- function intersectLinePolygon(line, polygon) {
- const results = [];
- (0, optimized_1.forEach)(polygon.calcPoints, (to, index) => {
- const from = index
- ? polygon.calcPoints[index - 1]
- : polygon.calcPoints[polygon.calcPoints.length - 1];
- const side = {
- start: { x: from.x + polygon.pos.x, y: from.y + polygon.pos.y },
- end: { x: to.x + polygon.pos.x, y: to.y + polygon.pos.y },
- };
- const hit = intersectLineLine(line, side);
- if (hit) {
- results.push(hit);
+ if (!this.convexPolygons) {
+ this.convexPolygons = [];
+ }
+ (0, optimized_1.forEach)(convex, (points, index) => {
+ // lazy create
+ if (!this.convexPolygons[index]) {
+ this.convexPolygons[index] = new sat_1.Polygon();
}
- });
- return results;
+ this.convexPolygons[index].pos.x = this.pos.x;
+ this.convexPolygons[index].pos.y = this.pos.y;
+ this.convexPolygons[index].angle = this.angle;
+ this.convexPolygons[index].setPoints((0, utils_1.ensurePolygonPoints)((0, optimized_1.map)(points, utils_1.mapArrayToVector)));
+ });
+ // trim array length
+ this.convexPolygons.length = convex.length;
+ }
+ /**
+ * after points update set is convex
+ */
+ updateIsConvex() {
+ // all other types other than polygon are always convex
+ const convex = this.getConvex();
+ // everything with empty array or one element array
+ this.isConvex = convex.length <= 1;
+ this.updateConvexPolygons(convex);
+ }
+}
+exports.Polygon = Polygon;
+
+
+/***/ }),
+
+/***/ "./src/intersect.ts":
+/*!**************************!*\
+ !*** ./src/intersect.ts ***!
+ \**************************/
+/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.ensureConvex = ensureConvex;
+exports.polygonInCircle = polygonInCircle;
+exports.pointInPolygon = pointInPolygon;
+exports.polygonInPolygon = polygonInPolygon;
+exports.pointOnCircle = pointOnCircle;
+exports.circleInCircle = circleInCircle;
+exports.circleInPolygon = circleInPolygon;
+exports.circleOutsidePolygon = circleOutsidePolygon;
+exports.intersectLineCircle = intersectLineCircle;
+exports.intersectLineLineFast = intersectLineLineFast;
+exports.intersectLineLine = intersectLineLine;
+exports.intersectLinePolygon = intersectLinePolygon;
+const model_1 = __webpack_require__(/*! ./model */ "./src/model.ts");
+const optimized_1 = __webpack_require__(/*! ./optimized */ "./src/optimized.ts");
+const sat_1 = __webpack_require__(/*! sat */ "./node_modules/sat/SAT.js");
+/**
+ * replace body with array of related convex polygons
+ */
+function ensureConvex(body) {
+ if (body.isConvex || body.typeGroup !== model_1.BodyGroup.Polygon) {
+ return [body];
+ }
+ return body.convexPolygons;
+}
+function polygonInCircle(polygon, circle) {
+ return (0, optimized_1.every)(polygon.calcPoints, p => (0, sat_1.pointInCircle)({ x: p.x + polygon.pos.x, y: p.y + polygon.pos.y }, circle));
+}
+function pointInPolygon(point, polygon) {
+ return (0, optimized_1.some)(ensureConvex(polygon), convex => (0, sat_1.pointInPolygon)(point, convex));
+}
+function polygonInPolygon(polygonA, polygonB) {
+ return (0, optimized_1.every)(polygonA.calcPoints, point => pointInPolygon({ x: point.x + polygonA.pos.x, y: point.y + polygonA.pos.y }, polygonB));
+}
+/**
+ * https://stackoverflow.com/a/68197894/1749528
+ */
+function pointOnCircle(point, circle) {
+ return ((point.x - circle.pos.x) * (point.x - circle.pos.x) +
+ (point.y - circle.pos.y) * (point.y - circle.pos.y) ===
+ circle.r * circle.r);
+}
+/**
+ * https://stackoverflow.com/a/68197894/1749528
+ */
+function circleInCircle(bodyA, bodyB) {
+ const x1 = bodyA.pos.x;
+ const y1 = bodyA.pos.y;
+ const x2 = bodyB.pos.x;
+ const y2 = bodyB.pos.y;
+ const r1 = bodyA.r;
+ const r2 = bodyB.r;
+ const distSq = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
+ return distSq + r2 === r1 || distSq + r2 < r1;
+}
+/**
+ * https://stackoverflow.com/a/68197894/1749528
+ */
+function circleInPolygon(circle, polygon) {
+ // Circle with radius 0 isn't a circle
+ if (circle.r === 0) {
+ return false;
+ }
+ // If the center of the circle is not within the polygon,
+ // then the circle may overlap, but it'll never be "contained"
+ // so return false
+ if (!pointInPolygon(circle.pos, polygon)) {
+ return false;
+ }
+ // Necessary add polygon pos to points
+ const points = (0, optimized_1.map)(polygon.calcPoints, ({ x, y }) => ({
+ x: x + polygon.pos.x,
+ y: y + polygon.pos.y
+ }));
+ // If the center of the circle is within the polygon,
+ // the circle is not outside of the polygon completely.
+ // so return false.
+ if ((0, optimized_1.some)(points, point => (0, sat_1.pointInCircle)(point, circle))) {
+ return false;
+ }
+ // If any line-segment of the polygon intersects the circle,
+ // the circle is not "contained"
+ // so return false
+ if ((0, optimized_1.some)(points, (end, index) => {
+ const start = index
+ ? points[index - 1]
+ : points[points.length - 1];
+ return intersectLineCircle({ start, end }, circle).length > 0;
+ })) {
+ return false;
+ }
+ return true;
+}
+/**
+ * https://stackoverflow.com/a/68197894/1749528
+ */
+function circleOutsidePolygon(circle, polygon) {
+ // Circle with radius 0 isn't a circle
+ if (circle.r === 0) {
+ return false;
+ }
+ // If the center of the circle is within the polygon,
+ // the circle is not outside of the polygon completely.
+ // so return false.
+ if (pointInPolygon(circle.pos, polygon)) {
+ return false;
+ }
+ // Necessary add polygon pos to points
+ const points = (0, optimized_1.map)(polygon.calcPoints, ({ x, y }) => ({
+ x: x + polygon.pos.x,
+ y: y + polygon.pos.y
+ }));
+ // If the center of the circle is within the polygon,
+ // the circle is not outside of the polygon completely.
+ // so return false.
+ if ((0, optimized_1.some)(points, point => (0, sat_1.pointInCircle)(point, circle) || pointOnCircle(point, circle))) {
+ return false;
+ }
+ // If any line-segment of the polygon intersects the circle,
+ // the circle is not "contained"
+ // so return false
+ if ((0, optimized_1.some)(points, (end, index) => {
+ const start = index
+ ? points[index - 1]
+ : points[points.length - 1];
+ return intersectLineCircle({ start, end }, circle).length > 0;
+ })) {
+ return false;
+ }
+ return true;
+}
+/**
+ * https://stackoverflow.com/a/37225895/1749528
+ */
+function intersectLineCircle(line, { pos, r }) {
+ const v1 = { x: line.end.x - line.start.x, y: line.end.y - line.start.y };
+ const v2 = { x: line.start.x - pos.x, y: line.start.y - pos.y };
+ const b = (v1.x * v2.x + v1.y * v2.y) * -2;
+ const c = (v1.x * v1.x + v1.y * v1.y) * 2;
+ const d = Math.sqrt(b * b - (v2.x * v2.x + v2.y * v2.y - r * r) * c * 2);
+ if (isNaN(d)) {
+ // no intercept
+ return [];
+ }
+ const u1 = (b - d) / c; // these represent the unit distance of point one and two on the line
+ const u2 = (b + d) / c;
+ const results = []; // return array
+ if (u1 <= 1 && u1 >= 0) {
+ // add point if on the line segment
+ results.push({ x: line.start.x + v1.x * u1, y: line.start.y + v1.y * u1 });
+ }
+ if (u2 <= 1 && u2 >= 0) {
+ // second add point if on the line segment
+ results.push({ x: line.start.x + v1.x * u2, y: line.start.y + v1.y * u2 });
+ }
+ return results;
+}
+/**
+ * helper for intersectLineLineFast
+ */
+function isTurn(point1, point2, point3) {
+ const A = (point3.x - point1.x) * (point2.y - point1.y);
+ const B = (point2.x - point1.x) * (point3.y - point1.y);
+ return A > B + Number.EPSILON ? 1 : A + Number.EPSILON < B ? -1 : 0;
+}
+/**
+ * faster implementation of intersectLineLine
+ * https://stackoverflow.com/a/16725715/1749528
+ */
+function intersectLineLineFast(line1, line2) {
+ return (isTurn(line1.start, line2.start, line2.end) !==
+ isTurn(line1.end, line2.start, line2.end) &&
+ isTurn(line1.start, line1.end, line2.start) !==
+ isTurn(line1.start, line1.end, line2.end));
+}
+/**
+ * returns the point of intersection
+ * https://stackoverflow.com/a/24392281/1749528
+ */
+function intersectLineLine(line1, line2) {
+ const dX = line1.end.x - line1.start.x;
+ const dY = line1.end.y - line1.start.y;
+ const determinant = dX * (line2.end.y - line2.start.y) - (line2.end.x - line2.start.x) * dY;
+ if (determinant === 0) {
+ return null;
+ }
+ const lambda = ((line2.end.y - line2.start.y) * (line2.end.x - line1.start.x) +
+ (line2.start.x - line2.end.x) * (line2.end.y - line1.start.y)) /
+ determinant;
+ const gamma = ((line1.start.y - line1.end.y) * (line2.end.x - line1.start.x) +
+ dX * (line2.end.y - line1.start.y)) /
+ determinant;
+ // check if there is an intersection
+ if (!(lambda >= 0 && lambda <= 1) || !(gamma >= 0 && gamma <= 1)) {
+ return null;
+ }
+ return { x: line1.start.x + lambda * dX, y: line1.start.y + lambda * dY };
+}
+function intersectLinePolygon(line, polygon) {
+ const results = [];
+ (0, optimized_1.forEach)(polygon.calcPoints, (to, index) => {
+ const from = index
+ ? polygon.calcPoints[index - 1]
+ : polygon.calcPoints[polygon.calcPoints.length - 1];
+ const side = {
+ start: { x: from.x + polygon.pos.x, y: from.y + polygon.pos.y },
+ end: { x: to.x + polygon.pos.x, y: to.y + polygon.pos.y }
+ };
+ const hit = intersectLineLine(line, side);
+ if (hit) {
+ results.push(hit);
}
- exports.intersectLinePolygon = intersectLinePolygon;
+ });
+ return results;
+}
- /***/
- },
- /***/ "./src/model.ts":
- /*!**********************!*\
+/***/ }),
+
+/***/ "./src/model.ts":
+/*!**********************!*\
!*** ./src/model.ts ***!
\**********************/
- /***/ function (__unused_webpack_module, exports, __webpack_require__) {
- "use strict";
-
- var __importDefault =
- (this && this.__importDefault) ||
- function (mod) {
- return mod && mod.__esModule ? mod : { default: mod };
- };
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.BodyGroup =
- exports.BodyType =
- exports.SATCircle =
- exports.SATPolygon =
- exports.SATVector =
- exports.Response =
- exports.RBush =
- exports.isSimple =
- void 0;
- const rbush_1 = __importDefault(
- __webpack_require__(
- /*! rbush */ "./node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/rbush.min.js",
- ),
- );
- Object.defineProperty(exports, "RBush", {
- enumerable: true,
- get: function () {
- return rbush_1.default;
- },
- });
- const sat_1 = __webpack_require__(
- /*! sat */ "./node_modules/.pnpm/sat@0.9.0/node_modules/sat/SAT.js",
- );
- Object.defineProperty(exports, "SATCircle", {
- enumerable: true,
- get: function () {
- return sat_1.Circle;
- },
- });
- Object.defineProperty(exports, "SATPolygon", {
- enumerable: true,
- get: function () {
- return sat_1.Polygon;
- },
- });
- Object.defineProperty(exports, "Response", {
- enumerable: true,
- get: function () {
- return sat_1.Response;
- },
- });
- Object.defineProperty(exports, "SATVector", {
- enumerable: true,
- get: function () {
- return sat_1.Vector;
- },
- });
- var poly_decomp_es_1 = __webpack_require__(
- /*! poly-decomp-es */ "./node_modules/.pnpm/poly-decomp-es@0.4.2/node_modules/poly-decomp-es/dist/poly-decomp-es.js",
- );
- Object.defineProperty(exports, "isSimple", {
- enumerable: true,
- get: function () {
- return poly_decomp_es_1.isSimple;
- },
- });
- /**
- * types
- */
- var BodyType;
- (function (BodyType) {
- BodyType["Ellipse"] = "Ellipse";
- BodyType["Circle"] = "Circle";
- BodyType["Polygon"] = "Polygon";
- BodyType["Box"] = "Box";
- BodyType["Line"] = "Line";
- BodyType["Point"] = "Point";
- })((BodyType = exports.BodyType || (exports.BodyType = {})));
- /**
- * for groups
- */
- var BodyGroup;
- (function (BodyGroup) {
- BodyGroup[(BodyGroup["Ellipse"] = 32)] = "Ellipse";
- BodyGroup[(BodyGroup["Circle"] = 16)] = "Circle";
- BodyGroup[(BodyGroup["Polygon"] = 8)] = "Polygon";
- BodyGroup[(BodyGroup["Box"] = 4)] = "Box";
- BodyGroup[(BodyGroup["Line"] = 2)] = "Line";
- BodyGroup[(BodyGroup["Point"] = 1)] = "Point";
- })((BodyGroup = exports.BodyGroup || (exports.BodyGroup = {})));
-
- /***/
- },
-
- /***/ "./src/optimized.ts":
- /*!**************************!*\
+/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
+
+"use strict";
+
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.BodyGroup = exports.BodyType = exports.SATCircle = exports.SATPolygon = exports.SATVector = exports.Response = exports.RBush = exports.isSimple = void 0;
+const sat_1 = __webpack_require__(/*! sat */ "./node_modules/sat/SAT.js");
+Object.defineProperty(exports, "SATCircle", ({ enumerable: true, get: function () { return sat_1.Circle; } }));
+Object.defineProperty(exports, "SATPolygon", ({ enumerable: true, get: function () { return sat_1.Polygon; } }));
+Object.defineProperty(exports, "Response", ({ enumerable: true, get: function () { return sat_1.Response; } }));
+Object.defineProperty(exports, "SATVector", ({ enumerable: true, get: function () { return sat_1.Vector; } }));
+// version 4.0.0 1=1 copy
+const rbush_1 = __importDefault(__webpack_require__(/*! ./rbush */ "./src/rbush.js"));
+exports.RBush = rbush_1.default;
+var poly_decomp_es_1 = __webpack_require__(/*! poly-decomp-es */ "./node_modules/poly-decomp-es/dist/poly-decomp-es.js");
+Object.defineProperty(exports, "isSimple", ({ enumerable: true, get: function () { return poly_decomp_es_1.isSimple; } }));
+/**
+ * types
+ */
+var BodyType;
+(function (BodyType) {
+ BodyType["Ellipse"] = "Ellipse";
+ BodyType["Circle"] = "Circle";
+ BodyType["Polygon"] = "Polygon";
+ BodyType["Box"] = "Box";
+ BodyType["Line"] = "Line";
+ BodyType["Point"] = "Point";
+})(BodyType || (exports.BodyType = BodyType = {}));
+/**
+ * for groups
+ */
+var BodyGroup;
+(function (BodyGroup) {
+ BodyGroup[BodyGroup["Ellipse"] = 32] = "Ellipse";
+ BodyGroup[BodyGroup["Circle"] = 16] = "Circle";
+ BodyGroup[BodyGroup["Polygon"] = 8] = "Polygon";
+ BodyGroup[BodyGroup["Box"] = 4] = "Box";
+ BodyGroup[BodyGroup["Line"] = 2] = "Line";
+ BodyGroup[BodyGroup["Point"] = 1] = "Point";
+})(BodyGroup || (exports.BodyGroup = BodyGroup = {}));
+
+
+/***/ }),
+
+/***/ "./src/optimized.ts":
+/*!**************************!*\
!*** ./src/optimized.ts ***!
\**************************/
- /***/ (__unused_webpack_module, exports) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.map =
- exports.filter =
- exports.every =
- exports.some =
- exports.forEach =
- void 0;
+/***/ ((__unused_webpack_module, exports) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.map = exports.filter = exports.every = exports.some = exports.forEach = void 0;
+/**
+ * 40-90% faster than built-in Array.forEach function.
+ *
+ * basic benchmark: https://jsbench.me/urle772xdn
+ */
+const forEach = (array, callback) => {
+ for (let i = 0, l = array.length; i < l; i++) {
+ callback(array[i], i);
+ }
+};
+exports.forEach = forEach;
+/**
+ * 20-90% faster than built-in Array.some function.
+ *
+ * basic benchmark: https://jsbench.me/l0le7bnnsq
+ */
+const some = (array, callback) => {
+ for (let i = 0, l = array.length; i < l; i++) {
+ if (callback(array[i], i)) {
+ return true;
+ }
+ }
+ return false;
+};
+exports.some = some;
+/**
+ * 20-40% faster than built-in Array.every function.
+ *
+ * basic benchmark: https://jsbench.me/unle7da29v
+ */
+const every = (array, callback) => {
+ for (let i = 0, l = array.length; i < l; i++) {
+ if (!callback(array[i], i)) {
+ return false;
+ }
+ }
+ return true;
+};
+exports.every = every;
+/**
+ * 20-60% faster than built-in Array.filter function.
+ *
+ * basic benchmark: https://jsbench.me/o1le77ev4l
+ */
+const filter = (array, callback) => {
+ const output = [];
+ for (let i = 0, l = array.length; i < l; i++) {
+ const item = array[i];
+ if (callback(item, i)) {
+ output.push(item);
+ }
+ }
+ return output;
+};
+exports.filter = filter;
+/**
+ * 20-70% faster than built-in Array.map
+ *
+ * basic benchmark: https://jsbench.me/oyle77vbpc
+ */
+const map = (array, callback) => {
+ const l = array.length;
+ const output = new Array(l);
+ for (let i = 0; i < l; i++) {
+ output[i] = callback(array[i], i);
+ }
+ return output;
+};
+exports.map = map;
+
+
+/***/ }),
+
+/***/ "./src/system.ts":
+/*!***********************!*\
+ !*** ./src/system.ts ***!
+ \***********************/
+/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.System = void 0;
+const model_1 = __webpack_require__(/*! ./model */ "./src/model.ts");
+const utils_1 = __webpack_require__(/*! ./utils */ "./src/utils.ts");
+const intersect_1 = __webpack_require__(/*! ./intersect */ "./src/intersect.ts");
+const optimized_1 = __webpack_require__(/*! ./optimized */ "./src/optimized.ts");
+const base_system_1 = __webpack_require__(/*! ./base-system */ "./src/base-system.ts");
+const line_1 = __webpack_require__(/*! ./bodies/line */ "./src/bodies/line.ts");
+/**
+ * collision system
+ */
+class System extends base_system_1.BaseSystem {
+ constructor() {
+ super(...arguments);
/**
- * 40-90% faster than built-in Array.forEach function.
- *
- * basic benchmark: https://jsbench.me/urle772xdn
+ * the last collision result
*/
- const forEach = (array, callback) => {
- for (let i = 0, l = array.length; i < l; i++) {
- callback(array[i], i);
- }
+ this.response = new model_1.Response();
+ }
+ /**
+ * re-insert body into collision tree and update its bbox
+ * every body can be part of only one system
+ */
+ insert(body) {
+ const insertResult = super.insert(body);
+ // set system for later body.system.updateBody(body)
+ body.system = this;
+ return insertResult;
+ }
+ /**
+ * separate (move away) bodies
+ */
+ separate() {
+ (0, optimized_1.forEach)(this.all(), (body) => {
+ this.separateBody(body);
+ });
+ }
+ /**
+ * separate (move away) 1 body
+ */
+ separateBody(body) {
+ if (body.isStatic || body.isTrigger) {
+ return;
+ }
+ const offsets = { x: 0, y: 0 };
+ const addOffsets = ({ overlapV: { x, y } }) => {
+ offsets.x += x;
+ offsets.y += y;
};
- exports.forEach = forEach;
- /**
- * 20-90% faster than built-in Array.some function.
- *
- * basic benchmark: https://jsbench.me/l0le7bnnsq
- */
- const some = (array, callback) => {
- for (let i = 0, l = array.length; i < l; i++) {
- if (callback(array[i], i)) {
- return true;
+ this.checkOne(body, addOffsets);
+ if (offsets.x || offsets.y) {
+ body.setPosition(body.x - offsets.x, body.y - offsets.y);
+ }
+ }
+ /**
+ * check one body collisions with callback
+ */
+ checkOne(body, callback = utils_1.returnTrue, response = this.response) {
+ // no need to check static body collision
+ if (body.isStatic) {
+ return false;
+ }
+ const bodies = this.search(body);
+ const checkCollision = (candidate) => {
+ if (candidate !== body &&
+ this.checkCollision(body, candidate, response)) {
+ return callback(response);
}
- }
- return false;
};
- exports.some = some;
- /**
- * 20-40% faster than built-in Array.every function.
- *
- * basic benchmark: https://jsbench.me/unle7da29v
- */
- const every = (array, callback) => {
- for (let i = 0, l = array.length; i < l; i++) {
- if (!callback(array[i], i)) {
- return false;
- }
- }
- return true;
+ return (0, optimized_1.some)(bodies, checkCollision);
+ }
+ /**
+ * check all bodies collisions in area with callback
+ */
+ checkArea(area, callback = utils_1.returnTrue, response = this.response) {
+ const checkOne = (body) => {
+ return this.checkOne(body, callback, response);
};
- exports.every = every;
- /**
- * 20-60% faster than built-in Array.filter function.
- *
- * basic benchmark: https://jsbench.me/o1le77ev4l
- */
- const filter = (array, callback) => {
- const output = [];
- for (let i = 0, l = array.length; i < l; i++) {
- const item = array[i];
- if (callback(item, i)) {
- output.push(item);
- }
- }
- return output;
+ return (0, optimized_1.some)(this.search(area), checkOne);
+ }
+ /**
+ * check all bodies collisions with callback
+ */
+ checkAll(callback = utils_1.returnTrue, response = this.response) {
+ const checkOne = (body) => {
+ return this.checkOne(body, callback, response);
};
- exports.filter = filter;
- /**
- * 20-70% faster than built-in Array.map
- *
- * basic benchmark: https://jsbench.me/oyle77vbpc
- */
- const map = (array, callback) => {
- const l = array.length;
- const output = new Array(l);
- for (let i = 0; i < l; i++) {
- output[i] = callback(array[i], i);
+ return (0, optimized_1.some)(this.all(), checkOne);
+ }
+ /**
+ * check do 2 objects collide
+ */
+ checkCollision(bodyA, bodyB, response = this.response) {
+ const { bbox: bboxA } = bodyA;
+ const { bbox: bboxB } = bodyB;
+ // assess the bodies real aabb without padding
+ if (!(0, utils_1.canInteract)(bodyA, bodyB) ||
+ !bboxA ||
+ !bboxB ||
+ (0, utils_1.notIntersectAABB)(bboxA, bboxB)) {
+ return false;
+ }
+ const sat = (0, utils_1.getSATTest)(bodyA, bodyB);
+ // 99% of cases
+ if (bodyA.isConvex && bodyB.isConvex) {
+ // always first clear response
+ response.clear();
+ return sat(bodyA, bodyB, response);
+ }
+ // more complex (non convex) cases
+ const convexBodiesA = (0, intersect_1.ensureConvex)(bodyA);
+ const convexBodiesB = (0, intersect_1.ensureConvex)(bodyB);
+ let overlapX = 0;
+ let overlapY = 0;
+ let collided = false;
+ (0, optimized_1.forEach)(convexBodiesA, convexBodyA => {
+ (0, optimized_1.forEach)(convexBodiesB, convexBodyB => {
+ // always first clear response
+ response.clear();
+ if (sat(convexBodyA, convexBodyB, response)) {
+ collided = true;
+ overlapX += response.overlapV.x;
+ overlapY += response.overlapV.y;
+ }
+ });
+ });
+ if (collided) {
+ const vector = new model_1.SATVector(overlapX, overlapY);
+ response.a = bodyA;
+ response.b = bodyB;
+ response.overlapV.x = overlapX;
+ response.overlapV.y = overlapY;
+ response.overlapN = vector.normalize();
+ response.overlap = vector.len();
+ response.aInB = (0, utils_1.checkAInB)(bodyA, bodyB);
+ response.bInA = (0, utils_1.checkAInB)(bodyB, bodyA);
+ }
+ return collided;
+ }
+ /**
+ * raycast to get collider of ray from start to end
+ */
+ raycast(start, end, allow = utils_1.returnTrue) {
+ let minDistance = Infinity;
+ let result = null;
+ if (!this.ray) {
+ this.ray = new line_1.Line(start, end, { isTrigger: true });
+ }
+ else {
+ this.ray.start = start;
+ this.ray.end = end;
+ }
+ this.insert(this.ray);
+ this.checkOne(this.ray, ({ b: body }) => {
+ if (!allow(body, this.ray)) {
+ return false;
+ }
+ const points = body.typeGroup === model_1.BodyGroup.Circle
+ ? (0, intersect_1.intersectLineCircle)(this.ray, body)
+ : (0, intersect_1.intersectLinePolygon)(this.ray, body);
+ (0, optimized_1.forEach)(points, (point) => {
+ const pointDistance = (0, utils_1.distance)(start, point);
+ if (pointDistance < minDistance) {
+ minDistance = pointDistance;
+ result = { point, body };
+ }
+ });
+ });
+ this.remove(this.ray);
+ return result;
+ }
+}
+exports.System = System;
+
+
+/***/ }),
+
+/***/ "./src/utils.ts":
+/*!**********************!*\
+ !*** ./src/utils.ts ***!
+ \**********************/
+/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.RAD2DEG = exports.DEG2RAD = void 0;
+exports.deg2rad = deg2rad;
+exports.rad2deg = rad2deg;
+exports.createEllipse = createEllipse;
+exports.createBox = createBox;
+exports.ensureVectorPoint = ensureVectorPoint;
+exports.ensurePolygonPoints = ensurePolygonPoints;
+exports.distance = distance;
+exports.clockwise = clockwise;
+exports.extendBody = extendBody;
+exports.bodyMoved = bodyMoved;
+exports.notIntersectAABB = notIntersectAABB;
+exports.intersectAABB = intersectAABB;
+exports.canInteract = canInteract;
+exports.checkAInB = checkAInB;
+exports.clonePointsArray = clonePointsArray;
+exports.mapVectorToArray = mapVectorToArray;
+exports.mapArrayToVector = mapArrayToVector;
+exports.getBounceDirection = getBounceDirection;
+exports.getSATTest = getSATTest;
+exports.dashLineTo = dashLineTo;
+exports.drawPolygon = drawPolygon;
+exports.drawBVH = drawBVH;
+exports.cloneResponse = cloneResponse;
+exports.returnTrue = returnTrue;
+exports.getGroup = getGroup;
+exports.bin2dec = bin2dec;
+exports.ensureNumber = ensureNumber;
+exports.groupBits = groupBits;
+exports.move = move;
+const model_1 = __webpack_require__(/*! ./model */ "./src/model.ts");
+const sat_1 = __webpack_require__(/*! sat */ "./node_modules/sat/SAT.js");
+const intersect_1 = __webpack_require__(/*! ./intersect */ "./src/intersect.ts");
+const optimized_1 = __webpack_require__(/*! ./optimized */ "./src/optimized.ts");
+/* helpers for faster getSATTest() and checkAInB() */
+const testMap = {
+ satCircleCircle: sat_1.testCircleCircle,
+ satCirclePolygon: sat_1.testCirclePolygon,
+ satPolygonCircle: sat_1.testPolygonCircle,
+ satPolygonPolygon: sat_1.testPolygonPolygon,
+ inCircleCircle: intersect_1.circleInCircle,
+ inCirclePolygon: intersect_1.circleInPolygon,
+ inPolygonCircle: intersect_1.polygonInCircle,
+ inPolygonPolygon: intersect_1.polygonInPolygon
+};
+function createMap(bodyType, testType) {
+ return Object.values(model_1.BodyType).reduce((result, type) => (Object.assign(Object.assign({}, result), { [type]: type === model_1.BodyType.Circle
+ ? testMap[`${testType}${bodyType}Circle`]
+ : testMap[`${testType}${bodyType}Polygon`] })), {});
+}
+const circleSATFunctions = createMap(model_1.BodyType.Circle, "sat");
+const circleInFunctions = createMap(model_1.BodyType.Circle, "in");
+const polygonSATFunctions = createMap(model_1.BodyType.Polygon, "sat");
+const polygonInFunctions = createMap(model_1.BodyType.Polygon, "in");
+exports.DEG2RAD = Math.PI / 180;
+exports.RAD2DEG = 180 / Math.PI;
+/**
+ * convert from degrees to radians
+ */
+function deg2rad(degrees) {
+ return degrees * exports.DEG2RAD;
+}
+/**
+ * convert from radians to degrees
+ */
+function rad2deg(radians) {
+ return radians * exports.RAD2DEG;
+}
+/**
+ * creates ellipse-shaped polygon based on params
+ */
+function createEllipse(radiusX, radiusY = radiusX, step = 1) {
+ const steps = Math.PI * Math.hypot(radiusX, radiusY) * 2;
+ const length = Math.max(8, Math.ceil(steps / Math.max(1, step)));
+ const ellipse = [];
+ for (let index = 0; index < length; index++) {
+ const value = (index / length) * 2 * Math.PI;
+ const x = Math.cos(value) * radiusX;
+ const y = Math.sin(value) * radiusY;
+ ellipse.push(new sat_1.Vector(x, y));
+ }
+ return ellipse;
+}
+/**
+ * creates box shaped polygon points
+ */
+function createBox(width, height) {
+ return [
+ new sat_1.Vector(0, 0),
+ new sat_1.Vector(width, 0),
+ new sat_1.Vector(width, height),
+ new sat_1.Vector(0, height)
+ ];
+}
+/**
+ * ensure SATVector type point result
+ */
+function ensureVectorPoint(point = {}) {
+ return point instanceof sat_1.Vector
+ ? point
+ : new sat_1.Vector(point.x || 0, point.y || 0);
+}
+/**
+ * ensure Vector points (for polygon) in counter-clockwise order
+ */
+function ensurePolygonPoints(points = []) {
+ const polygonPoints = (0, optimized_1.map)(points, ensureVectorPoint);
+ return clockwise(polygonPoints) ? polygonPoints.reverse() : polygonPoints;
+}
+/**
+ * get distance between two Vector points
+ */
+function distance(bodyA, bodyB) {
+ const xDiff = bodyA.x - bodyB.x;
+ const yDiff = bodyA.y - bodyB.y;
+ return Math.hypot(xDiff, yDiff);
+}
+/**
+ * check [is clockwise] direction of polygon
+ */
+function clockwise(points) {
+ const length = points.length;
+ let sum = 0;
+ (0, optimized_1.forEach)(points, (v1, index) => {
+ const v2 = points[(index + 1) % length];
+ sum += (v2.x - v1.x) * (v2.y + v1.y);
+ });
+ return sum > 0;
+}
+/**
+ * used for all types of bodies in constructor
+ */
+function extendBody(body, options = {}) {
+ body.isStatic = !!options.isStatic;
+ body.isTrigger = !!options.isTrigger;
+ body.padding = options.padding || 0;
+ body.group = typeof options.group === "number" ? options.group : 0x7FFFFFFF;
+ if (body.typeGroup !== model_1.BodyGroup.Circle) {
+ body.isCentered = options.isCentered || false;
+ }
+ body.setAngle(options.angle || 0);
+}
+/**
+ * check if body moved outside of its padding
+ */
+function bodyMoved(body) {
+ const { bbox, minX, minY, maxX, maxY } = body;
+ return (bbox.minX < minX || bbox.minY < minY || bbox.maxX > maxX || bbox.maxY > maxY);
+}
+/**
+ * returns true if two boxes not intersect
+ */
+function notIntersectAABB(bodyA, bodyB) {
+ return (bodyB.minX > bodyA.maxX ||
+ bodyB.minY > bodyA.maxY ||
+ bodyB.maxX < bodyA.minX ||
+ bodyB.maxY < bodyA.minY);
+}
+/**
+ * checks if two boxes intersect
+ */
+function intersectAABB(bodyA, bodyB) {
+ return !notIntersectAABB(bodyA, bodyB);
+}
+/**
+ * checks if two bodies can interact (for collision filtering)
+ */
+function canInteract(bodyA, bodyB) {
+ return (((bodyA.group >> 16) & (bodyB.group & 0xFFFF) &&
+ (bodyB.group >> 16) & (bodyA.group & 0xFFFF)) !== 0);
+}
+/**
+ * checks if body a is in body b
+ */
+function checkAInB(bodyA, bodyB) {
+ const check = bodyA.typeGroup === model_1.BodyGroup.Circle
+ ? circleInFunctions
+ : polygonInFunctions;
+ return check[bodyB.type](bodyA, bodyB);
+}
+/**
+ * clone sat vector points array into vector points array
+ */
+function clonePointsArray(points) {
+ return (0, optimized_1.map)(points, ({ x, y }) => ({ x, y }));
+}
+/**
+ * change format from SAT.js to poly-decomp
+ */
+function mapVectorToArray({ x, y } = { x: 0, y: 0 }) {
+ return [x, y];
+}
+/**
+ * change format from poly-decomp to SAT.js
+ */
+function mapArrayToVector([x, y] = [0, 0]) {
+ return { x, y };
+}
+/**
+ * given 2 bodies calculate vector of bounce assuming equal mass and they are circles
+ */
+function getBounceDirection(body, collider) {
+ const v2 = new sat_1.Vector(collider.x - body.x, collider.y - body.y);
+ const v1 = new sat_1.Vector(body.x - collider.x, body.y - collider.y);
+ const len = v1.dot(v2.normalize()) * 2;
+ return new sat_1.Vector(v2.x * len - v1.x, v2.y * len - v1.y).normalize();
+}
+/**
+ * returns correct sat.js testing function based on body types
+ */
+function getSATTest(bodyA, bodyB) {
+ const check = bodyA.typeGroup === model_1.BodyGroup.Circle
+ ? circleSATFunctions
+ : polygonSATFunctions;
+ return check[bodyB.type];
+}
+/**
+ * draws dashed line on canvas context
+ */
+function dashLineTo(context, fromX, fromY, toX, toY, dash = 2, gap = 4) {
+ const xDiff = toX - fromX;
+ const yDiff = toY - fromY;
+ const arc = Math.atan2(yDiff, xDiff);
+ const offsetX = Math.cos(arc);
+ const offsetY = Math.sin(arc);
+ let posX = fromX;
+ let posY = fromY;
+ let dist = Math.hypot(xDiff, yDiff);
+ while (dist > 0) {
+ const step = Math.min(dist, dash);
+ context.moveTo(posX, posY);
+ context.lineTo(posX + offsetX * step, posY + offsetY * step);
+ posX += offsetX * (dash + gap);
+ posY += offsetY * (dash + gap);
+ dist -= dash + gap;
+ }
+}
+/**
+ * draw polygon
+ */
+function drawPolygon(context, { pos, calcPoints }, isTrigger = false) {
+ const lastPoint = calcPoints[calcPoints.length - 1];
+ const fromX = pos.x + lastPoint.x;
+ const fromY = pos.y + lastPoint.y;
+ if (calcPoints.length === 1) {
+ context.arc(fromX, fromY, 1, 0, Math.PI * 2);
+ }
+ else {
+ context.moveTo(fromX, fromY);
+ }
+ (0, optimized_1.forEach)(calcPoints, (point, index) => {
+ const toX = pos.x + point.x;
+ const toY = pos.y + point.y;
+ if (isTrigger) {
+ const prev = calcPoints[index - 1] || lastPoint;
+ dashLineTo(context, pos.x + prev.x, pos.y + prev.y, toX, toY);
+ }
+ else {
+ context.lineTo(toX, toY);
+ }
+ });
+}
+/**
+ * draw body bounding body box
+ */
+function drawBVH(context, body) {
+ drawPolygon(context, {
+ pos: { x: body.minX, y: body.minY },
+ calcPoints: createBox(body.maxX - body.minX, body.maxY - body.minY)
+ });
+}
+/**
+ * clone response object returning new response with previous ones values
+ */
+function cloneResponse(response) {
+ const clone = new sat_1.Response();
+ const { a, b, overlap, overlapN, overlapV, aInB, bInA } = response;
+ clone.a = a;
+ clone.b = b;
+ clone.overlap = overlap;
+ clone.overlapN = overlapN.clone();
+ clone.overlapV = overlapV.clone();
+ clone.aInB = aInB;
+ clone.bInA = bInA;
+ return clone;
+}
+/**
+ * dummy fn used as default, for optimization
+ */
+function returnTrue() {
+ return true;
+}
+/**
+ * for groups
+ */
+function getGroup(group) {
+ return Math.max(0, Math.min(group, 0x7FFFFFFF));
+}
+/**
+ * binary string to decimal number
+ */
+function bin2dec(binary) {
+ return Number(`0b${binary}`.replace(/\s/g, ""));
+}
+/**
+ * helper for groupBits()
+ *
+ * @param input - number or binary string
+ */
+function ensureNumber(input) {
+ return typeof input === "number" ? input : bin2dec(input);
+}
+/**
+ * create group bits from category and mask
+ *
+ * @param category - category bits
+ * @param mask - mask bits (default: category)
+ */
+function groupBits(category, mask = category) {
+ return (ensureNumber(category) << 16) | ensureNumber(mask);
+}
+function move(body, speed = 1, updateNow = true) {
+ if (!speed) {
+ return;
+ }
+ const moveX = Math.cos(body.angle) * speed;
+ const moveY = Math.sin(body.angle) * speed;
+ body.setPosition(body.x + moveX, body.y + moveY, updateNow);
+}
+
+
+/***/ }),
+
+/***/ "./src/demo/canvas.js":
+/*!****************************!*\
+ !*** ./src/demo/canvas.js ***!
+ \****************************/
+/***/ ((module) => {
+
+const width = window.innerWidth || 1024;
+const height = window.innerHeight || 768;
+
+class TestCanvas {
+ constructor(test) {
+ this.test = test;
+
+ this.element = document.createElement("div");
+ this.element.id = "debug";
+ this.element.innerHTML = `${this.test.legend}
+
+
+ Show Bounding Volume Hierarchy
+
+
`;
+
+ this.canvas = document.createElement("canvas");
+ this.canvas.width = width;
+ this.canvas.height = height;
+
+ this.context = this.canvas.getContext("2d");
+ this.context.font = "24px Arial";
+ this.test.context = this.context;
+
+ this.bvhCheckbox = this.element.querySelector("#bvh");
+
+ if (this.canvas instanceof Node) {
+ this.element.appendChild(this.canvas);
+ }
+
+ this.fps = 0;
+ this.frame = 0;
+ this.started = Date.now();
+
+ loop(this.update.bind(this));
+ }
+
+ update() {
+ this.frame++;
+
+ const timeDiff = Date.now() - this.started;
+ if (timeDiff >= 1000) {
+ this.fps = this.frame / (timeDiff / 1000);
+ this.frame = 0;
+ this.started = Date.now();
+ }
+
+ // Clear the canvas
+ this.context.fillStyle = "#000000";
+ this.context.fillRect(0, 0, width, height);
+
+ // Render the bodies
+ this.context.strokeStyle = "#FFFFFF";
+ this.context.beginPath();
+ this.test.physics.draw(this.context);
+ this.context.stroke();
+
+ // Render the BVH
+ if (this.bvhCheckbox.checked) {
+ this.context.strokeStyle = "#00FF00";
+ this.context.beginPath();
+ this.test.physics.drawBVH(this.context);
+ this.context.stroke();
+ }
+
+ // Render the FPS
+ this.context.fillStyle = "#FFCC00";
+ this.context.fillText(
+ `FPS: ${this.fps ? this.fps.toFixed(0) : "?"}`,
+ 24,
+ 48,
+ );
+
+ if (this.test.drawCallback) {
+ this.test.drawCallback();
+ }
+ }
+}
+
+function loop(callback) {
+ // interval for fps instead of setTimeout
+ // and ms = 1 which is lowest nonzero value
+ // for responsiveness of user input
+ setInterval(callback, 1);
+}
+
+module.exports.TestCanvas = TestCanvas;
+
+module.exports.loop = loop;
+
+module.exports.width = width;
+
+module.exports.height = height;
+
+
+/***/ }),
+
+/***/ "./src/demo/stress.js":
+/*!****************************!*\
+ !*** ./src/demo/stress.js ***!
+ \****************************/
+/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
+
+const { BodyGroup } = __webpack_require__(/*! ../model */ "./src/model.ts");
+const { System } = __webpack_require__(/*! ../system */ "./src/system.ts");
+const { getBounceDirection, groupBits } = __webpack_require__(/*! ../utils */ "./src/utils.ts");
+const { width, height, loop } = __webpack_require__(/*! ./canvas */ "./src/demo/canvas.js");
+const seededRandom = (__webpack_require__(/*! random-seed */ "./node_modules/random-seed/index.js").create)("@Prozi").random;
+
+function random(min, max) {
+ return Math.floor(seededRandom() * max) + min;
+}
+
+class Stress {
+ constructor(count = 2000) {
+ this.size = Math.sqrt((width * height) / (count * 50));
+
+ this.physics = new System(5);
+ this.bodies = [];
+ this.polygons = 0;
+ this.boxes = 0;
+ this.circles = 0;
+ this.ellipses = 0;
+ this.lines = 0;
+ this.lastVariant = 0;
+ this.count = count;
+ this.bounds = this.getBounds();
+ this.enableFiltering = false;
+
+ for (let i = 0; i < count; ++i) {
+ this.createShape(!random(0, 20));
+ }
+
+ this.legend = `Total: ${count}
+ Polygons: ${this.polygons}
+ Boxes: ${this.boxes}
+ Circles: ${this.circles}
+ Ellipses: ${this.ellipses}
+ Lines: ${this.lines}
+
+
+ Enable Collision Filtering
+
+
+ `;
+
+ this.lastTime = Date.now();
+ this.updateBody = this.updateBody.bind(this);
+
+ // observer #debug & add filtering checkbox event
+ const observer = new window.MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ mutation.addedNodes.forEach((node) => {
+ if (node.id == "debug") {
+ document
+ .querySelector("#filtering")
+ .addEventListener("change", () => this.toggleFiltering());
+ observer.disconnect();
}
- return output;
- };
- exports.map = map;
+ });
+ });
+ });
+ observer.observe(document.querySelector("body"), {
+ subtree: false,
+ childList: true,
+ });
+
+ this.start = () => {
+ loop(this.update.bind(this));
+ };
+ }
- /***/
- },
+ getBounds() {
+ return [
+ this.physics.createBox({ x: 0, y: 0 }, width, 10, {
+ isStatic: true,
+ }),
+ this.physics.createBox({ x: width - 10, y: 0 }, 10, height, {
+ isStatic: true,
+ }),
+ this.physics.createBox({ x: 0, y: height - 10 }, width, 10, {
+ isStatic: true,
+ }),
+ this.physics.createBox({ x: 0, y: 0 }, 10, height, {
+ isStatic: true,
+ }),
+ ];
+ }
- /***/ "./src/system.ts":
- /*!***********************!*\
- !*** ./src/system.ts ***!
- \***********************/
- /***/ (__unused_webpack_module, exports, __webpack_require__) => {
- "use strict";
+ toggleFiltering() {
+ this.enableFiltering = !this.enableFiltering;
+ this.physics.clear();
+ this.bodies.length = 0;
+ this.polygons = 0;
+ this.boxes = 0;
+ this.circles = 0;
+ this.ellipses = 0;
+ this.lines = 0;
+ this.lastVariant = 0;
+ this.bounds = this.getBounds();
+ for (let i = 0; i < this.count; ++i) {
+ this.createShape(!random(0, 20));
+ }
+ }
+
+ update() {
+ const now = Date.now();
+ this.timeScale = Math.min(1000, now - this.lastTime) / 60;
+ this.lastTime = now;
+ this.bodies.forEach(this.updateBody);
+ }
+
+ updateBody(body) {
+ body.setAngle(body.angle + body.rotationSpeed * this.timeScale, false);
+
+ if (seededRandom() < 0.05 * this.timeScale) {
+ body.targetScale.x = 0.5 + seededRandom();
+ }
+
+ if (seededRandom() < 0.05 * this.timeScale) {
+ body.targetScale.y = 0.5 + seededRandom();
+ }
+
+ if (Math.abs(body.targetScale.x - body.scaleX) > 0.01) {
+ const scaleX =
+ body.scaleX +
+ Math.sign(body.targetScale.x - body.scaleX) * 0.02 * this.timeScale;
+ const scaleY =
+ body.scaleY +
+ Math.sign(body.targetScale.y - body.scaleY) * 0.02 * this.timeScale;
+
+ body.setScale(scaleX, scaleY, false);
+ }
+
+ // as last step update position, and bounding box
+ body.setPosition(
+ body.x + body.directionX * this.timeScale,
+ body.y + body.directionY * this.timeScale,
+ );
+
+ // separate + bounce
+ this.bounceBody(body);
+ }
+
+ bounceBody(body) {
+ const bounces = { x: 0, y: 0 };
+ const addBounces = ({ overlapV: { x, y } }) => {
+ bounces.x += x;
+ bounces.y += y;
+ };
+
+ this.physics.checkOne(body, addBounces);
+
+ if (bounces.x || bounces.y) {
+ const size = 0.5 * (body.scaleX + body.scaleY);
+ const bounce = getBounceDirection(body, {
+ x: body.x + bounces.x,
+ y: body.y + bounces.y,
+ });
+
+ bounce.scale(body.size).add({
+ x: body.directionX * size,
+ y: body.directionY * size,
+ });
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.System = void 0;
- const base_system_1 = __webpack_require__(
- /*! ./base-system */ "./src/base-system.ts",
+ const { x, y } = bounce.normalize();
+
+ body.directionX = x;
+ body.directionY = y;
+ body.rotationSpeed = (seededRandom() - seededRandom()) * 0.1;
+
+ body.setPosition(body.x - bounces.x, body.y - bounces.y);
+ }
+ }
+
+ createShape(large) {
+ const minSize = this.size * 1.0 * (large ? seededRandom() + 1 : 1);
+ const maxSize = this.size * 1.25 * (large ? seededRandom() * 2 + 1 : 1);
+ const x = random(0, width);
+ const y = random(0, height);
+ const direction = (random(0, 360) * Math.PI) / 180;
+ const options = {
+ isCentered: true,
+ padding: (minSize + maxSize) * 0.2,
+ };
+
+ let body;
+ let variant = this.lastVariant++ % 5;
+
+ switch (variant) {
+ case 0:
+ if (this.enableFiltering) {
+ options.group = groupBits(BodyGroup.Circle);
+ }
+ body = this.physics.createCircle(
+ { x, y },
+ random(minSize, maxSize) / 2,
+ options,
);
- const line_1 = __webpack_require__(
- /*! ./bodies/line */ "./src/bodies/line.ts",
+
+ ++this.circles;
+ break;
+
+ case 1:
+ const width = random(minSize, maxSize);
+ const height = random(minSize, maxSize);
+ if (this.enableFiltering) {
+ options.group = groupBits(BodyGroup.Ellipse);
+ console.log();
+ }
+ body = this.physics.createEllipse({ x, y }, width, height, 2, options);
+
+ ++this.ellipses;
+ break;
+
+ case 2:
+ if (this.enableFiltering) {
+ options.group = groupBits(BodyGroup.Box);
+ }
+ body = this.physics.createBox(
+ { x, y },
+ random(minSize, maxSize),
+ random(minSize, maxSize),
+ options,
);
- const intersect_1 = __webpack_require__(
- /*! ./intersect */ "./src/intersect.ts",
+
+ ++this.boxes;
+ break;
+
+ case 3:
+ if (this.enableFiltering) {
+ options.group = groupBits(BodyGroup.Line);
+ }
+ body = this.physics.createLine(
+ { x, y },
+ {
+ x: x + random(minSize, maxSize),
+ y: y + random(minSize, maxSize),
+ },
+ options,
);
- const model_1 = __webpack_require__(/*! ./model */ "./src/model.ts");
- const optimized_1 = __webpack_require__(
- /*! ./optimized */ "./src/optimized.ts",
+
+ ++this.lines;
+ break;
+
+ default:
+ if (this.enableFiltering) {
+ options.group = groupBits(BodyGroup.Polygon);
+ }
+ body = this.physics.createPolygon(
+ { x, y },
+ [
+ { x: -random(minSize, maxSize), y: random(minSize, maxSize) },
+ { x: random(minSize, maxSize), y: random(minSize, maxSize) },
+ { x: random(minSize, maxSize), y: -random(minSize, maxSize) },
+ { x: -random(minSize, maxSize), y: -random(minSize, maxSize) },
+ ],
+ options,
);
- const utils_1 = __webpack_require__(/*! ./utils */ "./src/utils.ts");
- /**
- * collision system
- */
- class System extends base_system_1.BaseSystem {
- constructor() {
- super(...arguments);
- /**
- * the last collision result
- */
- this.response = new model_1.Response();
- }
- /**
- * re-insert body into collision tree and update its bbox
- * every body can be part of only one system
- */
- insert(body) {
- const insertResult = super.insert(body);
- // set system for later body.system.updateBody(body)
- body.system = this;
- return insertResult;
- }
- /**
- * separate (move away) bodies
- */
- separate() {
- (0, optimized_1.forEach)(this.all(), (body) => {
- this.separateBody(body);
- });
- }
- /**
- * separate (move away) 1 body
- */
- separateBody(body) {
- if (body.isStatic || body.isTrigger) {
- return;
- }
- const offsets = { x: 0, y: 0 };
- const addOffsets = ({ overlapV: { x, y } }) => {
- offsets.x += x;
- offsets.y += y;
- };
- this.checkOne(body, addOffsets);
- if (offsets.x || offsets.y) {
- body.setPosition(body.x - offsets.x, body.y - offsets.y);
- }
- }
- /**
- * check one body collisions with callback
- */
- checkOne(
- body,
- callback = utils_1.returnTrue,
- response = this.response,
- ) {
- // no need to check static body collision
- if (body.isStatic) {
- return false;
- }
- const bodies = this.search(body);
- const checkCollision = (candidate) => {
- if (
- candidate !== body &&
- this.checkCollision(body, candidate, response)
- ) {
- return callback(response);
- }
- };
- return (0, optimized_1.some)(bodies, checkCollision);
- }
- /**
- * check all bodies collisions in area with callback
- */
- checkArea(
- area,
- callback = utils_1.returnTrue,
- response = this.response,
- ) {
- const checkOne = (body) => {
- return this.checkOne(body, callback, response);
- };
- return (0, optimized_1.some)(this.search(area), checkOne);
- }
- /**
- * check all bodies collisions with callback
- */
- checkAll(callback = utils_1.returnTrue, response = this.response) {
- const checkOne = (body) => {
- return this.checkOne(body, callback, response);
- };
- return (0, optimized_1.some)(this.all(), checkOne);
- }
- /**
- * check do 2 objects collide
- */
- checkCollision(bodyA, bodyB, response = this.response) {
- const { bbox: bboxA } = bodyA;
- const { bbox: bboxB } = bodyA;
- // assess the bodies real aabb without padding
- if (
- !(0, utils_1.canInteract)(bodyA, bodyB) ||
- !bboxA ||
- !bboxB ||
- (0, utils_1.notIntersectAABB)(bboxA, bboxB)
- ) {
- return false;
- }
- const sat = (0, utils_1.getSATTest)(bodyA, bodyB);
- // 99% of cases
- if (bodyA.isConvex && bodyB.isConvex) {
- // always first clear response
- response.clear();
- return sat(bodyA, bodyB, response);
- }
- // more complex (non convex) cases
- const convexBodiesA = (0, intersect_1.ensureConvex)(bodyA);
- const convexBodiesB = (0, intersect_1.ensureConvex)(bodyB);
- let overlapX = 0;
- let overlapY = 0;
- let collided = false;
- (0, optimized_1.forEach)(convexBodiesA, (convexBodyA) => {
- (0, optimized_1.forEach)(convexBodiesB, (convexBodyB) => {
- // always first clear response
- response.clear();
- if (sat(convexBodyA, convexBodyB, response)) {
- collided = true;
- overlapX += response.overlapV.x;
- overlapY += response.overlapV.y;
+
+ ++this.polygons;
+ break;
+ }
+
+ // set initial rotation angle direction
+ body.rotationSpeed = (seededRandom() - seededRandom()) * 0.1;
+ body.setAngle((random(0, 360) * Math.PI) / 180);
+
+ body.targetScale = { x: 1, y: 1 };
+ body.size = (minSize + maxSize) / 2;
+
+ body.directionX = Math.cos(direction);
+ body.directionY = Math.sin(direction);
+
+ this.bodies.push(body);
+ }
+}
+
+module.exports = Stress;
+
+
+/***/ }),
+
+/***/ "./src/demo/tank.js":
+/*!**************************!*\
+ !*** ./src/demo/tank.js ***!
+ \**************************/
+/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
+
+const { BodyGroup } = __webpack_require__(/*! ../model */ "./src/model.ts");
+const { System } = __webpack_require__(/*! ../system */ "./src/system.ts");
+const { mapVectorToArray } = __webpack_require__(/*! ../utils */ "./src/utils.ts");
+const { width, height, loop } = __webpack_require__(/*! ./canvas */ "./src/demo/canvas.js");
+
+class Tank {
+ constructor() {
+ this.physics = new System();
+ this.bodies = [];
+ this.player = this.createPlayer(400, 300);
+
+ this.createPolygon(
+ 300,
+ 300,
+ [
+ { x: -11.25, y: -6.76 },
+ { x: -12.5, y: -6.76 },
+ { x: -12.5, y: 6.75 },
+ { x: -3.1, y: 6.75 },
+ { x: -3.1, y: 0.41 },
+ { x: -2.35, y: 0.41 },
+ { x: -2.35, y: 6.75 },
+ { x: 0.77, y: 6.75 },
+ { x: 0.77, y: 7.5 },
+ { x: -13.25, y: 7.5 },
+ { x: -13.25, y: -7.51 },
+ { x: -11.25, y: -7.51 },
+ ]
+ .map(mapVectorToArray)
+ .map(([x, y]) => [x * 10, y * 10]),
+ );
+
+ this.up = false;
+ this.down = false;
+ this.left = false;
+ this.right = false;
+
+ this.legend = `W, S - Accelerate/Decelerate
+ A, D - Turn
`;
+
+ const updateKeys = ({ type, key }) => {
+ const keyDown = type === "keydown";
+ const keyLowerCase = key.toLowerCase();
+
+ keyLowerCase === "w" && (this.up = keyDown);
+ keyLowerCase === "s" && (this.down = keyDown);
+ keyLowerCase === "a" && (this.left = keyDown);
+ keyLowerCase === "d" && (this.right = keyDown);
+ };
+
+ document.addEventListener("keydown", updateKeys);
+ document.addEventListener("keyup", updateKeys);
+
+ if (this.canvas instanceof Node) {
+ this.element.appendChild(this.canvas);
+ }
+
+ this.createMap();
+ this.lastTime = Date.now();
+
+ this.start = () => {
+ loop(this.update.bind(this));
+ };
+ }
+
+ update() {
+ const now = Date.now();
+ this.timeScale = Math.min(1000, now - this.lastTime) / 60;
+ this.lastTime = now;
+ this.handleInput();
+ this.processGameLogic();
+ this.handleCollisions();
+ this.updateTurret();
+ }
+
+ handleInput() {
+ if (this.up) {
+ this.player.velocity += 0.2 * this.timeScale;
+ }
+
+ if (this.down) {
+ this.player.velocity -= 0.2 * this.timeScale;
+ }
+
+ if (this.left) {
+ this.player.setAngle(this.player.angle - 0.2 * this.timeScale);
+ }
+
+ if (this.right) {
+ this.player.setAngle(this.player.angle + 0.2 * this.timeScale);
+ }
+ }
+
+ processGameLogic() {
+ const x = Math.cos(this.player.angle);
+ const y = Math.sin(this.player.angle);
+
+ if (this.player.velocity > 0) {
+ this.player.velocity = Math.max(
+ this.player.velocity - 0.1 * this.timeScale,
+ 0,
+ );
+
+ if (this.player.velocity > 2) {
+ this.player.velocity = 2;
+ }
+ } else if (this.player.velocity < 0) {
+ this.player.velocity = Math.min(
+ this.player.velocity + 0.1 * this.timeScale,
+ 0,
+ );
+
+ if (this.player.velocity < -2) {
+ this.player.velocity = -2;
+ }
+ }
+
+ if (!Math.round(this.player.velocity * 100)) {
+ this.player.velocity = 0;
+ }
+
+ if (this.player.velocity) {
+ this.player.setPosition(
+ this.player.x + x * this.player.velocity,
+ this.player.y + y * this.player.velocity,
+ );
+ }
+ }
+
+ handleCollisions() {
+ this.physics.checkAll(({ a, b, overlapV }) => {
+ if (a.isTrigger || b.isTrigger) {
+ return;
+ }
+
+ if (a.typeGroup === BodyGroup.Polygon || a === this.player) {
+ a.setPosition(a.pos.x - overlapV.x, a.pos.y - overlapV.y);
+ }
+
+ if (b.typeGroup === BodyGroup.Circle || b === this.player) {
+ b.setPosition(b.pos.x + overlapV.x, b.pos.y + overlapV.y);
+ }
+
+ if (a === this.player) {
+ a.velocity *= 0.9;
+ }
+ });
+ }
+
+ updateTurret() {
+ this.playerTurret.setAngle(this.player.angle, false);
+ this.playerTurret.setPosition(this.player.x, this.player.y);
+
+ const hit = this.physics.raycast(
+ this.playerTurret.start,
+ this.playerTurret.end,
+ (test) => test !== this.player,
+ );
+
+ this.drawCallback = () => {
+ if (hit) {
+ this.context.strokeStyle = "#FF0000";
+ this.context.beginPath();
+ this.context.arc(hit.point.x, hit.point.y, 5, 0, 2 * Math.PI);
+ this.context.stroke();
+ }
+ };
+ }
+
+ createPlayer(x, y, size = 13) {
+ const player =
+ Math.random() < 0.5
+ ? this.physics.createCircle(
+ { x: this.scaleX(x), y: this.scaleY(y) },
+ this.scaleX(size / 2),
+ { isCentered: true },
+ )
+ : this.physics.createBox(
+ { x: this.scaleX(x - size / 2), y: this.scaleY(y - size / 2) },
+ this.scaleX(size),
+ this.scaleX(size),
+ { isCentered: true },
+ );
+
+ player.velocity = 0;
+ player.setOffset({ x: -this.scaleX(size / 2), y: 0 });
+ player.setAngle(0.2);
+
+ this.physics.updateBody(player);
+ this.playerTurret = this.physics.createLine(
+ player,
+ { x: player.x + this.scaleX(20) + this.scaleY(20), y: player.y },
+ { angle: 0.2, isTrigger: true },
+ );
+
+ return player;
+ }
+
+ scaleX(x) {
+ return (x / 800) * width;
+ }
+
+ scaleY(y) {
+ return (y / 600) * height;
+ }
+
+ createCircle(x, y, radius) {
+ this.physics.createCircle(
+ { x: this.scaleX(x), y: this.scaleY(y) },
+ this.scaleX(radius),
+ );
+ }
+
+ createEllipse(x, y, radiusX, radiusY, step, angle) {
+ this.physics.createEllipse(
+ { x: this.scaleX(x), y: this.scaleY(y) },
+ this.scaleX(radiusX),
+ this.scaleY(radiusY),
+ step,
+ { angle },
+ );
+ }
+
+ createPolygon(x, y, points, angle) {
+ const scaledPoints = points.map(([pointX, pointY]) => ({
+ x: this.scaleX(pointX),
+ y: this.scaleY(pointY),
+ }));
+
+ return this.physics.createPolygon(
+ { x: this.scaleX(x), y: this.scaleY(y) },
+ scaledPoints,
+ { angle, isStatic: true },
+ );
+ }
+
+ createMap(width = 800, height = 600) {
+ // World bounds
+ // World bounds
+ this.createPolygon(0, 0, [
+ [0, 0],
+ [width, 0],
+ ]);
+ this.createPolygon(0, 0, [
+ [width, 0],
+ [width, height],
+ ]);
+ this.createPolygon(0, 0, [
+ [width, height],
+ [0, height],
+ ]);
+ this.createPolygon(0, 0, [
+ [0, height],
+ [0, 0],
+ ]);
+
+ // Factory
+ this.createPolygon(
+ 100,
+ 100,
+ [
+ [-50, -50],
+ [50, -50],
+ [50, 50],
+ [-50, 50],
+ ],
+ 0.4,
+ );
+ this.createPolygon(
+ 190,
+ 105,
+ [
+ [-20, -20],
+ [20, -20],
+ [20, 20],
+ [-20, 20],
+ ],
+ 0.4,
+ );
+ this.createCircle(170, 140, 6);
+ this.createCircle(185, 155, 6);
+ this.createCircle(165, 165, 6);
+ this.createCircle(145, 165, 6);
+
+ // Airstrip
+ this.createPolygon(
+ 230,
+ 50,
+ [
+ [-150, -30],
+ [150, -30],
+ [150, 30],
+ [-150, 30],
+ ],
+ 0.4,
+ );
+
+ // HQ
+ this.createPolygon(
+ 100,
+ 500,
+ [
+ [-40, -50],
+ [40, -50],
+ [50, 50],
+ [-50, 50],
+ ],
+ 0.2,
+ );
+ this.createCircle(180, 490, 12);
+ this.createCircle(175, 540, 12);
+
+ // Barracks
+ this.createPolygon(
+ 400,
+ 500,
+ [
+ [-60, -20],
+ [60, -20],
+ [60, 20],
+ [-60, 20],
+ ],
+ 1.7,
+ );
+ this.createPolygon(
+ 350,
+ 494,
+ [
+ [-60, -20],
+ [60, -20],
+ [60, 20],
+ [-60, 20],
+ ],
+ 1.7,
+ );
+
+ // Mountains
+ this.createPolygon(750, 0, [
+ [0, 0],
+ [-20, 100],
+ ]);
+ this.createPolygon(750, 0, [
+ [-20, 100],
+ [30, 250],
+ ]);
+ this.createPolygon(750, 0, [
+ [30, 250],
+ [20, 300],
+ ]);
+ this.createPolygon(750, 0, [
+ [20, 300],
+ [-50, 320],
+ ]);
+ this.createPolygon(750, 0, [
+ [-50, 320],
+ [-90, 500],
+ ]);
+ this.createPolygon(750, 0, [
+ [-90, 500],
+ [-200, 600],
+ ]);
+
+ // Lake
+ this.createEllipse(530, 130, 80, 70, 10, -0.2);
+ }
+}
+
+module.exports = Tank;
+
+
+/***/ }),
+
+/***/ "./src/rbush.js":
+/*!**********************!*\
+ !*** ./src/rbush.js ***!
+ \**********************/
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (/* binding */ RBush)
+/* harmony export */ });
+/* harmony import */ var quickselect__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! quickselect */ "./node_modules/quickselect/index.js");
+
+
+class RBush {
+ constructor(maxEntries = 9) {
+ // max entries in a node is 9 by default; min node fill is 40% for best performance
+ this._maxEntries = Math.max(4, maxEntries);
+ this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
+ this.clear();
+ }
+
+ all() {
+ return this._all(this.data, []);
+ }
+
+ search(bbox) {
+ let node = this.data;
+ const result = [];
+
+ if (!intersects(bbox, node)) return result;
+
+ const toBBox = this.toBBox;
+ const nodesToSearch = [];
+
+ while (node) {
+ for (let i = 0; i < node.children.length; i++) {
+ const child = node.children[i];
+ const childBBox = node.leaf ? toBBox(child) : child;
+
+ if (intersects(bbox, childBBox)) {
+ if (node.leaf) result.push(child);
+ else if (contains(bbox, childBBox)) this._all(child, result);
+ else nodesToSearch.push(child);
}
- });
- });
- if (collided) {
- const vector = new model_1.SATVector(overlapX, overlapY);
- response.a = bodyA;
- response.b = bodyB;
- response.overlapV.x = overlapX;
- response.overlapV.y = overlapY;
- response.overlapN = vector.normalize();
- response.overlap = vector.len();
- response.aInB = (0, utils_1.checkAInB)(bodyA, bodyB);
- response.bInA = (0, utils_1.checkAInB)(bodyB, bodyA);
- }
- return collided;
- }
- /**
- * raycast to get collider of ray from start to end
- */
- raycast(start, end, allow = utils_1.returnTrue) {
- let minDistance = Infinity;
- let result = null;
- if (!this.ray) {
- this.ray = new line_1.Line(start, end, { isTrigger: true });
- } else {
- this.ray.start = start;
- this.ray.end = end;
}
- this.insert(this.ray);
- this.checkOne(this.ray, ({ b: body }) => {
- if (!allow(body, this.ray)) {
- return false;
- }
- const points =
- body.typeGroup === model_1.BodyGroup.Circle
- ? (0, intersect_1.intersectLineCircle)(this.ray, body)
- : (0, intersect_1.intersectLinePolygon)(this.ray, body);
- (0, optimized_1.forEach)(points, (point) => {
- const pointDistance = (0, utils_1.distance)(start, point);
- if (pointDistance < minDistance) {
- minDistance = pointDistance;
- result = { point, body };
- }
- });
- });
- this.remove(this.ray);
- return result;
- }
+ node = nodesToSearch.pop();
}
- exports.System = System;
- /***/
- },
+ return result;
+ }
- /***/ "./src/utils.ts":
- /*!**********************!*\
- !*** ./src/utils.ts ***!
- \**********************/
- /***/ (__unused_webpack_module, exports, __webpack_require__) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.groupBits =
- exports.ensureNumber =
- exports.bin2dec =
- exports.getGroup =
- exports.returnTrue =
- exports.cloneResponse =
- exports.drawBVH =
- exports.drawPolygon =
- exports.dashLineTo =
- exports.getSATTest =
- exports.getBounceDirection =
- exports.mapArrayToVector =
- exports.mapVectorToArray =
- exports.clonePointsArray =
- exports.checkAInB =
- exports.canInteract =
- exports.intersectAABB =
- exports.notIntersectAABB =
- exports.bodyMoved =
- exports.extendBody =
- exports.clockwise =
- exports.distance =
- exports.ensurePolygonPoints =
- exports.ensureVectorPoint =
- exports.createBox =
- exports.createEllipse =
- exports.rad2deg =
- exports.deg2rad =
- exports.RAD2DEG =
- exports.DEG2RAD =
- void 0;
- const sat_1 = __webpack_require__(
- /*! sat */ "./node_modules/.pnpm/sat@0.9.0/node_modules/sat/SAT.js",
- );
- const intersect_1 = __webpack_require__(
- /*! ./intersect */ "./src/intersect.ts",
- );
- const model_1 = __webpack_require__(/*! ./model */ "./src/model.ts");
- const optimized_1 = __webpack_require__(
- /*! ./optimized */ "./src/optimized.ts",
- );
- /* helpers for faster getSATTest() and checkAInB() */
- const testMap = {
- satCircleCircle: sat_1.testCircleCircle,
- satCirclePolygon: sat_1.testCirclePolygon,
- satPolygonCircle: sat_1.testPolygonCircle,
- satPolygonPolygon: sat_1.testPolygonPolygon,
- inCircleCircle: intersect_1.circleInCircle,
- inCirclePolygon: intersect_1.circleInPolygon,
- inPolygonCircle: intersect_1.polygonInCircle,
- inPolygonPolygon: intersect_1.polygonInPolygon,
- };
- function createMap(bodyType, testType) {
- return Object.values(model_1.BodyType).reduce(
- (result, type) =>
- Object.assign(Object.assign({}, result), {
- [type]:
- type === model_1.BodyType.Circle
- ? testMap[`${testType}${bodyType}Circle`]
- : testMap[`${testType}${bodyType}Polygon`],
- }),
- {},
- );
- }
- const circleSATFunctions = createMap(model_1.BodyType.Circle, "sat");
- const circleInFunctions = createMap(model_1.BodyType.Circle, "in");
- const polygonSATFunctions = createMap(model_1.BodyType.Polygon, "sat");
- const polygonInFunctions = createMap(model_1.BodyType.Polygon, "in");
- exports.DEG2RAD = Math.PI / 180;
- exports.RAD2DEG = 180 / Math.PI;
- /**
- * convert from degrees to radians
- */
- function deg2rad(degrees) {
- return degrees * exports.DEG2RAD;
- }
- exports.deg2rad = deg2rad;
- /**
- * convert from radians to degrees
- */
- function rad2deg(radians) {
- return radians * exports.RAD2DEG;
- }
- exports.rad2deg = rad2deg;
- /**
- * creates ellipse-shaped polygon based on params
- */
- function createEllipse(radiusX, radiusY = radiusX, step = 1) {
- const steps = Math.PI * Math.hypot(radiusX, radiusY) * 2;
- const length = Math.max(8, Math.ceil(steps / Math.max(1, step)));
- const ellipse = [];
- for (let index = 0; index < length; index++) {
- const value = (index / length) * 2 * Math.PI;
- const x = Math.cos(value) * radiusX;
- const y = Math.sin(value) * radiusY;
- ellipse.push(new sat_1.Vector(x, y));
- }
- return ellipse;
- }
- exports.createEllipse = createEllipse;
- /**
- * creates box shaped polygon points
- */
- function createBox(width, height) {
- return [
- new sat_1.Vector(0, 0),
- new sat_1.Vector(width, 0),
- new sat_1.Vector(width, height),
- new sat_1.Vector(0, height),
- ];
- }
- exports.createBox = createBox;
- /**
- * ensure SATVector type point result
- */
- function ensureVectorPoint(point = {}) {
- return point instanceof sat_1.Vector
- ? point
- : new sat_1.Vector(point.x || 0, point.y || 0);
- }
- exports.ensureVectorPoint = ensureVectorPoint;
- /**
- * ensure Vector points (for polygon) in counter-clockwise order
- */
- function ensurePolygonPoints(points = []) {
- const polygonPoints = (0, optimized_1.map)(points, ensureVectorPoint);
- return clockwise(polygonPoints)
- ? polygonPoints.reverse()
- : polygonPoints;
- }
- exports.ensurePolygonPoints = ensurePolygonPoints;
- /**
- * get distance between two Vector points
- */
- function distance(bodyA, bodyB) {
- const xDiff = bodyA.x - bodyB.x;
- const yDiff = bodyA.y - bodyB.y;
- return Math.hypot(xDiff, yDiff);
- }
- exports.distance = distance;
- /**
- * check [is clockwise] direction of polygon
- */
- function clockwise(points) {
- const length = points.length;
- let sum = 0;
- (0, optimized_1.forEach)(points, (v1, index) => {
- const v2 = points[(index + 1) % length];
- sum += (v2.x - v1.x) * (v2.y + v1.y);
- });
- return sum > 0;
- }
- exports.clockwise = clockwise;
- /**
- * used for all types of bodies in constructor
- */
- function extendBody(body, options = {}) {
- body.isStatic = !!options.isStatic;
- body.isTrigger = !!options.isTrigger;
- body.padding = options.padding || 0;
- body.group =
- typeof options.group === "number" ? options.group : 0x7fffffff;
- if (body.typeGroup !== model_1.BodyGroup.Circle) {
- body.isCentered = options.isCentered || false;
- }
- body.setAngle(options.angle || 0);
- }
- exports.extendBody = extendBody;
- /**
- * check if body moved outside of its padding
- */
- function bodyMoved(body) {
- const { bbox, minX, minY, maxX, maxY } = body;
- return (
- bbox.minX < minX ||
- bbox.minY < minY ||
- bbox.maxX > maxX ||
- bbox.maxY > maxY
- );
- }
- exports.bodyMoved = bodyMoved;
- /**
- * returns true if two boxes not intersect
- */
- function notIntersectAABB(bodyA, bodyB) {
- return (
- bodyB.minX > bodyA.maxX ||
- bodyB.minY > bodyA.maxY ||
- bodyB.maxX < bodyA.minX ||
- bodyB.maxY < bodyA.minY
- );
- }
- exports.notIntersectAABB = notIntersectAABB;
- /**
- * checks if two boxes intersect
- */
- function intersectAABB(bodyA, bodyB) {
- return !notIntersectAABB(bodyA, bodyB);
- }
- exports.intersectAABB = intersectAABB;
- /**
- * checks if two bodies can interact (for collision filtering)
- */
- function canInteract(bodyA, bodyB) {
- return (
- ((bodyA.group >> 16) & (bodyB.group & 0xffff) &&
- (bodyB.group >> 16) & (bodyA.group & 0xffff)) !== 0
- );
- }
- exports.canInteract = canInteract;
- /**
- * checks if body a is in body b
- */
- function checkAInB(bodyA, bodyB) {
- const check =
- bodyA.typeGroup === model_1.BodyGroup.Circle
- ? circleInFunctions
- : polygonInFunctions;
- return check[bodyB.type](bodyA, bodyB);
- }
- exports.checkAInB = checkAInB;
- /**
- * clone sat vector points array into vector points array
- */
- function clonePointsArray(points) {
- return (0, optimized_1.map)(points, ({ x, y }) => ({ x, y }));
- }
- exports.clonePointsArray = clonePointsArray;
- /**
- * change format from SAT.js to poly-decomp
- */
- function mapVectorToArray({ x, y } = { x: 0, y: 0 }) {
- return [x, y];
- }
- exports.mapVectorToArray = mapVectorToArray;
- /**
- * change format from poly-decomp to SAT.js
- */
- function mapArrayToVector([x, y] = [0, 0]) {
- return { x, y };
- }
- exports.mapArrayToVector = mapArrayToVector;
- /**
- * given 2 bodies calculate vector of bounce assuming equal mass and they are circles
- */
- function getBounceDirection(body, collider) {
- const v2 = new sat_1.Vector(collider.x - body.x, collider.y - body.y);
- const v1 = new sat_1.Vector(body.x - collider.x, body.y - collider.y);
- const len = v1.dot(v2.normalize()) * 2;
- return new sat_1.Vector(
- v2.x * len - v1.x,
- v2.y * len - v1.y,
- ).normalize();
- }
- exports.getBounceDirection = getBounceDirection;
- /**
- * returns correct sat.js testing function based on body types
- */
- function getSATTest(bodyA, bodyB) {
- const check =
- bodyA.typeGroup === model_1.BodyGroup.Circle
- ? circleSATFunctions
- : polygonSATFunctions;
- return check[bodyB.type];
- }
- exports.getSATTest = getSATTest;
- /**
- * draws dashed line on canvas context
- */
- function dashLineTo(
- context,
- fromX,
- fromY,
- toX,
- toY,
- dash = 2,
- gap = 4,
- ) {
- const xDiff = toX - fromX;
- const yDiff = toY - fromY;
- const arc = Math.atan2(yDiff, xDiff);
- const offsetX = Math.cos(arc);
- const offsetY = Math.sin(arc);
- let posX = fromX;
- let posY = fromY;
- let dist = Math.hypot(xDiff, yDiff);
- while (dist > 0) {
- const step = Math.min(dist, dash);
- context.moveTo(posX, posY);
- context.lineTo(posX + offsetX * step, posY + offsetY * step);
- posX += offsetX * (dash + gap);
- posY += offsetY * (dash + gap);
- dist -= dash + gap;
- }
- }
- exports.dashLineTo = dashLineTo;
- /**
- * draw polygon
- */
- function drawPolygon(context, { pos, calcPoints }, isTrigger = false) {
- const lastPoint = calcPoints[calcPoints.length - 1];
- const fromX = pos.x + lastPoint.x;
- const fromY = pos.y + lastPoint.y;
- if (calcPoints.length === 1) {
- context.arc(fromX, fromY, 1, 0, Math.PI * 2);
- } else {
- context.moveTo(fromX, fromY);
- }
- (0, optimized_1.forEach)(calcPoints, (point, index) => {
- const toX = pos.x + point.x;
- const toY = pos.y + point.y;
- if (isTrigger) {
- const prev = calcPoints[index - 1] || lastPoint;
- dashLineTo(context, pos.x + prev.x, pos.y + prev.y, toX, toY);
- } else {
- context.lineTo(toX, toY);
+ collides(bbox) {
+ let node = this.data;
+
+ if (!intersects(bbox, node)) return false;
+
+ const nodesToSearch = [];
+ while (node) {
+ for (let i = 0; i < node.children.length; i++) {
+ const child = node.children[i];
+ const childBBox = node.leaf ? this.toBBox(child) : child;
+
+ if (intersects(bbox, childBBox)) {
+ if (node.leaf || contains(bbox, childBBox)) return true;
+ nodesToSearch.push(child);
+ }
}
- });
- }
- exports.drawPolygon = drawPolygon;
- /**
- * draw body bounding body box
- */
- function drawBVH(context, body) {
- drawPolygon(context, {
- pos: { x: body.minX, y: body.minY },
- calcPoints: createBox(body.maxX - body.minX, body.maxY - body.minY),
- });
- }
- exports.drawBVH = drawBVH;
- /**
- * clone response object returning new response with previous ones values
- */
- function cloneResponse(response) {
- const clone = new sat_1.Response();
- const { a, b, overlap, overlapN, overlapV, aInB, bInA } = response;
- clone.a = a;
- clone.b = b;
- clone.overlap = overlap;
- clone.overlapN = overlapN.clone();
- clone.overlapV = overlapV.clone();
- clone.aInB = aInB;
- clone.bInA = bInA;
- return clone;
- }
- exports.cloneResponse = cloneResponse;
- /**
- * dummy fn used as default, for optimization
- */
- function returnTrue() {
- return true;
- }
- exports.returnTrue = returnTrue;
- /**
- * for groups
- */
- function getGroup(group) {
- return Math.max(0, Math.min(group, 0x7fffffff));
+ node = nodesToSearch.pop();
}
- exports.getGroup = getGroup;
- /**
- * binary string to decimal number
- */
- function bin2dec(binary) {
- return Number(`0b${binary}`.replace(/\s/g, ""));
- }
- exports.bin2dec = bin2dec;
- /**
- * helper for groupBits()
- *
- * @param input - number or binary string
- */
- function ensureNumber(input) {
- return typeof input === "number" ? input : bin2dec(input);
- }
- exports.ensureNumber = ensureNumber;
- /**
- * create group bits from category and mask
- *
- * @param category - category bits
- * @param mask - mask bits (default: category)
- */
- function groupBits(category, mask = category) {
- return (ensureNumber(category) << 16) | ensureNumber(mask);
+
+ return false;
+ }
+
+ load(data) {
+ if (!(data && data.length)) return this;
+
+ if (data.length < this._minEntries) {
+ for (let i = 0; i < data.length; i++) {
+ this.insert(data[i]);
+ }
+ return this;
}
- exports.groupBits = groupBits;
- /***/
- },
+ // recursively build the tree with the given data from scratch using OMT algorithm
+ let node = this._build(data.slice(), 0, data.length - 1, 0);
- /***/ "./src/demo/canvas.js":
- /*!****************************!*\
- !*** ./src/demo/canvas.js ***!
- \****************************/
- /***/ (module) => {
- const width = window.innerWidth || 1024;
- const height = window.innerHeight || 768;
+ if (!this.data.children.length) {
+ // save as is if tree is empty
+ this.data = node;
- class TestCanvas {
- constructor(test) {
- this.test = test;
+ } else if (this.data.height === node.height) {
+ // split root if trees have the same height
+ this._splitRoot(this.data, node);
- this.element = document.createElement("div");
- this.element.id = "debug";
- this.element.innerHTML = `${this.test.legend}
-
-
- Show Bounding Volume Hierarchy
-
-
`;
+ } else {
+ if (this.data.height < node.height) {
+ // swap trees if inserted one is bigger
+ const tmpNode = this.data;
+ this.data = node;
+ node = tmpNode;
+ }
- this.canvas = document.createElement("canvas");
- this.canvas.width = width;
- this.canvas.height = height;
+ // insert the small tree into the large tree at appropriate level
+ this._insert(node, this.data.height - node.height - 1, true);
+ }
- this.context = this.canvas.getContext("2d");
- this.context.font = "24px Arial";
- this.test.context = this.context;
+ return this;
+ }
- this.bvhCheckbox = this.element.querySelector("#bvh");
+ insert(item) {
+ if (item) this._insert(item, this.data.height - 1);
+ return this;
+ }
- if (this.canvas instanceof Node) {
- this.element.appendChild(this.canvas);
- }
+ clear() {
+ this.data = createNode([]);
+ return this;
+ }
- this.fps = 0;
- this.frame = 0;
- this.started = Date.now();
+ remove(item, equalsFn) {
+ if (!item) return this;
- loop(this.update.bind(this));
- }
+ let node = this.data;
+ const bbox = this.toBBox(item);
+ const path = [];
+ const indexes = [];
+ let i, parent, goingUp;
- update() {
- this.frame++;
+ // depth-first iterative tree traversal
+ while (node || path.length) {
- const timeDiff = Date.now() - this.started;
- if (timeDiff >= 1000) {
- this.fps = this.frame / (timeDiff / 1000);
- this.frame = 0;
- this.started = Date.now();
+ if (!node) { // go up
+ node = path.pop();
+ parent = path[path.length - 1];
+ i = indexes.pop();
+ goingUp = true;
}
- // Clear the canvas
- this.context.fillStyle = "#000000";
- this.context.fillRect(0, 0, width, height);
-
- // Render the bodies
- this.context.strokeStyle = "#FFFFFF";
- this.context.beginPath();
- this.test.physics.draw(this.context);
- this.context.stroke();
-
- // Render the BVH
- if (this.bvhCheckbox.checked) {
- this.context.strokeStyle = "#00FF00";
- this.context.beginPath();
- this.test.physics.drawBVH(this.context);
- this.context.stroke();
+ if (node.leaf) { // check current node
+ const index = findItem(item, node.children, equalsFn);
+
+ if (index !== -1) {
+ // item found, remove the item and condense tree upwards
+ node.children.splice(index, 1);
+ path.push(node);
+ this._condense(path);
+ return this;
+ }
}
- // Render the FPS
- this.context.fillStyle = "#FFCC00";
- this.context.fillText(
- `FPS: ${this.fps ? this.fps.toFixed(0) : "?"}`,
- 24,
- 48,
- );
+ if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
+ path.push(node);
+ indexes.push(i);
+ i = 0;
+ parent = node;
+ node = node.children[0];
- if (this.test.drawCallback) {
- this.test.drawCallback();
- }
- }
- }
+ } else if (parent) { // go right
+ i++;
+ node = parent.children[i];
+ goingUp = false;
- function loop(callback) {
- // interval for fps instead of setTimeout
- // and ms = 1 which is lowest nonzero value
- // for responsiveness of user input
- setInterval(callback, 1);
+ } else node = null; // nothing found
}
- module.exports.TestCanvas = TestCanvas;
+ return this;
+ }
- module.exports.loop = loop;
+ toBBox(item) { return item; }
- module.exports.width = width;
+ compareMinX(a, b) { return a.minX - b.minX; }
+ compareMinY(a, b) { return a.minY - b.minY; }
- module.exports.height = height;
+ toJSON() { return this.data; }
- /***/
- },
+ fromJSON(data) {
+ this.data = data;
+ return this;
+ }
- /***/ "./src/demo/stress.js":
- /*!****************************!*\
- !*** ./src/demo/stress.js ***!
- \****************************/
- /***/ (module, __unused_webpack_exports, __webpack_require__) => {
- const { BodyGroup } = __webpack_require__(
- /*! ../model */ "./src/model.ts",
- );
- const { System } = __webpack_require__(
- /*! ../system */ "./src/system.ts",
- );
- const { getBounceDirection, groupBits } = __webpack_require__(
- /*! ../utils */ "./src/utils.ts",
- );
- const { width, height, loop } = __webpack_require__(
- /*! ./canvas */ "./src/demo/canvas.js",
- );
- const seededRandom = __webpack_require__(
- /*! random-seed */ "./node_modules/.pnpm/random-seed@0.3.0/node_modules/random-seed/index.js",
- ).create("@Prozi").random;
+ _all(node, result) {
+ const nodesToSearch = [];
+ while (node) {
+ if (node.leaf) result.push(...node.children);
+ else nodesToSearch.push(...node.children);
- function random(min, max) {
- return Math.floor(seededRandom() * max) + min;
+ node = nodesToSearch.pop();
}
+ return result;
+ }
- class Stress {
- constructor(count = 2000) {
- this.size = Math.sqrt((width * height) / (count * 50));
-
- this.physics = new System(5);
- this.bodies = [];
- this.polygons = 0;
- this.boxes = 0;
- this.circles = 0;
- this.ellipses = 0;
- this.lines = 0;
- this.lastVariant = 0;
- this.count = count;
- this.bounds = this.getBounds();
- this.enableFiltering = false;
-
- for (let i = 0; i < count; ++i) {
- this.createShape(!random(0, 20));
- }
+ _build(items, left, right, height) {
- this.legend = `Total: ${count}
- Polygons: ${this.polygons}
- Boxes: ${this.boxes}
- Circles: ${this.circles}
- Ellipses: ${this.ellipses}
- Lines: ${this.lines}
-
-
- Enable Collision Filtering
-
-
- `;
+ const N = right - left + 1;
+ let M = this._maxEntries;
+ let node;
- this.lastTime = Date.now();
- this.updateBody = this.updateBody.bind(this);
-
- // observer #debug & add filtering checkbox event
- const observer = new window.MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- mutation.addedNodes.forEach((node) => {
- if (node.id == "debug") {
- document
- .querySelector("#filtering")
- .addEventListener("change", () => this.toggleFiltering());
- observer.disconnect();
- }
- });
- });
- });
- observer.observe(document.querySelector("body"), {
- subtree: false,
- childList: true,
- });
+ if (N <= M) {
+ // reached leaf level; return leaf
+ node = createNode(items.slice(left, right + 1));
+ calcBBox(node, this.toBBox);
+ return node;
+ }
- this.start = () => {
- loop(this.update.bind(this));
- };
- }
+ if (!height) {
+ // target height of the bulk-loaded tree
+ height = Math.ceil(Math.log(N) / Math.log(M));
- getBounds() {
- return [
- this.physics.createBox({ x: 0, y: 0 }, width, 10, {
- isStatic: true,
- }),
- this.physics.createBox({ x: width - 10, y: 0 }, 10, height, {
- isStatic: true,
- }),
- this.physics.createBox({ x: 0, y: height - 10 }, width, 10, {
- isStatic: true,
- }),
- this.physics.createBox({ x: 0, y: 0 }, 10, height, {
- isStatic: true,
- }),
- ];
- }
+ // target number of root entries to maximize storage utilization
+ M = Math.ceil(N / Math.pow(M, height - 1));
+ }
- toggleFiltering() {
- this.enableFiltering = !this.enableFiltering;
- this.physics.clear();
- this.bodies.length = 0;
- this.polygons = 0;
- this.boxes = 0;
- this.circles = 0;
- this.ellipses = 0;
- this.lines = 0;
- this.lastVariant = 0;
- this.bounds = this.getBounds();
- for (let i = 0; i < this.count; ++i) {
- this.createShape(!random(0, 20));
- }
- }
+ node = createNode([]);
+ node.leaf = false;
+ node.height = height;
- update() {
- const now = Date.now();
- this.timeScale = Math.min(1000, now - this.lastTime) / 60;
- this.lastTime = now;
- this.bodies.forEach(this.updateBody);
- }
+ // split the items into M mostly square tiles
- updateBody(body) {
- body.setAngle(
- body.angle + body.rotationSpeed * this.timeScale,
- false,
- );
+ const N2 = Math.ceil(N / M);
+ const N1 = N2 * Math.ceil(Math.sqrt(M));
- if (seededRandom() < 0.05 * this.timeScale) {
- body.targetScale.x = 0.5 + seededRandom();
- }
+ multiSelect(items, left, right, N1, this.compareMinX);
- if (seededRandom() < 0.05 * this.timeScale) {
- body.targetScale.y = 0.5 + seededRandom();
- }
+ for (let i = left; i <= right; i += N1) {
- if (Math.abs(body.targetScale.x - body.scaleX) > 0.01) {
- const scaleX =
- body.scaleX +
- Math.sign(body.targetScale.x - body.scaleX) *
- 0.02 *
- this.timeScale;
- const scaleY =
- body.scaleY +
- Math.sign(body.targetScale.y - body.scaleY) *
- 0.02 *
- this.timeScale;
-
- body.setScale(scaleX, scaleY, false);
- }
+ const right2 = Math.min(i + N1 - 1, right);
- // as last step update position, and bounding box
- body.setPosition(
- body.x + body.directionX * this.timeScale,
- body.y + body.directionY * this.timeScale,
- );
+ multiSelect(items, i, right2, N2, this.compareMinY);
- // separate + bounce
- this.bounceBody(body);
- }
+ for (let j = i; j <= right2; j += N2) {
- bounceBody(body) {
- const bounces = { x: 0, y: 0 };
- const addBounces = ({ overlapV: { x, y } }) => {
- bounces.x += x;
- bounces.y += y;
- };
+ const right3 = Math.min(j + N2 - 1, right2);
- this.physics.checkOne(body, addBounces);
+ // pack each entry recursively
+ node.children.push(this._build(items, j, right3, height - 1));
+ }
+ }
- if (bounces.x || bounces.y) {
- const size = 0.5 * (body.scaleX + body.scaleY);
- const bounce = getBounceDirection(body, {
- x: body.x + bounces.x,
- y: body.y + bounces.y,
- });
+ calcBBox(node, this.toBBox);
- bounce.scale(body.size).add({
- x: body.directionX * size,
- y: body.directionY * size,
- });
+ return node;
+ }
- const { x, y } = bounce.normalize();
+ _chooseSubtree(bbox, node, level, path) {
+ while (true) {
+ path.push(node);
- body.directionX = x;
- body.directionY = y;
- body.rotationSpeed = (seededRandom() - seededRandom()) * 0.1;
+ if (node.leaf || path.length - 1 === level) break;
- body.setPosition(body.x - bounces.x, body.y - bounces.y);
- }
- }
+ let minArea = Infinity;
+ let minEnlargement = Infinity;
+ let targetNode;
- createShape(large) {
- const minSize = this.size * 1.0 * (large ? seededRandom() + 1 : 1);
- const maxSize =
- this.size * 1.25 * (large ? seededRandom() * 2 + 1 : 1);
- const x = random(0, width);
- const y = random(0, height);
- const direction = (random(0, 360) * Math.PI) / 180;
- const options = {
- isCentered: true,
- padding: (minSize + maxSize) * 0.2,
- };
-
- let body;
- let variant = this.lastVariant++ % 5;
-
- switch (variant) {
- case 0:
- if (this.enableFiltering) {
- options.group = groupBits(BodyGroup.Circle);
- }
- body = this.physics.createCircle(
- { x, y },
- random(minSize, maxSize) / 2,
- options,
- );
-
- ++this.circles;
- break;
-
- case 1:
- const width = random(minSize, maxSize);
- const height = random(minSize, maxSize);
- if (this.enableFiltering) {
- options.group = groupBits(BodyGroup.Ellipse);
- console.log();
- }
- body = this.physics.createEllipse(
- { x, y },
- width,
- height,
- 2,
- options,
- );
-
- ++this.ellipses;
- break;
-
- case 2:
- if (this.enableFiltering) {
- options.group = groupBits(BodyGroup.Box);
- }
- body = this.physics.createBox(
- { x, y },
- random(minSize, maxSize),
- random(minSize, maxSize),
- options,
- );
-
- ++this.boxes;
- break;
-
- case 3:
- if (this.enableFiltering) {
- options.group = groupBits(BodyGroup.Line);
- }
- body = this.physics.createLine(
- { x, y },
- {
- x: x + random(minSize, maxSize),
- y: y + random(minSize, maxSize),
- },
- options,
- );
-
- ++this.lines;
- break;
-
- default:
- if (this.enableFiltering) {
- options.group = groupBits(BodyGroup.Polygon);
+ for (let i = 0; i < node.children.length; i++) {
+ const child = node.children[i];
+ const area = bboxArea(child);
+ const enlargement = enlargedArea(bbox, child) - area;
+
+ // choose entry with the least area enlargement
+ if (enlargement < minEnlargement) {
+ minEnlargement = enlargement;
+ minArea = area < minArea ? area : minArea;
+ targetNode = child;
+
+ } else if (enlargement === minEnlargement) {
+ // otherwise choose one with the smallest area
+ if (area < minArea) {
+ minArea = area;
+ targetNode = child;
+ }
}
- body = this.physics.createPolygon(
- { x, y },
- [
- {
- x: -random(minSize, maxSize),
- y: random(minSize, maxSize),
- },
- {
- x: random(minSize, maxSize),
- y: random(minSize, maxSize),
- },
- {
- x: random(minSize, maxSize),
- y: -random(minSize, maxSize),
- },
- {
- x: -random(minSize, maxSize),
- y: -random(minSize, maxSize),
- },
- ],
- options,
- );
-
- ++this.polygons;
- break;
}
- // set initial rotation angle direction
- body.rotationSpeed = (seededRandom() - seededRandom()) * 0.1;
- body.setAngle((random(0, 360) * Math.PI) / 180);
+ node = targetNode || node.children[0];
+ }
+
+ return node;
+ }
+
+ _insert(item, level, isNode) {
+ const bbox = isNode ? item : this.toBBox(item);
+ const insertPath = [];
- body.targetScale = { x: 1, y: 1 };
- body.size = (minSize + maxSize) / 2;
+ // find the best node for accommodating the item, saving all nodes along the path too
+ const node = this._chooseSubtree(bbox, this.data, level, insertPath);
- body.directionX = Math.cos(direction);
- body.directionY = Math.sin(direction);
+ // put the item into the node
+ node.children.push(item);
+ extend(node, bbox);
- this.bodies.push(body);
- }
+ // split on node overflow; propagate upwards if necessary
+ while (level >= 0) {
+ if (insertPath[level].children.length > this._maxEntries) {
+ this._split(insertPath, level);
+ level--;
+ } else break;
}
- module.exports = Stress;
+ // adjust bboxes along the insertion path
+ this._adjustParentBBoxes(bbox, insertPath, level);
+ }
- /***/
- },
+ // split overflowed node into two
+ _split(insertPath, level) {
+ const node = insertPath[level];
+ const M = node.children.length;
+ const m = this._minEntries;
- /***/ "./src/demo/tank.js":
- /*!**************************!*\
- !*** ./src/demo/tank.js ***!
- \**************************/
- /***/ (module, __unused_webpack_exports, __webpack_require__) => {
- const { BodyGroup } = __webpack_require__(
- /*! ../model */ "./src/model.ts",
- );
- const { System } = __webpack_require__(
- /*! ../system */ "./src/system.ts",
- );
- const { mapVectorToArray } = __webpack_require__(
- /*! ../utils */ "./src/utils.ts",
- );
- const { width, height, loop } = __webpack_require__(
- /*! ./canvas */ "./src/demo/canvas.js",
- );
+ this._chooseSplitAxis(node, m, M);
- class Tank {
- constructor() {
- this.physics = new System();
- this.bodies = [];
- this.player = this.createPlayer(400, 300);
-
- this.createPolygon(
- 300,
- 300,
- [
- { x: -11.25, y: -6.76 },
- { x: -12.5, y: -6.76 },
- { x: -12.5, y: 6.75 },
- { x: -3.1, y: 6.75 },
- { x: -3.1, y: 0.41 },
- { x: -2.35, y: 0.41 },
- { x: -2.35, y: 6.75 },
- { x: 0.77, y: 6.75 },
- { x: 0.77, y: 7.5 },
- { x: -13.25, y: 7.5 },
- { x: -13.25, y: -7.51 },
- { x: -11.25, y: -7.51 },
- ]
- .map(mapVectorToArray)
- .map(([x, y]) => [x * 10, y * 10]),
- );
-
- this.up = false;
- this.down = false;
- this.left = false;
- this.right = false;
-
- this.legend = `W, S - Accelerate/Decelerate
- A, D - Turn
`;
+ const splitIndex = this._chooseSplitIndex(node, m, M);
- const updateKeys = ({ type, key }) => {
- const keyDown = type === "keydown";
- const keyLowerCase = key.toLowerCase();
+ const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
+ newNode.height = node.height;
+ newNode.leaf = node.leaf;
- keyLowerCase === "w" && (this.up = keyDown);
- keyLowerCase === "s" && (this.down = keyDown);
- keyLowerCase === "a" && (this.left = keyDown);
- keyLowerCase === "d" && (this.right = keyDown);
- };
+ calcBBox(node, this.toBBox);
+ calcBBox(newNode, this.toBBox);
- document.addEventListener("keydown", updateKeys);
- document.addEventListener("keyup", updateKeys);
+ if (level) insertPath[level - 1].children.push(newNode);
+ else this._splitRoot(node, newNode);
+ }
- if (this.canvas instanceof Node) {
- this.element.appendChild(this.canvas);
- }
+ _splitRoot(node, newNode) {
+ // split root node
+ this.data = createNode([node, newNode]);
+ this.data.height = node.height + 1;
+ this.data.leaf = false;
+ calcBBox(this.data, this.toBBox);
+ }
- this.createMap();
- this.lastTime = Date.now();
+ _chooseSplitIndex(node, m, M) {
+ let index;
+ let minOverlap = Infinity;
+ let minArea = Infinity;
- this.start = () => {
- loop(this.update.bind(this));
- };
- }
+ for (let i = m; i <= M - m; i++) {
+ const bbox1 = distBBox(node, 0, i, this.toBBox);
+ const bbox2 = distBBox(node, i, M, this.toBBox);
- update() {
- const now = Date.now();
- this.timeScale = Math.min(1000, now - this.lastTime) / 60;
- this.lastTime = now;
- this.handleInput();
- this.processGameLogic();
- this.handleCollisions();
- this.updateTurret();
- }
+ const overlap = intersectionArea(bbox1, bbox2);
+ const area = bboxArea(bbox1) + bboxArea(bbox2);
- handleInput() {
- if (this.up) {
- this.player.velocity += 0.2 * this.timeScale;
- }
+ // choose distribution with minimum overlap
+ if (overlap < minOverlap) {
+ minOverlap = overlap;
+ index = i;
- if (this.down) {
- this.player.velocity -= 0.2 * this.timeScale;
- }
+ minArea = area < minArea ? area : minArea;
- if (this.left) {
- this.player.setAngle(this.player.angle - 0.2 * this.timeScale);
+ } else if (overlap === minOverlap) {
+ // otherwise choose distribution with minimum area
+ if (area < minArea) {
+ minArea = area;
+ index = i;
+ }
}
+ }
- if (this.right) {
- this.player.setAngle(this.player.angle + 0.2 * this.timeScale);
- }
- }
+ return index || M - m;
+ }
- processGameLogic() {
- const x = Math.cos(this.player.angle);
- const y = Math.sin(this.player.angle);
-
- if (this.player.velocity > 0) {
- this.player.velocity = Math.max(
- this.player.velocity - 0.1 * this.timeScale,
- 0,
- );
-
- if (this.player.velocity > 2) {
- this.player.velocity = 2;
- }
- } else if (this.player.velocity < 0) {
- this.player.velocity = Math.min(
- this.player.velocity + 0.1 * this.timeScale,
- 0,
- );
-
- if (this.player.velocity < -2) {
- this.player.velocity = -2;
- }
- }
+ // sorts node children by the best axis for split
+ _chooseSplitAxis(node, m, M) {
+ const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
+ const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
+ const xMargin = this._allDistMargin(node, m, M, compareMinX);
+ const yMargin = this._allDistMargin(node, m, M, compareMinY);
- if (!Math.round(this.player.velocity * 100)) {
- this.player.velocity = 0;
- }
+ // if total distributions margin value is minimal for x, sort by minX,
+ // otherwise it's already sorted by minY
+ if (xMargin < yMargin) node.children.sort(compareMinX);
+ }
- if (this.player.velocity) {
- this.player.setPosition(
- this.player.x + x * this.player.velocity,
- this.player.y + y * this.player.velocity,
- );
- }
- }
+ // total margin of all possible split distributions where each node is at least m full
+ _allDistMargin(node, m, M, compare) {
+ node.children.sort(compare);
- handleCollisions() {
- this.physics.checkAll(({ a, b, overlapV }) => {
- if (a.isTrigger || b.isTrigger) {
- return;
- }
+ const toBBox = this.toBBox;
+ const leftBBox = distBBox(node, 0, m, toBBox);
+ const rightBBox = distBBox(node, M - m, M, toBBox);
+ let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
- if (a.typeGroup === BodyGroup.Polygon || a === this.player) {
- a.setPosition(a.pos.x - overlapV.x, a.pos.y - overlapV.y);
- }
+ for (let i = m; i < M - m; i++) {
+ const child = node.children[i];
+ extend(leftBBox, node.leaf ? toBBox(child) : child);
+ margin += bboxMargin(leftBBox);
+ }
- if (b.typeGroup === BodyGroup.Circle || b === this.player) {
- b.setPosition(b.pos.x + overlapV.x, b.pos.y + overlapV.y);
- }
+ for (let i = M - m - 1; i >= m; i--) {
+ const child = node.children[i];
+ extend(rightBBox, node.leaf ? toBBox(child) : child);
+ margin += bboxMargin(rightBBox);
+ }
- if (a === this.player) {
- a.velocity *= 0.9;
- }
- });
- }
+ return margin;
+ }
- updateTurret() {
- this.playerTurret.setAngle(this.player.angle, false);
- this.playerTurret.setPosition(this.player.x, this.player.y);
-
- const hit = this.physics.raycast(
- this.playerTurret.start,
- this.playerTurret.end,
- (test) => test !== this.player,
- );
-
- this.drawCallback = () => {
- if (hit) {
- this.context.strokeStyle = "#FF0000";
- this.context.beginPath();
- this.context.arc(hit.point.x, hit.point.y, 5, 0, 2 * Math.PI);
- this.context.stroke();
- }
- };
- }
+ _adjustParentBBoxes(bbox, path, level) {
+ // adjust bboxes along the given tree path
+ for (let i = level; i >= 0; i--) {
+ extend(path[i], bbox);
+ }
+ }
- createPlayer(x, y, size = 13) {
- const player =
- Math.random() < 0.5
- ? this.physics.createCircle(
- { x: this.scaleX(x), y: this.scaleY(y) },
- this.scaleX(size / 2),
- { isCentered: true },
- )
- : this.physics.createBox(
- {
- x: this.scaleX(x - size / 2),
- y: this.scaleY(y - size / 2),
- },
- this.scaleX(size),
- this.scaleX(size),
- { isCentered: true },
- );
-
- player.velocity = 0;
- player.setOffset({ x: -this.scaleX(size / 2), y: 0 });
- player.setAngle(0.2);
-
- this.physics.updateBody(player);
- this.playerTurret = this.physics.createLine(
- player,
- { x: player.x + this.scaleX(20) + this.scaleY(20), y: player.y },
- { angle: 0.2, isTrigger: true },
- );
-
- return player;
- }
+ _condense(path) {
+ // go through the path, removing empty nodes and updating bboxes
+ for (let i = path.length - 1, siblings; i >= 0; i--) {
+ if (path[i].children.length === 0) {
+ if (i > 0) {
+ siblings = path[i - 1].children;
+ siblings.splice(siblings.indexOf(path[i]), 1);
- scaleX(x) {
- return (x / 800) * width;
- }
+ } else this.clear();
- scaleY(y) {
- return (y / 600) * height;
- }
+ } else calcBBox(path[i], this.toBBox);
+ }
+ }
+}
- createCircle(x, y, radius) {
- this.physics.createCircle(
- { x: this.scaleX(x), y: this.scaleY(y) },
- this.scaleX(radius),
- );
- }
+function findItem(item, items, equalsFn) {
+ if (!equalsFn) return items.indexOf(item);
- createEllipse(x, y, radiusX, radiusY, step, angle) {
- this.physics.createEllipse(
- { x: this.scaleX(x), y: this.scaleY(y) },
- this.scaleX(radiusX),
- this.scaleY(radiusY),
- step,
- { angle },
- );
- }
+ for (let i = 0; i < items.length; i++) {
+ if (equalsFn(item, items[i])) return i;
+ }
+ return -1;
+}
+
+// calculate node's bbox from bboxes of its children
+function calcBBox(node, toBBox) {
+ distBBox(node, 0, node.children.length, toBBox, node);
+}
+
+// min bounding rectangle of node children from k to p-1
+function distBBox(node, k, p, toBBox, destNode) {
+ if (!destNode) destNode = createNode(null);
+ destNode.minX = Infinity;
+ destNode.minY = Infinity;
+ destNode.maxX = -Infinity;
+ destNode.maxY = -Infinity;
+
+ for (let i = k; i < p; i++) {
+ const child = node.children[i];
+ extend(destNode, node.leaf ? toBBox(child) : child);
+ }
- createPolygon(x, y, points, angle) {
- const scaledPoints = points.map(([pointX, pointY]) => ({
- x: this.scaleX(pointX),
- y: this.scaleY(pointY),
- }));
-
- return this.physics.createPolygon(
- { x: this.scaleX(x), y: this.scaleY(y) },
- scaledPoints,
- { angle },
- );
- }
+ return destNode;
+}
+
+function extend(a, b) {
+ a.minX = Math.min(a.minX, b.minX);
+ a.minY = Math.min(a.minY, b.minY);
+ a.maxX = Math.max(a.maxX, b.maxX);
+ a.maxY = Math.max(a.maxY, b.maxY);
+ return a;
+}
+
+function compareNodeMinX(a, b) { return a.minX - b.minX; }
+function compareNodeMinY(a, b) { return a.minY - b.minY; }
+
+function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
+function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
+
+function enlargedArea(a, b) {
+ return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
+ (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
+}
+
+function intersectionArea(a, b) {
+ const minX = Math.max(a.minX, b.minX);
+ const minY = Math.max(a.minY, b.minY);
+ const maxX = Math.min(a.maxX, b.maxX);
+ const maxY = Math.min(a.maxY, b.maxY);
+
+ return Math.max(0, maxX - minX) *
+ Math.max(0, maxY - minY);
+}
+
+function contains(a, b) {
+ return a.minX <= b.minX &&
+ a.minY <= b.minY &&
+ b.maxX <= a.maxX &&
+ b.maxY <= a.maxY;
+}
+
+function intersects(a, b) {
+ return b.minX <= a.maxX &&
+ b.minY <= a.maxY &&
+ b.maxX >= a.minX &&
+ b.maxY >= a.minY;
+}
+
+function createNode(children) {
+ return {
+ children,
+ height: 1,
+ leaf: true,
+ minX: Infinity,
+ minY: Infinity,
+ maxX: -Infinity,
+ maxY: -Infinity
+ };
+}
- createMap(width = 800, height = 600) {
- // World bounds
- // World bounds
- this.createPolygon(0, 0, [
- [0, 0],
- [width, 0],
- ]);
- this.createPolygon(0, 0, [
- [width, 0],
- [width, height],
- ]);
- this.createPolygon(0, 0, [
- [width, height],
- [0, height],
- ]);
- this.createPolygon(0, 0, [
- [0, height],
- [0, 0],
- ]);
-
- // Factory
- this.createPolygon(
- 100,
- 100,
- [
- [-50, -50],
- [50, -50],
- [50, 50],
- [-50, 50],
- ],
- 0.4,
- );
- this.createPolygon(
- 190,
- 105,
- [
- [-20, -20],
- [20, -20],
- [20, 20],
- [-20, 20],
- ],
- 0.4,
- );
- this.createCircle(170, 140, 6);
- this.createCircle(185, 155, 6);
- this.createCircle(165, 165, 6);
- this.createCircle(145, 165, 6);
-
- // Airstrip
- this.createPolygon(
- 230,
- 50,
- [
- [-150, -30],
- [150, -30],
- [150, 30],
- [-150, 30],
- ],
- 0.4,
- );
-
- // HQ
- this.createPolygon(
- 100,
- 500,
- [
- [-40, -50],
- [40, -50],
- [50, 50],
- [-50, 50],
- ],
- 0.2,
- );
- this.createCircle(180, 490, 12);
- this.createCircle(175, 540, 12);
-
- // Barracks
- this.createPolygon(
- 400,
- 500,
- [
- [-60, -20],
- [60, -20],
- [60, 20],
- [-60, 20],
- ],
- 1.7,
- );
- this.createPolygon(
- 350,
- 494,
- [
- [-60, -20],
- [60, -20],
- [60, 20],
- [-60, 20],
- ],
- 1.7,
- );
-
- // Mountains
- this.createPolygon(750, 0, [
- [0, 0],
- [-20, 100],
- ]);
- this.createPolygon(750, 0, [
- [-20, 100],
- [30, 250],
- ]);
- this.createPolygon(750, 0, [
- [30, 250],
- [20, 300],
- ]);
- this.createPolygon(750, 0, [
- [20, 300],
- [-50, 320],
- ]);
- this.createPolygon(750, 0, [
- [-50, 320],
- [-90, 500],
- ]);
- this.createPolygon(750, 0, [
- [-90, 500],
- [-200, 600],
- ]);
-
- // Lake
- this.createEllipse(530, 130, 80, 70, 10, -0.2);
- }
- }
+// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
+// combines selection algorithm with binary divide & conquer approach
- module.exports = Tank;
+function multiSelect(arr, left, right, n, compare) {
+ const stack = [left, right];
- /***/
- },
+ while (stack.length) {
+ right = stack.pop();
+ left = stack.pop();
- /******/
- };
- /************************************************************************/
- /******/ // The module cache
- /******/ var __webpack_module_cache__ = {};
- /******/
- /******/ // The require function
- /******/ function __webpack_require__(moduleId) {
- /******/ // Check if module is in cache
- /******/ var cachedModule = __webpack_module_cache__[moduleId];
- /******/ if (cachedModule !== undefined) {
- /******/ return cachedModule.exports;
- /******/
- }
- /******/ // Create a new module (and put it into the cache)
- /******/ var module = (__webpack_module_cache__[moduleId] = {
- /******/ // no module.id needed
- /******/ // no module.loaded needed
- /******/ exports: {},
- /******/
- });
- /******/
- /******/ // Execute the module function
- /******/ __webpack_modules__[moduleId].call(
- module.exports,
- module,
- module.exports,
- __webpack_require__,
- );
- /******/
- /******/ // Return the exports of the module
- /******/ return module.exports;
- /******/
- }
- /******/
- /************************************************************************/
- /******/ /* webpack/runtime/define property getters */
- /******/ (() => {
- /******/ // define getter functions for harmony exports
- /******/ __webpack_require__.d = (exports, definition) => {
- /******/ for (var key in definition) {
- /******/ if (
- __webpack_require__.o(definition, key) &&
- !__webpack_require__.o(exports, key)
- ) {
- /******/ Object.defineProperty(exports, key, {
- enumerable: true,
- get: definition[key],
- });
- /******/
- }
- /******/
- }
- /******/
- };
- /******/
- })();
- /******/
- /******/ /* webpack/runtime/hasOwnProperty shorthand */
- /******/ (() => {
- /******/ __webpack_require__.o = (obj, prop) =>
- Object.prototype.hasOwnProperty.call(obj, prop);
- /******/
- })();
- /******/
- /******/ /* webpack/runtime/make namespace object */
- /******/ (() => {
- /******/ // define __esModule on exports
- /******/ __webpack_require__.r = (exports) => {
- /******/ if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
- /******/ Object.defineProperty(exports, Symbol.toStringTag, {
- value: "Module",
- });
- /******/
- }
- /******/ Object.defineProperty(exports, "__esModule", { value: true });
- /******/
- };
- /******/
- })();
- /******/
- /************************************************************************/
- var __webpack_exports__ = {};
- /*!***************************!*\
+ if (right - left <= n) continue;
+
+ const mid = left + Math.ceil((right - left) / n / 2) * n;
+ (0,quickselect__WEBPACK_IMPORTED_MODULE_0__["default"])(arr, mid, left, right, compare);
+
+ stack.push(left, mid, mid, right);
+ }
+}
+
+
+/***/ })
+
+/******/ });
+/************************************************************************/
+/******/ // The module cache
+/******/ var __webpack_module_cache__ = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/ // Check if module is in cache
+/******/ var cachedModule = __webpack_module_cache__[moduleId];
+/******/ if (cachedModule !== undefined) {
+/******/ return cachedModule.exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = __webpack_module_cache__[moduleId] = {
+/******/ // no module.id needed
+/******/ // no module.loaded needed
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/************************************************************************/
+/******/ /* webpack/runtime/define property getters */
+/******/ (() => {
+/******/ // define getter functions for harmony exports
+/******/ __webpack_require__.d = (exports, definition) => {
+/******/ for(var key in definition) {
+/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
+/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
+/******/ }
+/******/ }
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/hasOwnProperty shorthand */
+/******/ (() => {
+/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
+/******/ })();
+/******/
+/******/ /* webpack/runtime/make namespace object */
+/******/ (() => {
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = (exports) => {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/ })();
+/******/
+/************************************************************************/
+var __webpack_exports__ = {};
+/*!***************************!*\
!*** ./src/demo/index.js ***!
\***************************/
- const { TestCanvas } = __webpack_require__(
- /*! ./canvas */ "./src/demo/canvas.js",
- );
+const { TestCanvas } = __webpack_require__(/*! ./canvas */ "./src/demo/canvas.js");
- const isStressTest = window.location.search.indexOf("?stress") !== -1;
- const Test = isStressTest
- ? __webpack_require__(/*! ./stress */ "./src/demo/stress.js")
- : __webpack_require__(/*! ./tank */ "./src/demo/tank.js");
+const isStressTest = window.location.search.indexOf("?stress") !== -1;
+const Test = isStressTest ? __webpack_require__(/*! ./stress */ "./src/demo/stress.js") : __webpack_require__(/*! ./tank */ "./src/demo/tank.js");
- const test = new Test();
- const canvas = new TestCanvas(test);
+const test = new Test();
+const canvas = new TestCanvas(test);
- document.body.appendChild(canvas.element);
+document.body.appendChild(canvas.element);
- if (test.start) {
- test.start();
- }
+if (test.start) {
+ test.start();
+}
- /******/
-})();
+/******/ })()
+;
\ No newline at end of file
diff --git a/docs/demo/index.html b/docs/demo/index.html
index ab476592..26b2181b 100644
--- a/docs/demo/index.html
+++ b/docs/demo/index.html
@@ -50,7 +50,5 @@
-
-
-
+
collider - box