UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

413 lines (349 loc) 16.5 kB
/** * The `Matter.Bodies` module contains factory methods for creating rigid body models * with commonly used body configurations (such as rectangles, circles and other polygons). * * See the included usage [examples](https://github.com/liabru/matter-js/tree/master/examples). * * @class Bodies */ // TODO: true circle bodies var Bodies = {}; module.exports = Bodies; var Vertices = require('../geometry/Vertices'); var Common = require('../core/Common'); var Body = require('../body/Body'); var Bounds = require('../geometry/Bounds'); var Vector = require('../geometry/Vector'); (function() { /** * Creates a new rigid body model with a rectangle hull. * The options parameter is an object that specifies any properties you wish to override the defaults. * See the properties section of the `Matter.Body` module for detailed information on what you can pass via the `options` object. * @method rectangle * @param {number} x * @param {number} y * @param {number} width * @param {number} height * @param {object} [options] * @return {body} A new rectangle body */ Bodies.rectangle = function(x, y, width, height, options) { options = options || {}; var rectangle = { label: 'Rectangle Body', position: { x: x, y: y }, vertices: Vertices.fromPath('L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height) }; if (options.chamfer) { var chamfer = options.chamfer; rectangle.vertices = Vertices.chamfer(rectangle.vertices, chamfer.radius, chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); delete options.chamfer; } return Body.create(Common.extend({}, rectangle, options)); }; /** * Creates a new rigid body model with a trapezoid hull. * The `slope` is parameterised as a fraction of `width` and must be < 1 to form a valid trapezoid. * The options parameter is an object that specifies any properties you wish to override the defaults. * See the properties section of the `Matter.Body` module for detailed information on what you can pass via the `options` object. * @method trapezoid * @param {number} x * @param {number} y * @param {number} width * @param {number} height * @param {number} slope Must be a number < 1. * @param {object} [options] * @return {body} A new trapezoid body */ Bodies.trapezoid = function(x, y, width, height, slope, options) { options = options || {}; if (slope >= 1) { Common.warn('Bodies.trapezoid: slope parameter must be < 1.'); } slope *= 0.5; var roof = (1 - (slope * 2)) * width; var x1 = width * slope, x2 = x1 + roof, x3 = x2 + x1, verticesPath; if (slope < 0.5) { verticesPath = 'L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0'; } else { verticesPath = 'L 0 0 L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0'; } var trapezoid = { label: 'Trapezoid Body', position: { x: x, y: y }, vertices: Vertices.fromPath(verticesPath) }; if (options.chamfer) { var chamfer = options.chamfer; trapezoid.vertices = Vertices.chamfer(trapezoid.vertices, chamfer.radius, chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); delete options.chamfer; } return Body.create(Common.extend({}, trapezoid, options)); }; /** * Creates a new rigid body model with a circle hull. * The options parameter is an object that specifies any properties you wish to override the defaults. * See the properties section of the `Matter.Body` module for detailed information on what you can pass via the `options` object. * @method circle * @param {number} x * @param {number} y * @param {number} radius * @param {object} [options] * @param {number} [maxSides] * @return {body} A new circle body */ Bodies.circle = function(x, y, radius, options, maxSides) { options = options || {}; var circle = { label: 'Circle Body', circleRadius: radius }; // approximate circles with polygons until true circles implemented in SAT maxSides = maxSides || 25; var sides = Math.ceil(Math.max(10, Math.min(maxSides, radius))); // optimisation: always use even number of sides (half the number of unique axes) if (sides % 2 === 1) sides += 1; return Bodies.polygon(x, y, sides, radius, Common.extend({}, circle, options)); }; /** * Creates a new rigid body model with a regular polygon hull with the given number of sides. * The options parameter is an object that specifies any properties you wish to override the defaults. * See the properties section of the `Matter.Body` module for detailed information on what you can pass via the `options` object. * @method polygon * @param {number} x * @param {number} y * @param {number} sides * @param {number} radius * @param {object} [options] * @return {body} A new regular polygon body */ Bodies.polygon = function(x, y, sides, radius, options) { options = options || {}; if (sides < 3) return Bodies.circle(x, y, radius, options); var theta = 2 * Math.PI / sides, path = '', offset = theta * 0.5; for (var i = 0; i < sides; i += 1) { var angle = offset + (i * theta), xx = Math.cos(angle) * radius, yy = Math.sin(angle) * radius; path += 'L ' + xx.toFixed(3) + ' ' + yy.toFixed(3) + ' '; } var polygon = { label: 'Polygon Body', position: { x: x, y: y }, vertices: Vertices.fromPath(path) }; if (options.chamfer) { var chamfer = options.chamfer; polygon.vertices = Vertices.chamfer(polygon.vertices, chamfer.radius, chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); delete options.chamfer; } return Body.create(Common.extend({}, polygon, options)); }; /** * Utility to create a compound body based on set(s) of vertices. * * _Note:_ To optionally enable automatic concave vertices decomposition the [poly-decomp](https://github.com/schteppe/poly-decomp.js) * package must be first installed and provided see `Common.setDecomp`, otherwise the convex hull of each vertex set will be used. * * The resulting vertices are reorientated about their centre of mass, * and offset such that `body.position` corresponds to this point. * * The resulting offset may be found if needed by subtracting `body.bounds` from the original input bounds. * To later move the centre of mass see `Body.setCentre`. * * Note that automatic conconcave decomposition results are not always optimal. * For best results, simplify the input vertices as much as possible first. * By default this function applies some addtional simplification to help. * * Some outputs may also require further manual processing afterwards to be robust. * In particular some parts may need to be overlapped to avoid collision gaps. * Thin parts and sharp points should be avoided or removed where possible. * * The options parameter object specifies any `Matter.Body` properties you wish to override the defaults. * * See the properties section of the `Matter.Body` module for detailed information on what you can pass via the `options` object. * @method fromVertices * @param {number} x * @param {number} y * @param {array} vertexSets One or more arrays of vertex points e.g. `[[{ x: 0, y: 0 }...], ...]`. * @param {object} [options] The body options. * @param {bool} [flagInternal=false] Optionally marks internal edges with `isInternal`. * @param {number} [removeCollinear=0.01] Threshold when simplifying vertices along the same edge. * @param {number} [minimumArea=10] Threshold when removing small parts. * @param {number} [removeDuplicatePoints=0.01] Threshold when simplifying nearby vertices. * @return {body} */ Bodies.fromVertices = function(x, y, vertexSets, options, flagInternal, removeCollinear, minimumArea, removeDuplicatePoints) { var decomp = Common.getDecomp(), canDecomp, body, parts, isConvex, isConcave, vertices, i, j, k, v, z; // check decomp is as expected canDecomp = Boolean(decomp && decomp.quickDecomp); options = options || {}; parts = []; flagInternal = typeof flagInternal !== 'undefined' ? flagInternal : false; removeCollinear = typeof removeCollinear !== 'undefined' ? removeCollinear : 0.01; minimumArea = typeof minimumArea !== 'undefined' ? minimumArea : 10; removeDuplicatePoints = typeof removeDuplicatePoints !== 'undefined' ? removeDuplicatePoints : 0.01; // ensure vertexSets is an array of arrays if (!Common.isArray(vertexSets[0])) { vertexSets = [vertexSets]; } for (v = 0; v < vertexSets.length; v += 1) { vertices = vertexSets[v]; isConvex = Vertices.isConvex(vertices); isConcave = !isConvex; if (isConcave && !canDecomp) { Common.warnOnce( 'Bodies.fromVertices: Install the \'poly-decomp\' library and use Common.setDecomp or provide \'decomp\' as a global to decompose concave vertices.' ); } if (isConvex || !canDecomp) { if (isConvex) { vertices = Vertices.clockwiseSort(vertices); } else { // fallback to convex hull when decomposition is not possible vertices = Vertices.hull(vertices); } parts.push({ position: { x: x, y: y }, vertices: vertices }); } else { // initialise a decomposition var concave = vertices.map(function(vertex) { return [vertex.x, vertex.y]; }); // vertices are concave and simple, we can decompose into parts decomp.makeCCW(concave); if (removeCollinear !== false) decomp.removeCollinearPoints(concave, removeCollinear); if (removeDuplicatePoints !== false && decomp.removeDuplicatePoints) decomp.removeDuplicatePoints(concave, removeDuplicatePoints); // use the quick decomposition algorithm (Bayazit) var decomposed = decomp.quickDecomp(concave); // for each decomposed chunk for (i = 0; i < decomposed.length; i++) { var chunk = decomposed[i]; // convert vertices into the correct structure var chunkVertices = chunk.map(function(vertices) { return { x: vertices[0], y: vertices[1] }; }); // skip small chunks if (minimumArea > 0 && Vertices.area(chunkVertices) < minimumArea) continue; // create a compound part parts.push({ position: Vertices.centre(chunkVertices), vertices: chunkVertices }); } } } // create body parts for (i = 0; i < parts.length; i++) { parts[i] = Body.create(Common.extend(parts[i], options)); } // flag internal edges (coincident part edges) if (flagInternal) { var coincident_max_dist = 5; for (i = 0; i < parts.length; i++) { var partA = parts[i]; for (j = i + 1; j < parts.length; j++) { var partB = parts[j]; if (Bounds.overlaps(partA.bounds, partB.bounds)) { var pav = partA.vertices, pbv = partB.vertices; // iterate vertices of both parts for (k = 0; k < partA.vertices.length; k++) { for (z = 0; z < partB.vertices.length; z++) { // find distances between the vertices var da = Vector.magnitudeSquared(Vector.sub(pav[(k + 1) % pav.length], pbv[z])), db = Vector.magnitudeSquared(Vector.sub(pav[k], pbv[(z + 1) % pbv.length])); // if both vertices are very close, consider the edge concident (internal) if (da < coincident_max_dist && db < coincident_max_dist) { pav[k].isInternal = true; pbv[z].isInternal = true; } } } } } } } if (parts.length > 1) { // create the parent body to be returned, that contains generated compound parts body = Body.create(Common.extend({ parts: parts.slice(0) }, options)); // offset such that body.position is at the centre off mass Body.setPosition(body, { x: x, y: y }); return body; } else { return parts[0]; } }; /** * Takes an array of Body objects and flags all internal edges (coincident parts) based on the maxDistance * value. The array is changed in-place and returned, so you can pass this function a `Body.parts` property. * * @method flagCoincidentParts * @param {body[]} parts - The Body parts, or array of bodies, to flag. * @param {number} [maxDistance=5] * @return {body[]} The modified `parts` parameter. */ Bodies.flagCoincidentParts = function (parts, maxDistance) { if (maxDistance === undefined) { maxDistance = 5; } for (var i = 0; i < parts.length; i++) { var partA = parts[i]; for (var j = i + 1; j < parts.length; j++) { var partB = parts[j]; if (Bounds.overlaps(partA.bounds, partB.bounds)) { var pav = partA.vertices; var pbv = partB.vertices; // iterate vertices of both parts for (var k = 0; k < partA.vertices.length; k++) { for (var z = 0; z < partB.vertices.length; z++) { // find distances between the vertices var da = Vector.magnitudeSquared(Vector.sub(pav[(k + 1) % pav.length], pbv[z])); var db = Vector.magnitudeSquared(Vector.sub(pav[k], pbv[(z + 1) % pbv.length])); // if both vertices are very close, consider the edge concident (internal) if (da < maxDistance && db < maxDistance) { pav[k].isInternal = true; pbv[z].isInternal = true; } } } } } } return parts; }; })();