From 5e65166faa3ee9dc2d9786f2c1926f88f96de90b Mon Sep 17 00:00:00 2001 From: liabru Date: Fri, 7 Mar 2014 16:58:57 +0000 Subject: [PATCH] added new build tasks for development build --- Gruntfile.js | 32 +- build/matter-dev.js | 4213 +++++++++++++++++++++++++++++++++++++ build/matter-dev.min.js | 8 + demo/dev.html | 46 + demo/js/lib/matter-dev.js | 4213 +++++++++++++++++++++++++++++++++++++ 5 files changed, 8499 insertions(+), 13 deletions(-) create mode 100644 build/matter-dev.js create mode 100644 build/matter-dev.min.js create mode 100644 demo/dev.html create mode 100644 demo/js/lib/matter-dev.js diff --git a/Gruntfile.js b/Gruntfile.js index e99fbfb..2e6c3ef 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,6 +1,7 @@ module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), + buildName: 'matter', concat: { build: { options: { @@ -9,29 +10,29 @@ module.exports = function(grunt) { } }, src: ['src/**/*.js', '!src/module/*'], - dest: 'build/<%= pkg.name %>.js' + dest: 'build/<%= buildName %>.js' }, pack: { options: { - banner: '/**\n* <%= pkg.name %>.js <%= pkg.version %> <%= grunt.template.today("yyyy-mm-dd") %>\n* <%= pkg.homepage %>\n* License: <%= pkg.license %>\n*/\n\n', + banner: '/**\n* <%= buildName %>.js <%= pkg.version %> <%= grunt.template.today("yyyy-mm-dd") %>\n* <%= pkg.homepage %>\n* License: <%= pkg.license %>\n*/\n\n', }, - src: ['src/module/Intro.js', 'build/<%= pkg.name %>.js', 'src/module/Outro.js'], - dest: 'build/<%= pkg.name %>.js' + src: ['src/module/Intro.js', 'build/<%= buildName %>.js', 'src/module/Outro.js'], + dest: 'build/<%= buildName %>.js' } }, uglify: { options: { - banner: '/**\n* <%= pkg.name %>.min.js <%= pkg.version %> <%= grunt.template.today("yyyy-mm-dd") %>\n* <%= pkg.homepage %>\n* License: <%= pkg.license %>\n*/\n\n', + banner: '/**\n* <%= buildName %>.min.js <%= pkg.version %> <%= grunt.template.today("yyyy-mm-dd") %>\n* <%= pkg.homepage %>\n* License: <%= pkg.license %>\n*/\n\n', }, build: { - src: 'build/<%= pkg.name %>.js', - dest: 'build/<%= pkg.name %>.min.js' + src: 'build/<%= buildName %>.js', + dest: 'build/<%= buildName %>.min.js' } }, copy: { demo: { - src: 'build/<%= pkg.name %>.js', - dest: 'demo/js/lib/<%= pkg.name %>.js' + src: 'build/<%= buildName %>.js', + dest: 'demo/js/lib/<%= buildName %>.js' } }, jshint: { @@ -44,7 +45,7 @@ module.exports = function(grunt) { watch: { options: { port: 9000, - open: 'http://localhost:9000/demo', + open: 'http://localhost:9000/demo/dev.html', livereload: 9001 } } @@ -57,7 +58,7 @@ module.exports = function(grunt) { }, src: { files: ['src/**/*.js'], - tasks: ['concat', 'uglify', 'copy'] + tasks: ['set_config:buildName:matter-dev','build'] }, demo: { files: ['build/matter.js', 'demo/js/**/*.html', 'demo/js/**/*.js', 'demo/css/**/*.css'] @@ -87,8 +88,13 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-yuidoc'); - grunt.registerTask('default', ['test', 'concat', 'uglify', 'copy']); + grunt.registerTask('default', ['test', 'build']); + grunt.registerTask('build', ['concat', 'uglify', 'copy']); grunt.registerTask('test', ['jshint']); - grunt.registerTask('dev', ['connect:watch', 'watch']); + grunt.registerTask('dev', ['set_config:buildName:matter-dev', 'build', 'connect:watch', 'watch']); grunt.registerTask('doc', ['yuidoc']); + + grunt.registerTask('set_config', 'Set a config property.', function(name, val) { + grunt.config.set(name, val); + }); }; \ No newline at end of file diff --git a/build/matter-dev.js b/build/matter-dev.js new file mode 100644 index 0000000..b596e36 --- /dev/null +++ b/build/matter-dev.js @@ -0,0 +1,4213 @@ +/** +* matter-dev.js 0.5.0 2014-03-07 +* http://brm.io/matter-js/ +* License: MIT +*/ + +/** + * The MIT License (MIT) + * + * Copyright (c) 2014 Liam Brummitt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +(function() { + +var Matter = {}; + +// Begin Matter namespace closure + +// All Matter modules are included below during build +// Outro.js then closes at the end of the file + + +// Begin src/body/Body.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Body +*/ + +var Body = {}; + +(function() { + + var _nextId = 0, + _nextGroupId = 1; + + /** + * Description to be written. + * @method create + * @param {} options + * @return {body} body + */ + Body.create = function(options) { + var defaults = { + id: Body.nextId(), + angle: 0, + position: { x: 0, y: 0 }, + force: { x: 0, y: 0 }, + torque: 0, + positionImpulse: { x: 0, y: 0 }, + speed: 0, + angularSpeed: 0, + velocity: { x: 0, y: 0 }, + angularVelocity: 0, + isStatic: false, + isSleeping: false, + motion: 0, + sleepThreshold: 60, + density: 0.001, + restitution: 0, + friction: 0.1, + frictionAir: 0.01, + path: 'L 0 0 L 40 0 L 40 40 L 0 40', + fillStyle: options.isStatic ? '#eeeeee' : Common.choose(['#556270', '#4ECDC4', '#C7F464', '#FF6B6B', '#C44D58']), + lineWidth: 1.5, + groupId: 0, + slop: 0.05 + }; + + var body = Common.extend(defaults, options); + + Body.updateProperties(body); + + return body; + }; + + /** + * Description + * @method nextId + * @return {Number} Unique bodyID + */ + Body.nextId = function() { + return _nextId++; + }; + + /** + * Description + * @method nextGroupId + * @return {Number} Unique groupID + */ + Body.nextGroupId = function() { + return _nextGroupId++; + }; + + /** + * Description + * @method updateProperties + * @param {body} body + */ + Body.updateProperties = function(body) { + // calculated properties + body.vertices = body.vertices || Vertices.fromPath(body.path); + body.axes = body.axes || Axes.fromVertices(body.vertices); + body.area = Vertices.area(body.vertices); + body.bounds = Bounds.create(body.vertices); + body.mass = body.mass || (body.density * body.area); + body.inverseMass = 1 / body.mass; + body.inertia = body.inertia || Vertices.inertia(body.vertices, body.mass); + body.inverseInertia = 1 / body.inertia; + body.positionPrev = body.positionPrev || { x: body.position.x, y: body.position.y }; + body.anglePrev = body.anglePrev || body.angle; + body.strokeStyle = body.strokeStyle || Common.shadeColor(body.fillStyle, -20); + + // update geometry + Vertices.create(body.vertices, body); + var centre = Vertices.centre(body.vertices); + Vertices.translate(body.vertices, body.position); + Vertices.translate(body.vertices, centre, -1); + Vertices.rotate(body.vertices, body.angle, body.position); + Axes.rotate(body.axes, body.angle); + Bounds.update(body.bounds, body.vertices, body.velocity); + + if (body.isStatic) { + body.restitution = 0; + body.friction = 1; + body.mass = body.inertia = body.density = Infinity; + body.inverseMass = body.inverseInertia = 0; + body.lineWidth = 1; + } + + Sleeping.set(body, body.isSleeping); + }; + + /** + * Description + * @method resetForcesAll + * @param {body[]} bodies + * @param {vector} gravity + */ + Body.resetForcesAll = function(bodies, gravity) { + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i]; + + if (body.isStatic || body.isSleeping) + continue; + + // reset force buffers + body.force.x = 0; + body.force.y = 0; + body.torque = 0; + + // apply gravity + body.force.y += body.mass * gravity.y * 0.001; + body.force.x += body.mass * gravity.x * 0.001; + } + }; + + /** + * Description + * @method updateAll + * @param {body[]} bodies + * @param {number} deltaTime + * @param {number} correction + * @param {bounds} worldBounds + */ + Body.updateAll = function(bodies, deltaTime, correction, worldBounds) { + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i]; + + if (body.isStatic || body.isSleeping) + continue; + + // don't update out of world bodies + // TODO: viewports + if (body.bounds.max.x < worldBounds.min.x || body.bounds.min.x > worldBounds.max.x + || body.bounds.max.y < worldBounds.min.y || body.bounds.min.y > worldBounds.max.y) + continue; + + Body.update(body, deltaTime, correction); + } + }; + + /** + * Description + * @method update + * @param {body} body + * @param {number} deltaTime + * @param {number} correction + */ + Body.update = function(body, deltaTime, correction) { + var deltaTimeSquared = deltaTime * deltaTime; + + // from the previous step + var frictionAir = 1 - body.frictionAir, + velocityPrevX = body.position.x - body.positionPrev.x, + velocityPrevY = body.position.y - body.positionPrev.y; + + // update velocity with verlet integration + body.velocity.x = (velocityPrevX * frictionAir * correction) + (body.force.x / body.mass) * deltaTimeSquared; + body.velocity.y = (velocityPrevY * frictionAir * correction) + (body.force.y / body.mass) * deltaTimeSquared; + + body.positionPrev.x = body.position.x; + body.positionPrev.y = body.position.y; + body.position.x += body.velocity.x; + body.position.y += body.velocity.y; + + // update angular velocity with verlet integration + body.angularVelocity = ((body.angle - body.anglePrev) * frictionAir * correction) + (body.torque / body.inertia) * deltaTimeSquared; + body.anglePrev = body.angle; + body.angle += body.angularVelocity; + + // track speed and acceleration + body.speed = Vector.magnitude(body.velocity); + body.angularSpeed = Math.abs(body.angularVelocity); + + // transform the body geometry + Vertices.translate(body.vertices, body.velocity); + Vertices.rotate(body.vertices, body.angularVelocity, body.position); + Axes.rotate(body.axes, body.angularVelocity); + Bounds.update(body.bounds, body.vertices, body.velocity); + }; + + /** + * Description + * @method applyForce + * @param {body} body + * @param {vector} position + * @param {vector} force + */ + Body.applyForce = function(body, position, force) { + body.force.x += force.x; + body.force.y += force.y; + var offset = { x: position.x - body.position.x, y: position.y - body.position.y }; + body.torque += (offset.x * force.y - offset.y * force.x) * body.inverseInertia; + }; + + /** + * Description + * @method translate + * @param {body} body + * @param {vector} translation + */ + Body.translate = function(body, translation) { + body.positionPrev.x += translation.x; + body.positionPrev.y += translation.y; + body.position.x += translation.x; + body.position.y += translation.y; + Vertices.translate(body.vertices, translation); + Bounds.update(body.bounds, body.vertices, body.velocity); + }; + + /** + * Description + * @method rotate + * @param {body} body + * @param {number} angle + */ + Body.rotate = function(body, angle) { + body.anglePrev += angle; + body.angle += angle; + Vertices.rotate(body.vertices, angle, body.position); + Axes.rotate(body.axes, angle); + Bounds.update(body.bounds, body.vertices, body.velocity); + }; + +})(); + +; // End src/body/Body.js + + +// Begin src/body/Composite.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Composite +*/ + +// TODO: composite translate, rotate + +var Composite = {}; + +(function() { + + /** + * Description + * @method create + * @param {} options + * @return {composite} A new composite + */ + Composite.create = function(options) { + return Common.extend({ bodies: [], constraints: [], composites: [] }, options); + }; + + /** + * Description + * @method add + * @param {composite} compositeA + * @param {composite} compositeB + * @return {composite} The original compositeA with the objects from compositeB added + */ + Composite.add = function(compositeA, compositeB) { + if (compositeA.bodies && compositeB.bodies) + compositeA.bodies = compositeA.bodies.concat(compositeB.bodies); + + if (compositeA.constraints && compositeB.constraints) + compositeA.constraints = compositeA.constraints.concat(compositeB.constraints); + + if (compositeA.composites && compositeB.composites) + compositeA.composites = compositeA.composites.concat(compositeB.composites); + + return compositeA; + }; + + /** + * Description + * @method addBody + * @param {composite} composite + * @param {body} body + * @return {composite} The original composite with the body added + */ + Composite.addBody = function(composite, body) { + composite.bodies = composite.bodies || []; + composite.bodies.push(body); + return composite; + }; + + /** + * Description + * @method addConstraint + * @param {composite} composite + * @param {constraint} constraint + * @return {composite} The original composite with the constraint added + */ + Composite.addConstraint = function(composite, constraint) { + composite.constraints = composite.constraints || []; + composite.constraints.push(constraint); + return composite; + }; + +})(); + +; // End src/body/Composite.js + + +// Begin src/body/World.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class World +*/ + +var World = {}; + +(function() { + + /** + * Description + * @method create + * @constructor + * @param {} options + * @return {world} A new world + */ + World.create = function(options) { + var defaults = { + gravity: { x: 0, y: 1 }, + bodies: [], + constraints: [], + bounds: { + min: { x: 0, y: 0 }, + max: { x: 800, y: 600 } + } + }; + + return Common.extend(defaults, options); + }; + + /** + * Description + * @method clear + * @param {world} world + * @param {boolean} keepStatic + */ + World.clear = function(world, keepStatic) { + world.bodies = keepStatic ? world.bodies.filter(function(body) { return body.isStatic; }) : []; + world.constraints = []; + }; + + // World is a Composite body + // see src/module/Outro.js for these aliases: + + /** + * An alias for Composite.add since World is also a Composite (see Outro.js) + * @method addComposite + * @param {world} world + * @param {composite} composite + * @return {world} The original world with the objects from composite added + */ + + /** + * An alias for Composite.addBody since World is also a Composite (see Outro.js) + * @method addBody + * @param {world} world + * @param {body} body + * @return {world} The original world with the body added + */ + + /** + * An alias for Composite.addConstraint since World is also a Composite (see Outro.js) + * @method addConstraint + * @param {world} world + * @param {constraint} constraint + * @return {world} The original world with the constraint added + */ + +})(); + +; // End src/body/World.js + + +// Begin src/collision/Contact.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Contact +*/ + +var Contact = {}; + +(function() { + + /** + * Description + * @method create + * @param {vertex} vertex + * @return {contact} A new contact + */ + Contact.create = function(vertex) { + return { + id: Contact.id(vertex), + vertex: vertex, + normalImpulse: 0, + tangentImpulse: 0 + }; + }; + + /** + * Description + * @method id + * @param {vertex} vertex + * @return {Number} Unique contactID + */ + Contact.id = function(vertex) { + return vertex.body.id + '_' + vertex.index; + }; + +})(); + +; // End src/collision/Contact.js + + +// Begin src/collision/Detector.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Detector +*/ + +// TODO: speculative contacts + +var Detector = {}; + +(function() { + + /** + * Description + * @method collisions + * @param {pair[]} pairs + * @param {metrics} metrics + * @return {array} collisions + */ + Detector.collisions = function(pairs, metrics) { + var collisions = []; + + for (var i = 0; i < pairs.length; i++) { + var bodyA = pairs[i][0], + bodyB = pairs[i][1]; + + // NOTE: could share a function for the below, but may drop performance? + + if (bodyA.groupId && bodyB.groupId && bodyA.groupId === bodyB.groupId) + continue; + + if ((bodyA.isStatic || bodyA.isSleeping) && (bodyB.isStatic || bodyB.isSleeping)) + continue; + + metrics.midphaseTests += 1; + + // mid phase + if (Bounds.overlaps(bodyA.bounds, bodyB.bounds)) { + + // narrow phase + var collision = SAT.collides(bodyA, bodyB); + + metrics.narrowphaseTests += 1; + + if (collision.collided) { + collisions.push(collision); + metrics.narrowDetections += 1; + } + } + } + + return collisions; + }; + + /** + * Description + * @method bruteForce + * @param {body[]} bodies + * @param {metrics} metrics + * @return {array} collisions + */ + Detector.bruteForce = function(bodies, metrics) { + var collisions = []; + + for (var i = 0; i < bodies.length; i++) { + for (var j = i + 1; j < bodies.length; j++) { + var bodyA = bodies[i], + bodyB = bodies[j]; + + // NOTE: could share a function for the below, but may drop performance? + + if (bodyA.groupId && bodyB.groupId && bodyA.groupId === bodyB.groupId) + continue; + + if ((bodyA.isStatic || bodyA.isSleeping) && (bodyB.isStatic || bodyB.isSleeping)) + continue; + + metrics.midphaseTests += 1; + + // mid phase + if (Bounds.overlaps(bodyA.bounds, bodyB.bounds)) { + + // narrow phase + var collision = SAT.collides(bodyA, bodyB); + + metrics.narrowphaseTests += 1; + + if (collision.collided) { + collisions.push(collision); + metrics.narrowDetections += 1; + } + } + } + } + + return collisions; + }; + +})(); + +; // End src/collision/Detector.js + + +// Begin src/collision/Grid.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Grid +*/ + +var Grid = {}; + +(function() { + + /** + * Description + * @method create + * @param {number} bucketWidth + * @param {number} bucketHeight + * @return {grid} A new grid + */ + Grid.create = function(bucketWidth, bucketHeight) { + return { + buckets: {}, + pairs: {}, + pairsList: [], + bucketWidth: bucketWidth || 48, + bucketHeight: bucketHeight || 48 + }; + }; + + /** + * Description + * @method update + * @param {grid} grid + * @param {body[]} bodies + * @param {engine} engine + * @param {boolean} forceUpdate + */ + Grid.update = function(grid, bodies, engine, forceUpdate) { + var i, col, row, + world = engine.world, + buckets = grid.buckets, + bucket, + bucketId, + metrics = engine.metrics, + gridChanged = false; + + metrics.broadphaseTests = 0; + + for (i = 0; i < bodies.length; i++) { + var body = bodies[i]; + + if (body.isSleeping) + continue; + + // don't update out of world bodies + if (body.bounds.max.x < 0 || body.bounds.min.x > world.bounds.width + || body.bounds.max.y < 0 || body.bounds.min.y > world.bounds.height) + continue; + + var newRegion = _getRegion(grid, body); + + // if the body has changed grid region + if (!body.region || newRegion.id !== body.region.id || forceUpdate) { + + metrics.broadphaseTests += 1; + + if (!body.region || forceUpdate) + body.region = newRegion; + + var union = _regionUnion(newRegion, body.region); + + // update grid buckets affected by region change + // iterate over the union of both regions + for (col = union.startCol; col <= union.endCol; col++) { + for (row = union.startRow; row <= union.endRow; row++) { + bucketId = _getBucketId(col, row); + bucket = buckets[bucketId]; + + var isInsideNewRegion = (col >= newRegion.startCol && col <= newRegion.endCol + && row >= newRegion.startRow && row <= newRegion.endRow); + + var isInsideOldRegion = (col >= body.region.startCol && col <= body.region.endCol + && row >= body.region.startRow && row <= body.region.endRow); + + // remove from old region buckets + if (!isInsideNewRegion && isInsideOldRegion) { + if (isInsideOldRegion) { + if (bucket) + _bucketRemoveBody(grid, bucket, body); + } + } + + // add to new region buckets + if (body.region === newRegion || (isInsideNewRegion && !isInsideOldRegion) || forceUpdate) { + if (!bucket) + bucket = _createBucket(buckets, bucketId); + _bucketAddBody(grid, bucket, body); + } + } + } + + // set the new region + body.region = newRegion; + + // flag changes so we can update pairs + gridChanged = true; + } + } + + // update pairs list only if pairs changed (i.e. a body changed region) + if (gridChanged) + grid.pairsList = _createActivePairsList(grid); + }; + + /** + * Description + * @method clear + * @param {grid} grid + */ + Grid.clear = function(grid) { + grid.buckets = {}; + grid.pairs = {}; + grid.pairsList = []; + }; + + /** + * Description + * @method _regionUnion + * @private + * @param {} regionA + * @param {} regionB + * @return CallExpression + */ + var _regionUnion = function(regionA, regionB) { + var startCol = Math.min(regionA.startCol, regionB.startCol), + endCol = Math.max(regionA.endCol, regionB.endCol), + startRow = Math.min(regionA.startRow, regionB.startRow), + endRow = Math.max(regionA.endRow, regionB.endRow); + + return _createRegion(startCol, endCol, startRow, endRow); + }; + + /** + * Description + * @method _getRegion + * @private + * @param {} grid + * @param {} body + * @return CallExpression + */ + var _getRegion = function(grid, body) { + var bounds = body.bounds, + startCol = Math.floor(bounds.min.x / grid.bucketWidth), + endCol = Math.floor(bounds.max.x / grid.bucketWidth), + startRow = Math.floor(bounds.min.y / grid.bucketHeight), + endRow = Math.floor(bounds.max.y / grid.bucketHeight); + + return _createRegion(startCol, endCol, startRow, endRow); + }; + + /** + * Description + * @method _createRegion + * @private + * @param {} startCol + * @param {} endCol + * @param {} startRow + * @param {} endRow + * @return ObjectExpression + */ + var _createRegion = function(startCol, endCol, startRow, endRow) { + return { + id: startCol + ',' + endCol + ',' + startRow + ',' + endRow, + startCol: startCol, + endCol: endCol, + startRow: startRow, + endRow: endRow + }; + }; + + /** + * Description + * @method _getBucketId + * @private + * @param {} column + * @param {} row + * @return BinaryExpression + */ + var _getBucketId = function(column, row) { + return column + ',' + row; + }; + + /** + * Description + * @method _createBucket + * @private + * @param {} buckets + * @param {} bucketId + * @return bucket + */ + var _createBucket = function(buckets, bucketId) { + var bucket = buckets[bucketId] = []; + return bucket; + }; + + /** + * Description + * @method _bucketAddBody + * @private + * @param {} grid + * @param {} bucket + * @param {} body + */ + var _bucketAddBody = function(grid, bucket, body) { + // add new pairs + for (var i = 0; i < bucket.length; i++) { + var bodyB = bucket[i]; + + if (body.id === bodyB.id || (body.isStatic && bodyB.isStatic)) + continue; + + // keep track of the number of buckets the pair exists in + // important for Grid.update to work + var pairId = _getPairId(body, bodyB); + if (grid.pairs[pairId]) { + grid.pairs[pairId][2] += 1; + } else { + grid.pairs[pairId] = [body, bodyB, 1]; + } + } + + // add to bodies (after pairs, otherwise pairs with self) + bucket.push(body); + }; + + /** + * Description + * @method _bucketRemoveBody + * @private + * @param {} grid + * @param {} bucket + * @param {} body + */ + var _bucketRemoveBody = function(grid, bucket, body) { + // remove from bodies + for (var i = 0; i < bucket.length; i++) { + var bodyB = bucket[i]; + + // when position of body in bucket array is found, remove it + if (bodyB.id === body.id) { + bucket.splice(i, 1); + continue; + } else { + // keep track of the number of buckets the pair exists in + // important for Grid.update to work + var pairId = _getPairId(body, bodyB); + if (grid.pairs[pairId]) { + var pairCount = grid.pairs[pairId][2]; + grid.pairs[pairId][2] = pairCount > 0 ? pairCount - 1 : 0; + } + } + } + }; + + /** + * Description + * @method _getPairId + * @private + * @param {} bodyA + * @param {} bodyB + */ + var _getPairId = function(bodyA, bodyB) { + if (bodyA.id < bodyB.id) { + return bodyA.id + ',' + bodyB.id; + } else { + return bodyB.id + ',' + bodyA.id; + } + }; + + /** + * Description + * @method _createActivePairsList + * @private + * @param {} grid + * @return pairs + */ + var _createActivePairsList = function(grid) { + var pairKeys, + pair, + pairs = []; + + // grid.pairs is used as a hashmap + pairKeys = Common.keys(grid.pairs); + + // iterate over grid.pairs + for (var k = 0; k < pairKeys.length; k++) { + pair = grid.pairs[pairKeys[k]]; + + // if pair exists in at least one bucket + // it is a pair that needs further collision testing so push it + if (pair[2] > 0) + pairs.push(pair); + } + + return pairs; + }; + +})(); + +; // End src/collision/Grid.js + + +// Begin src/collision/Manager.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Manager +*/ + +var Manager = {}; + +(function() { + + var _pairMaxIdleLife = 500; + + /** + * Description + * @method updatePairs + * @param {object} pairs + * @param {pair[]} pairsList + * @param {pair[]} candidatePairs + * @param {metrics} metrics + * @param {detector} detector + * @return {bool} pairsUpdated flag + */ + Manager.updatePairs = function(pairs, pairsList, candidatePairs, metrics, detector) { + var i; + + // first set all pairs inactive + for (i = 0; i < pairsList.length; i++) { + var pair = pairsList[i]; + Pair.setActive(pair, false); + } + + // detect collisions in current step + var pairsUpdated = false, + collisions = detector(candidatePairs, metrics); + + // set collision pairs to active, or create if pair is new + for (i = 0; i < collisions.length; i++) { + var collision = collisions[i], + pairId = Pair.id(collision.bodyA, collision.bodyB); + + if (pairId in pairs) { + Pair.update(pairs[pairId], collision); + } else { + pairs[pairId] = Pair.create(collision); + pairsUpdated = true; + } + } + + return pairsUpdated; + }; + + /** + * Description + * @method removeOldPairs + * @param {object} pairs + * @param {pair[]} pairsList + * @return {bool} pairsRemoved flag + */ + Manager.removeOldPairs = function(pairs, pairsList) { + var timeNow = Common.now(), + pairsRemoved = false, + i; + + for (i = 0; i < pairsList.length; i++) { + var pair = pairsList[i], + collision = pair.collision; + + // never remove sleeping pairs + if (collision.bodyA.isSleeping || collision.bodyB.isSleeping) { + pair.timestamp = timeNow; + continue; + } + + // if pair is inactive for too long, remove it + if (timeNow - pair.timestamp > _pairMaxIdleLife) { + delete pairs[pair.id]; + pairsRemoved = true; + } + } + + return pairsRemoved; + }; + +})(); + +; // End src/collision/Manager.js + + +// Begin src/collision/Pair.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Pair +*/ + +var Pair = {}; + +(function() { + + /** + * Description + * @method create + * @param {collision} collision + * @return {pair} A new pair + */ + Pair.create = function(collision) { + var bodyA = collision.bodyA, + bodyB = collision.bodyB; + + var pair = { + id: Pair.id(bodyA, bodyB), + contacts: {}, + activeContacts: [], + separation: 0, + isActive: true, + timestamp: Common.now(), + inverseMass: bodyA.inverseMass + bodyB.inverseMass, + friction: Math.min(bodyA.friction, bodyB.friction), + restitution: Math.max(bodyA.restitution, bodyB.restitution), + slop: Math.max(bodyA.slop, bodyB.slop) + }; + + Pair.update(pair, collision); + + return pair; + }; + + /** + * Description + * @method update + * @param {pair} pair + * @param {collision} collision + */ + Pair.update = function(pair, collision) { + var contacts = pair.contacts, + supports = collision.supports, + activeContacts = []; + + pair.collision = collision; + + if (collision.collided) { + for (var i = 0; i < supports.length; i++) { + var support = supports[i], + contactId = Contact.id(support); + + if (contactId in contacts) { + activeContacts.push(contacts[contactId]); + } else { + activeContacts.push(contacts[contactId] = Contact.create(support)); + } + } + + pair.activeContacts = activeContacts; + pair.separation = collision.depth; + Pair.setActive(pair, true); + } else { + Pair.setActive(pair, false); + } + }; + + /** + * Description + * @method setActive + * @param {pair} pair + * @param {bool} isActive + */ + Pair.setActive = function(pair, isActive) { + if (isActive) { + pair.isActive = true; + pair.timestamp = Common.now(); + } else { + pair.isActive = false; + pair.activeContacts = []; + } + }; + + /** + * Description + * @method id + * @param {body} bodyA + * @param {body} bodyB + * @return {number} Unique pairId + */ + Pair.id = function(bodyA, bodyB) { + if (bodyA.id < bodyB.id) { + return bodyA.id + '_' + bodyB.id; + } else { + return bodyB.id + '_' + bodyA.id; + } + }; + +})(); + +; // End src/collision/Pair.js + + +// Begin src/collision/Resolver.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Resolver +*/ + +var Resolver = {}; + +(function() { + + var _restingThresh = 4, + _positionDampen = 0.2, + _positionWarming = 0.6; + + /** + * Description + * @method solvePosition + * @param {pair[]} pairs + */ + Resolver.solvePosition = function(pairs) { + var i, + pair, + collision, + bodyA, + bodyB, + vertex, + vertexCorrected, + normal, + bodyBtoA; + + // find impulses required to resolve penetration + for (i = 0; i < pairs.length; i++) { + pair = pairs[i]; + + if (!pair.isActive) + continue; + + collision = pair.collision; + bodyA = collision.bodyA; + bodyB = collision.bodyB; + vertex = collision.supports[0]; + vertexCorrected = collision.supportCorrected; + normal = collision.normal; + + // get current separation between body edges involved in collision + bodyBtoA = Vector.sub(Vector.add(bodyB.positionImpulse, vertex), + Vector.add(bodyA.positionImpulse, vertexCorrected)); + + pair.separation = Vector.dot(normal, bodyBtoA); + } + + for (i = 0; i < pairs.length; i++) { + pair = pairs[i]; + + if (!pair.isActive) + continue; + + collision = pair.collision; + bodyA = collision.bodyA; + bodyB = collision.bodyB; + normal = collision.normal; + positionImpulse = (pair.separation * _positionDampen) - pair.slop; + + if (bodyA.isStatic || bodyB.isStatic) + positionImpulse *= 2; + + if (!(bodyA.isStatic || bodyA.isSleeping)) { + bodyA.positionImpulse.x += normal.x * positionImpulse; + bodyA.positionImpulse.y += normal.y * positionImpulse; + } + + if (!(bodyB.isStatic || bodyB.isSleeping)) { + bodyB.positionImpulse.x -= normal.x * positionImpulse; + bodyB.positionImpulse.y -= normal.y * positionImpulse; + } + } + }; + + /** + * Description + * @method postSolvePosition + * @param {body[]} bodies + */ + Resolver.postSolvePosition = function(bodies) { + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i]; + + if (body.positionImpulse.x !== 0 || body.positionImpulse.y !== 0) { + // move the body without changing velocity + body.position.x += body.positionImpulse.x; + body.position.y += body.positionImpulse.y; + body.positionPrev.x += body.positionImpulse.x; + body.positionPrev.y += body.positionImpulse.y; + + // update body geometry + Vertices.translate(body.vertices, body.positionImpulse); + Bounds.update(body.bounds, body.vertices, body.velocity); + + // dampen accumulator to warm the next step + body.positionImpulse.x *= _positionWarming; + body.positionImpulse.y *= _positionWarming; + } + } + }; + + /** + * Description + * @method preSolveVelocity + * @param {pair[]} pairs + */ + Resolver.preSolveVelocity = function(pairs) { + var impulse = {}, + i, + j, + pair, + contacts, + collision, + bodyA, + bodyB, + normal, + tangent, + contact, + contactVertex, + normalImpulse, + tangentImpulse, + offset; + + for (i = 0; i < pairs.length; i++) { + pair = pairs[i]; + + if (!pair.isActive) + continue; + + contacts = pair.activeContacts; + collision = pair.collision; + bodyA = collision.bodyA; + bodyB = collision.bodyB; + normal = collision.normal; + tangent = collision.tangent; + + // resolve each contact + for (j = 0; j < contacts.length; j++) { + contact = contacts[j]; + contactVertex = contact.vertex; + normalImpulse = contact.normalImpulse; + tangentImpulse = contact.tangentImpulse; + + // total impulse from contact + impulse.x = (normal.x * normalImpulse) + (tangent.x * tangentImpulse); + impulse.y = (normal.y * normalImpulse) + (tangent.y * tangentImpulse); + + // apply impulse from contact + if (!(bodyA.isStatic || bodyA.isSleeping)) { + offset = Vector.sub(contactVertex, bodyA.position); + bodyA.positionPrev.x += impulse.x * bodyA.inverseMass; + bodyA.positionPrev.y += impulse.y * bodyA.inverseMass; + bodyA.anglePrev += Vector.cross(offset, impulse) * bodyA.inverseInertia; + } + + if (!(bodyB.isStatic || bodyB.isSleeping)) { + offset = Vector.sub(contactVertex, bodyB.position); + bodyB.positionPrev.x -= impulse.x * bodyB.inverseMass; + bodyB.positionPrev.y -= impulse.y * bodyB.inverseMass; + bodyB.anglePrev -= Vector.cross(offset, impulse) * bodyB.inverseInertia; + } + } + } + }; + + /** + * Description + * @method solveVelocity + * @param {pair[]} pairs + */ + Resolver.solveVelocity = function(pairs) { + var impulse = {}; + + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i]; + + if (!pair.isActive) + continue; + + var collision = pair.collision, + bodyA = collision.bodyA, + bodyB = collision.bodyB, + normal = collision.normal, + tangent = collision.tangent, + contacts = pair.activeContacts, + contactShare = 1 / contacts.length; + + // update body velocities + bodyA.velocity.x = bodyA.position.x - bodyA.positionPrev.x; + bodyA.velocity.y = bodyA.position.y - bodyA.positionPrev.y; + bodyB.velocity.x = bodyB.position.x - bodyB.positionPrev.x; + bodyB.velocity.y = bodyB.position.y - bodyB.positionPrev.y; + bodyA.angularVelocity = bodyA.angle - bodyA.anglePrev; + bodyB.angularVelocity = bodyB.angle - bodyB.anglePrev; + + // resolve each contact + for (var j = 0; j < contacts.length; j++) { + var contact = contacts[j], + contactVertex = contact.vertex, + offsetA = Vector.sub(contactVertex, bodyA.position), + offsetB = Vector.sub(contactVertex, bodyB.position), + velocityPointA = Vector.add(bodyA.velocity, Vector.mult(Vector.perp(offsetA), bodyA.angularVelocity)), + velocityPointB = Vector.add(bodyB.velocity, Vector.mult(Vector.perp(offsetB), bodyB.angularVelocity)), + relativeVelocity = Vector.sub(velocityPointA, velocityPointB), + normalVelocity = Vector.dot(normal, relativeVelocity); + + var tangentVelocity = Vector.dot(tangent, relativeVelocity), + tangentSpeed = Math.abs(tangentVelocity), + tangentVelocityDirection = Common.sign(tangentVelocity); + + // raw impulses + var normalImpulse = (1 + pair.restitution) * normalVelocity, + normalForce = Common.clamp(pair.separation + normalVelocity, 0, 1); + + // coulomb friction + var tangentImpulse = tangentVelocity; + if (tangentSpeed > normalForce * pair.friction) + tangentImpulse = normalForce * pair.friction * tangentVelocityDirection; + + // modify impulses accounting for mass, inertia and offset + var oAcN = Vector.cross(offsetA, normal), + oBcN = Vector.cross(offsetB, normal), + share = contactShare / (pair.inverseMass + bodyA.inverseInertia * oAcN * oAcN + bodyB.inverseInertia * oBcN * oBcN); + normalImpulse *= share; + tangentImpulse *= share; + + // handle high velocity and resting collisions separately + if (normalVelocity < 0 && normalVelocity * normalVelocity > _restingThresh) { + // high velocity so clear cached contact impulse + contact.normalImpulse = 0; + contact.tangentImpulse = 0; + } else { + // solve resting collision constraints using Erin Catto's method (GDC08) + + // impulse constraint, tends to 0 + var contactNormalImpulse = contact.normalImpulse; + contact.normalImpulse = Math.min(contact.normalImpulse + normalImpulse, 0); + normalImpulse = contact.normalImpulse - contactNormalImpulse; + + // tangent impulse, tends to -maxFriction or maxFriction + var contactTangentImpulse = contact.tangentImpulse; + contact.tangentImpulse = Common.clamp(contact.tangentImpulse + tangentImpulse, -tangentSpeed, tangentSpeed); + tangentImpulse = contact.tangentImpulse - contactTangentImpulse; + } + + // total impulse from contact + impulse.x = (normal.x * normalImpulse) + (tangent.x * tangentImpulse); + impulse.y = (normal.y * normalImpulse) + (tangent.y * tangentImpulse); + + // apply impulse from contact + if (!(bodyA.isStatic || bodyA.isSleeping)) { + bodyA.positionPrev.x += impulse.x * bodyA.inverseMass; + bodyA.positionPrev.y += impulse.y * bodyA.inverseMass; + bodyA.anglePrev += Vector.cross(offsetA, impulse) * bodyA.inverseInertia; + } + + if (!(bodyB.isStatic || bodyB.isSleeping)) { + bodyB.positionPrev.x -= impulse.x * bodyB.inverseMass; + bodyB.positionPrev.y -= impulse.y * bodyB.inverseMass; + bodyB.anglePrev -= Vector.cross(offsetB, impulse) * bodyB.inverseInertia; + } + } + } + }; + +})(); + +; // End src/collision/Resolver.js + + +// Begin src/collision/SAT.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class SAT +*/ + + +// TODO: true circles and curves +// TODO: cache the previously found axis and body, and start there first for faster early out + +var SAT = {}; + +(function() { + + /** + * Description + * @method collides + * @param {body} bodyA + * @param {body} bodyB + * @return {collision} collision + */ + SAT.collides = function(bodyA, bodyB) { + var overlapAB, + overlapBA, + minOverlap, + collision = { collided: false, bodyA: bodyA, bodyB: bodyB}; + + overlapAB = _overlapAxes(bodyA.vertices, bodyB.vertices, bodyA.axes); + + if (overlapAB.overlap === 0) + return collision; + + overlapBA = _overlapAxes(bodyB.vertices, bodyA.vertices, bodyB.axes); + + if (overlapBA.overlap === 0) + return collision; + + if (overlapAB.overlap < overlapBA.overlap) { + minOverlap = overlapAB; + } else { + minOverlap = overlapBA; + collision.bodyA = bodyB; + collision.bodyB = bodyA; + } + + collision.collided = true; + collision.normal = minOverlap.axis; + collision.depth = minOverlap.overlap; + + bodyA = collision.bodyA; + bodyB = collision.bodyB; + + // ensure normal is facing away from bodyA + if (Vector.dot(collision.normal, Vector.sub(bodyB.position, bodyA.position)) > 0) + collision.normal = Vector.neg(collision.normal); + + collision.tangent = Vector.perp(collision.normal); + + collision.penetration = { + x: collision.normal.x * collision.depth, + y: collision.normal.y * collision.depth + }; + + // find support points, there is always either exactly one or two + var verticesB = _findSupports(bodyA, bodyB, collision.normal), + supports = [verticesB[0]]; + + if (Vertices.contains(bodyA.vertices, verticesB[1])) { + supports.push(verticesB[1]); + } else { + var verticesA = _findSupports(bodyB, bodyA, Vector.neg(collision.normal)); + + if (Vertices.contains(bodyB.vertices, verticesA[0])) { + supports.push(verticesA[0]); + } + + if (supports.length < 2 && Vertices.contains(bodyB.vertices, verticesA[1])) { + supports.push(verticesA[1]); + } + } + + collision.supports = supports; + collision.supportCorrected = Vector.sub(verticesB[0], collision.penetration); + + return collision; + }; + + /** + * Description + * @method _overlapAxes + * @private + * @param {} verticesA + * @param {} verticesB + * @param {} axes + * @return result + */ + var _overlapAxes = function(verticesA, verticesB, axes) { + var projectionA = {}, + projectionB = {}, + result = { overlap: Number.MAX_VALUE }, + overlap, + axis; + + for (var i = 0; i < axes.length; i++) { + axis = axes[i]; + + _projectToAxis(projectionA, verticesA, axis); + _projectToAxis(projectionB, verticesB, axis); + + overlap = projectionA.min < projectionB.min + ? projectionA.max - projectionB.min + : projectionB.max - projectionA.min; + + if (overlap <= 0) + return { overlap: 0 }; + + if (overlap < result.overlap) { + result.overlap = overlap; + result.axis = axis; + } + } + + return result; + }; + + /** + * Description + * @method _projectToAxis + * @private + * @param {} projection + * @param {} vertices + * @param {} axis + */ + var _projectToAxis = function(projection, vertices, axis) { + var min = Vector.dot(vertices[0], axis), + max = min; + + for (var i = 1; i < vertices.length; i += 1) { + var dot = Vector.dot(vertices[i], axis); + + if (dot > max) { + max = dot; + } else if (dot < min) { + min = dot; + } + } + + projection.min = min; + projection.max = max; + }; + + /** + * Description + * @method _findSupports + * @private + * @param {} bodyA + * @param {} bodyB + * @param {} normal + * @return ArrayExpression + */ + var _findSupports = function(bodyA, bodyB, normal) { + var nearestDistance = Number.MAX_VALUE, + vertexToBody = { x: 0, y: 0 }, + vertices = bodyB.vertices, + bodyAPosition = bodyA.position, + distance, + vertex, + vertexA = vertices[0], + vertexB = vertices[1]; + + // find closest vertex on bodyB + for (var i = 0; i < vertices.length; i++) { + vertex = vertices[i]; + vertexToBody.x = vertex.x - bodyAPosition.x; + vertexToBody.y = vertex.y - bodyAPosition.y; + distance = -Vector.dot(normal, vertexToBody); + + if (distance < nearestDistance) { + nearestDistance = distance; + vertexA = vertex; + } + } + + // find next closest vertex using the two connected to it + var prevIndex = vertexA.index - 1 >= 0 ? vertexA.index - 1 : vertices.length - 1; + vertex = vertices[prevIndex]; + vertexToBody.x = vertex.x - bodyAPosition.x; + vertexToBody.y = vertex.y - bodyAPosition.y; + nearestDistance = -Vector.dot(normal, vertexToBody); + vertexB = vertex; + + var nextIndex = (vertexA.index + 1) % vertices.length; + vertex = vertices[nextIndex]; + vertexToBody.x = vertex.x - bodyAPosition.x; + vertexToBody.y = vertex.y - bodyAPosition.y; + distance = -Vector.dot(normal, vertexToBody); + if (distance < nearestDistance) { + nearestDistance = distance; + vertexB = vertex; + } + + return [vertexA, vertexB]; + }; + +})(); + +; // End src/collision/SAT.js + + +// Begin src/constraint/Constraint.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Constraint +*/ + +// TODO: fix instabillity issues with torque +// TODO: linked constraints +// TODO: breakable constraints +// TODO: collidable constraints +// TODO: allow constrained bodies to sleep +// TODO: handle 0 length constraints properly +// TODO: impulse caching and warming + +var Constraint = {}; + +(function() { + + var _minLength = 0.000001; + + /** + * Description + * @method create + * @param {} options + * @return {constraint} constraint + */ + Constraint.create = function(options) { + var constraint = options; + + // if bodies defined but no points, use body centre + if (constraint.bodyA && !constraint.pointA) + constraint.pointA = { x: 0, y: 0 }; + if (constraint.bodyB && !constraint.pointB) + constraint.pointB = { x: 0, y: 0 }; + + // calculate static length using initial world space points + var initialPointA = constraint.bodyA ? Vector.add(constraint.bodyA.position, constraint.pointA) : constraint.pointA, + initialPointB = constraint.bodyB ? Vector.add(constraint.bodyB.position, constraint.pointB) : constraint.pointB, + length = Vector.magnitude(Vector.sub(initialPointA, initialPointB)); + + constraint.length = constraint.length || length || _minLength; + + // option defaults + constraint.lineWidth = constraint.lineWidth || 2; + constraint.strokeStyle = constraint.strokeStyle || '#666'; + constraint.stiffness = constraint.stiffness || 1; + constraint.angularStiffness = constraint.angularStiffness || 0; + constraint.angleA = constraint.bodyA ? constraint.bodyA.angle : constraint.angleA; + constraint.angleB = constraint.bodyB ? constraint.bodyB.angle : constraint.angleB; + + return constraint; + }; + + /** + * Description + * @method updateAll + * @param {constraint[]} constraints + */ + Constraint.updateAll = function(constraints) { + for (var i = 0; i < constraints.length; i++) { + Constraint.update(constraints[i]); + } + }; + + /** + * Description + * @method update + * @param {constraint} constraint + */ + Constraint.update = function(constraint) { + var bodyA = constraint.bodyA, + bodyB = constraint.bodyB, + pointA = constraint.pointA, + pointB = constraint.pointB; + + // update reference angle + if (bodyA && !bodyA.isStatic) { + constraint.pointA = Vector.rotate(pointA, bodyA.angle - constraint.angleA); + constraint.angleA = bodyA.angle; + } + + // update reference angle + if (bodyB && !bodyB.isStatic) { + constraint.pointB = Vector.rotate(pointB, bodyB.angle - constraint.angleB); + constraint.angleB = bodyB.angle; + } + + var pointAWorld = pointA, + pointBWorld = pointB; + + if (bodyA) pointAWorld = Vector.add(bodyA.position, pointA); + if (bodyB) pointBWorld = Vector.add(bodyB.position, pointB); + + if (!pointAWorld || !pointBWorld) + return; + + var delta = Vector.sub(pointAWorld, pointBWorld), + currentLength = Vector.magnitude(delta); + + // prevent singularity + if (currentLength === 0) + currentLength = _minLength; + + // solve distance constraint with Gauss-Siedel method + var difference = (currentLength - constraint.length) / currentLength, + normal = Vector.div(delta, currentLength), + force = Vector.mult(delta, difference * 0.5 * constraint.stiffness); + + var velocityPointA, + velocityPointB, + offsetA, + offsetB, + oAn, + oBn, + bodyADenom, + bodyBDenom; + + if (bodyA && !bodyA.isStatic) { + // point body offset + offsetA = { + x: pointAWorld.x - bodyA.position.x + force.x, + y: pointAWorld.y - bodyA.position.y + force.y + }; + + // update velocity + bodyA.velocity.x = bodyA.position.x - bodyA.positionPrev.x; + bodyA.velocity.y = bodyA.position.y - bodyA.positionPrev.y; + bodyA.angularVelocity = bodyA.angle - bodyA.anglePrev; + + // find point velocity and body mass + velocityPointA = Vector.add(bodyA.velocity, Vector.mult(Vector.perp(offsetA), bodyA.angularVelocity)); + oAn = Vector.dot(offsetA, normal); + bodyADenom = bodyA.inverseMass + bodyA.inverseInertia * oAn * oAn; + } else { + velocityPointA = { x: 0, y: 0 }; + bodyADenom = bodyA ? bodyA.inverseMass : 0; + } + + if (bodyB && !bodyB.isStatic) { + // point body offset + offsetB = { + x: pointBWorld.x - bodyB.position.x - force.x, + y: pointBWorld.y - bodyB.position.y - force.y + }; + + // update velocity + bodyB.velocity.x = bodyB.position.x - bodyB.positionPrev.x; + bodyB.velocity.y = bodyB.position.y - bodyB.positionPrev.y; + bodyB.angularVelocity = bodyB.angle - bodyB.anglePrev; + + // find point velocity and body mass + velocityPointB = Vector.add(bodyB.velocity, Vector.mult(Vector.perp(offsetB), bodyB.angularVelocity)); + oBn = Vector.dot(offsetB, normal); + bodyBDenom = bodyB.inverseMass + bodyB.inverseInertia * oBn * oBn; + } else { + velocityPointB = { x: 0, y: 0 }; + bodyBDenom = bodyB ? bodyB.inverseMass : 0; + } + + var relativeVelocity = Vector.sub(velocityPointB, velocityPointA), + normalImpulse = Vector.dot(normal, relativeVelocity) / (bodyADenom + bodyBDenom); + + if (normalImpulse > 0) normalImpulse = 0; + + var normalVelocity = { + x: normal.x * normalImpulse, + y: normal.y * normalImpulse + }; + + var torque; + + if (bodyA && !bodyA.isStatic) { + torque = Vector.cross(offsetA, normalVelocity) * bodyA.inverseInertia * (1 - constraint.angularStiffness); + + Sleeping.set(bodyA, false); + + // clamp to prevent instabillity + // TODO: solve this properlly + torque = Common.clamp(torque, -0.01, 0.01); + + // apply forces + bodyA.position.x -= force.x; + bodyA.position.y -= force.y; + bodyA.angle += torque; + + // update geometry + Vertices.translate(bodyA.vertices, force, -1); + Vertices.rotate(bodyA.vertices, torque, bodyA.position); + Axes.rotate(bodyA.axes, torque); + Bounds.update(bodyA.bounds, bodyA.vertices, bodyA.velocity); + } + + if (bodyB && !bodyB.isStatic) { + torque = Vector.cross(offsetB, normalVelocity) * bodyB.inverseInertia * (1 - constraint.angularStiffness); + + Sleeping.set(bodyB, false); + + // clamp to prevent instabillity + // TODO: solve this properlly + torque = Common.clamp(torque, -0.01, 0.01); + + // apply forces + bodyB.position.x += force.x; + bodyB.position.y += force.y; + bodyB.angle -= torque; + + // update geometry + Vertices.translate(bodyB.vertices, force); + Vertices.rotate(bodyB.vertices, -torque, bodyB.position); + Axes.rotate(bodyB.axes, -torque); + Bounds.update(bodyB.bounds, bodyB.vertices, bodyB.velocity); + } + + }; + +})(); + +; // End src/constraint/Constraint.js + + +// Begin src/constraint/MouseConstraint.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class MouseConstraint +*/ + +var MouseConstraint = {}; + +(function() { + + /** + * Description + * @method create + * @param {mouse} mouse + * @return {MouseConstraint} A new MouseConstraint + */ + MouseConstraint.create = function(mouse) { + var constraint = Constraint.create({ + pointA: mouse.position, + pointB: { x: 0, y: 0 }, + length: 0.01, + stiffness: 0.1, + angularStiffness: 1, + strokeStyle: 'lightgreen', + lineWidth: 3 + }); + + return { + mouse: mouse, + dragBody: null, + dragPoint: null, + constraints: [constraint] + }; + }; + + /** + * Description + * @method update + * @param {MouseConstraint} mouseConstraint + * @param {body[]} bodies + */ + MouseConstraint.update = function(mouseConstraint, bodies) { + var mouse = mouseConstraint.mouse, + constraint = mouseConstraint.constraints[0]; + + if (mouse.button === 0 || mouse.button === 2) { + if (!constraint.bodyB) { + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i]; + if (Bounds.contains(body.bounds, mouse.position) + && Vertices.contains(body.vertices, mouse.position)) { + constraint.pointA = mouse.position; + constraint.bodyB = body; + constraint.pointB = { x: mouse.position.x - body.position.x, y: mouse.position.y - body.position.y }; + constraint.angleB = body.angle; + Sleeping.set(body, false); + } + } + } + } else { + constraint.bodyB = null; + constraint.pointB = null; + } + + if (constraint.bodyB) { + Sleeping.set(constraint.bodyB, false); + constraint.pointA = mouse.position; + } + }; + +})(); + +; // End src/constraint/MouseConstraint.js + + +// Begin src/core/Common.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Common +*/ + +var Common = {}; + +(function() { + + /** + * Description + * @method extend + * @param {} obj, obj, obj... + * @return {} obj extended + */ + Common.extend = function(obj) { + var args = Array.prototype.slice.call(arguments, 1); + + for (var i = 0; i < args.length; i++) { + var source = args[i]; + + if (source) { + for (var prop in source) { + if (source[prop].constructor === Object) { + if (!obj[prop] || obj[prop].constructor === Object) { + obj[prop] = obj[prop] || {}; + Common.extend(obj[prop], source[prop]); + } else { + obj[prop] = source[prop]; + } + } else { + obj[prop] = source[prop]; + } + } + } + } + + return obj; + }; + + /** + * Description + * @method keys + * @param {} obj + * @return {string[]} keys + */ + Common.keys = function(obj) { + if (Object.keys) + return Object.keys(obj); + + // avoid hasOwnProperty for performance + var keys = []; + for (var key in obj) + keys.push(key); + return keys; + }; + + /** + * Description + * @method values + * @param {} obj + * @return {array} Array of the objects property values + */ + Common.values = function(obj) { + var values = []; + + if (Object.keys) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + values.push(obj[keys[i]]); + } + return values; + } + + // avoid hasOwnProperty for performance + for (var key in obj) + values.push(obj[key]); + return values; + }; + + /** + * Description + * @method shadeColor + * @param {string} color + * @param {number} percent + * @return {string} A hex colour string made by lightening or darkening color by percent + */ + Common.shadeColor = function(color, percent) { + // http://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color + var colorInteger = parseInt(color.slice(1),16), + amount = Math.round(2.55 * percent), + R = (colorInteger >> 16) + amount, + B = (colorInteger >> 8 & 0x00FF) + amount, + G = (colorInteger & 0x0000FF) + amount; + return "#" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R :255) * 0x10000 + + (B < 255 ? B < 1 ? 0 : B : 255) * 0x100 + + (G < 255 ? G < 1 ? 0 : G : 255)).toString(16).slice(1); + }; + + /** + * Description + * @method shuffle + * @param {array} array + * @return {array} array shuffled randomly + */ + Common.shuffle = function(array) { + for (var i = array.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + return array; + }; + + /** + * Description + * @method choose + * @param {array} choices + * @return {object} A random choice object from the array + */ + Common.choose = function(choices) { + return choices[Math.floor(Math.random() * choices.length)]; + }; + + /** + * Description + * @method isElement + * @param {object} obj + * @return {boolean} True if the object is a HTMLElement, otherwise false + */ + Common.isElement = function(obj) { + // http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object + try { + return obj instanceof HTMLElement; + } + catch(e){ + return (typeof obj==="object") && + (obj.nodeType===1) && (typeof obj.style === "object") && + (typeof obj.ownerDocument ==="object"); + } + }; + + /** + * Description + * @method clamp + * @param {number} value + * @param {number} min + * @param {number} max + * @return {number} The value clamped between min and max inclusive + */ + Common.clamp = function(value, min, max) { + if (value < min) + return min; + if (value > max) + return max; + return value; + }; + + /** + * Description + * @method sign + * @param {number} value + * @return {number} -1 if negative, +1 if 0 or positive + */ + Common.sign = function(value) { + return value < 0 ? -1 : 1; + }; + + /** + * Description + * @method now + * @return {Date} the current DateTime + */ + Common.now = Date.now || function() { + // http://stackoverflow.com/questions/221294/how-do-you-get-a-timestamp-in-javascript + return +(new Date()); + }; + + /** + * Description + * @method random + * @param {number} min + * @param {number} max + * @return {number} A random number between min and max inclusive + */ + Common.random = function(min, max) { + return min + Math.random() * (max - min); + }; + +})(); + +; // End src/core/Common.js + + +// Begin src/core/Engine.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Engine +*/ + +// TODO: multiple event handlers, before & after handlers +// TODO: viewports +// TODO: frameskipping + +var Engine = {}; + +(function() { + + var _fps = 60, + _deltaSampleSize = 8, + _delta = 1000 / _fps; + + var _requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame + || window.mozRequestAnimationFrame || window.msRequestAnimationFrame + || function(callback){ window.setTimeout(function() { callback(Common.now()); }, _delta); }; + + /** + * Description + * @method create + * @param {HTMLElement} element + * @param {object} options + * @return {engine} engine + */ + Engine.create = function(element, options) { + var defaults = { + enabled: true, + positionIterations: 6, + velocityIterations: 4, + constraintIterations: 1, + pairs: {}, + pairsList: [], + enableSleeping: false, + timeScale: 1, + input: {}, + timing: { + fps: _fps, + timestamp: 0, + delta: _delta, + correction: 1, + deltaMin: 1000 / _fps, + deltaMax: 1000 / (_fps * 0.5) + } + }; + + var engine = Common.extend(defaults, options); + engine = Common.isElement(element) ? (engine || {}) : element; + + // create default renderer only if no custom renderer set + // but still allow engine.render.engine to pass through if set + if (!engine.render || (engine.render && !engine.render.controller)) { + engine.render = Render.create(engine.render); + if (Common.isElement(element)) + element.appendChild(engine.render.canvas); + } + + engine.world = World.create(engine.world); + engine.metrics = engine.metrics || Metrics.create(); + engine.input.mouse = engine.input.mouse || Mouse.create(engine.render.canvas); + engine.mouseConstraint = engine.mouseConstraint || MouseConstraint.create(engine.input.mouse); + World.addComposite(engine.world, engine.mouseConstraint); + + engine.broadphase = engine.broadphase || { + current: 'grid', + grid: { + controller: Grid, + instance: Grid.create(), + detector: Detector.collisions + }, + bruteForce: { + detector: Detector.bruteForce + } + }; + + engine.events = { + tick: function(engine) { + Engine.update(engine, engine.timing.delta, engine.timing.correction); + }, + render: function(engine) { + if (engine.render.options.enabled) + engine.render.controller.world(engine); + } + }; + + return engine; + }; + + /** + * Description + * @method run + * @param {engine} engine + */ + Engine.run = function(engine) { + var timing = engine.timing, + delta, + counterTimestamp = 0, + frameCounter = 0, + deltaHistory = []; + + (function render(timestamp){ + _requestAnimationFrame(render); + + if (!engine.enabled) + return; + + delta = (timestamp - timing.timestamp) || _delta; + + // optimistically filter delta over a few frames, to improve stability + deltaHistory.push(delta); + deltaHistory = deltaHistory.slice(-_deltaSampleSize); + delta = Math.min.apply(null, deltaHistory); + + // limit delta + delta = delta < engine.timing.deltaMin ? engine.timing.deltaMin : delta; + delta = delta > engine.timing.deltaMax ? engine.timing.deltaMax : delta; + + timing.timestamp = timestamp; + timing.correction = delta / timing.delta; + timing.delta = delta; + + frameCounter += 1; + + if (timestamp - counterTimestamp >= 1000) { + timing.fps = frameCounter * ((timestamp - counterTimestamp) / 1000); + counterTimestamp = timestamp; + frameCounter = 0; + } + + engine.events.tick(engine); + engine.events.render(engine); + })(); + }; + + /** + * Description + * @method update + * @param {engine} engine + * @param {number} delta + * @param {number} correction + * @return engine + */ + Engine.update = function(engine, delta, correction) { + var world = engine.world, + broadphase = engine.broadphase[engine.broadphase.current], + broadphasePairs = [], + i; + + Body.resetForcesAll(world.bodies, world.gravity); + Metrics.reset(engine.metrics); + + MouseConstraint.update(engine.mouseConstraint, world.bodies, engine.input); + Body.updateAll(world.bodies, delta * engine.timeScale, correction, world.bounds); + + for (i = 0; i < engine.constraintIterations; i++) { + Constraint.updateAll(world.constraints); + } + + if (broadphase.controller) { + broadphase.controller.update(broadphase.instance, world.bodies, engine); + broadphasePairs = broadphase.instance.pairsList; + } else { + broadphasePairs = world.bodies; + } + + var pairsUpdated = Manager.updatePairs(engine.pairs, engine.pairsList, broadphasePairs, engine.metrics, broadphase.detector), + pairsRemoved = Manager.removeOldPairs(engine.pairs, engine.pairsList); + + if (pairsUpdated || pairsRemoved) + engine.pairsList = Common.values(engine.pairs); + + // wake up bodies involved in collisions + if (engine.enableSleeping) + Sleeping.afterCollisions(engine.pairsList); + + // iteratively resolve velocity between collisions + Resolver.preSolveVelocity(engine.pairsList); + for (i = 0; i < engine.velocityIterations; i++) { + Resolver.solveVelocity(engine.pairsList); + } + + // iteratively resolve position between collisions + for (i = 0; i < engine.positionIterations; i++) { + Resolver.solvePosition(engine.pairsList); + } + Resolver.postSolvePosition(world.bodies); + + if (engine.enableSleeping) + Sleeping.update(world.bodies); + + Metrics.update(engine.metrics, engine); + + return engine; + }; + + /** + * Description + * @method merge + * @param {engine} engineA + * @param {engine} engineB + */ + Engine.merge = function(engineA, engineB) { + Common.extend(engineA, engineB); + + if (engineB.world) { + engineA.world = engineB.world; + + Engine.clear(engineA); + + var bodies = engineA.world.bodies; + + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i]; + Sleeping.set(body, false); + body.id = Body.nextId(); + } + + var broadphase = engineA.broadphase[engineA.broadphase.current]; + + if (broadphase.controller === Grid) { + var grid = broadphase.instance; + Grid.clear(grid); + Grid.update(grid, engineA.world.bodies, engineA, true); + } + } + }; + + /** + * Description + * @method clear + * @param {engine} engine + */ + Engine.clear = function(engine) { + var world = engine.world; + + engine.pairs = {}; + engine.pairsList = []; + + World.addComposite(engine.world, engine.mouseConstraint); + + var broadphase = engine.broadphase[engine.broadphase.current]; + + if (broadphase.controller === Grid) + Grid.clear(broadphase.instance); + + if (broadphase.controller) { + broadphase.controller.update(broadphase.instance, world.bodies, engine, true); + } + }; + +})(); + +; // End src/core/Engine.js + + +// Begin src/core/Metrics.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Metrics +*/ + +var Metrics = {}; + +(function() { + + /** + * Description + * @method create + * @return {metrics} A new metrics + */ + Metrics.create = function() { + return { + narrowDetections: 0, + narrowphaseTests: 0, + midphaseTests: 0, + broadphaseTests: 0, + narrowEff: 0.0001, + midEff: 0.0001, + broadEff: 0.0001, + collisions: 0, + buckets: 0, + bodies: 0, + pairs: 0 + }; + }; + + /** + * Description + * @method reset + * @param {metrics} metrics + */ + Metrics.reset = function(metrics) { + metrics.narrowDetections = 0; + metrics.narrowphaseTests = 0; + metrics.midphaseTests = 0; + metrics.broadphaseTests = 0; + metrics.narrowEff = 0; + metrics.midEff = 0; + metrics.broadEff = 0; + metrics.collisions = 0; + metrics.buckets = 0; + metrics.pairs = 0; + metrics.bodies = 0; + }; + + /** + * Description + * @method update + * @param {metrics} metrics + * @param {engine} engine + */ + Metrics.update = function(metrics, engine) { + var world = engine.world, + broadphase = engine.broadphase[engine.broadphase.current]; + + metrics.collisions = metrics.narrowDetections; + metrics.pairs = engine.pairsList.length; + metrics.bodies = world.bodies.length; + metrics.midEff = (metrics.narrowDetections / (metrics.midphaseTests || 1)).toFixed(2); + metrics.narrowEff = (metrics.narrowDetections / (metrics.narrowphaseTests || 1)).toFixed(2); + metrics.broadEff = (1 - (metrics.broadphaseTests / (world.bodies.length || 1))).toFixed(2); + if (broadphase.instance) + metrics.buckets = Common.keys(broadphase.instance.buckets).length; + }; + +})(); + +; // End src/core/Metrics.js + + +// Begin src/core/Mouse.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Mouse +*/ + +var Mouse; + +(function() { + + /** + * Description + * @param {HTMLElement} element + */ + Mouse = function(element) { + var mouse = this; + + element = element || document.body; + + this.position = { x: 0, y: 0 }; + this.mousedownPosition = { x: 0, y: 0 }; + this.mouseupPosition = { x: 0, y: 0 }; + this.button = -1; + + var mousemove = function(event) { + var position = _getRelativeMousePosition(event, element), + touches = event.changedTouches; + + if (touches) { + mouse.button = 0; + event.preventDefault(); + } + + mouse.position = position; + }; + + var mousedown = function(event) { + var position = _getRelativeMousePosition(event, element), + touches = event.changedTouches; + + if (touches) { + mouse.button = 0; + event.preventDefault(); + } else { + mouse.button = event.button; + } + + mouse.position = mouse.mousedownPosition = position; + }; + + var mouseup = function(event) { + var position = _getRelativeMousePosition(event, element), + touches = event.changedTouches; + + if (touches) { + event.preventDefault(); + } + + mouse.button = -1; + + mouse.position = mouse.mouseupPosition = position; + }; + + element.addEventListener('mousemove', mousemove); + element.addEventListener('mousedown', mousedown); + element.addEventListener('mouseup', mouseup); + + element.addEventListener('touchmove', mousemove); + element.addEventListener('touchstart', mousedown); + element.addEventListener('touchend', mouseup); + }; + + /** + * Description + * @method create + * @param {HTMLElement} element + * @return {mouse} A new mouse + */ + Mouse.create = function(element) { + return new Mouse(element); + }; + + /** + * Description + * @method _getRelativeMousePosition + * @private + * @param {} event + * @param {} element + * @return ObjectExpression + */ + var _getRelativeMousePosition = function(event, element) { + var elementBounds = element.getBoundingClientRect(), + scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft, + scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop, + touches = event.changedTouches, + x, y; + + if (touches) { + x = touches[0].pageX - elementBounds.left - scrollX; + y = touches[0].pageY - elementBounds.top - scrollY; + } else { + x = event.pageX - elementBounds.left - scrollX; + y = event.pageY - elementBounds.top - scrollY; + } + + return { + x: x / (element.clientWidth / element.width), + y: y / (element.clientHeight / element.height) + }; + }; + +})(); + +; // End src/core/Mouse.js + + +// Begin src/core/Sleeping.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Sleeping +*/ + +var Sleeping = {}; + +(function() { + + var _motionWakeThreshold = 0.18, + _motionSleepThreshold = 0.08, + _minBias = 0.9; + + /** + * Description + * @method update + * @param {body[]} bodies + */ + Sleeping.update = function(bodies) { + // update bodies sleeping status + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i], + motion = body.speed * body.speed + body.angularSpeed * body.angularSpeed; + + var minMotion = Math.min(body.motion, motion), + maxMotion = Math.max(body.motion, motion); + + // biased average motion estimation between frames + body.motion = _minBias * minMotion + (1 - _minBias) * maxMotion; + + if (body.sleepThreshold > 0 && body.motion < _motionSleepThreshold) { + body.sleepCounter += 1; + + if (body.sleepCounter >= body.sleepThreshold) + Sleeping.set(body, true); + } else if (body.sleepCounter > 0) { + body.sleepCounter -= 1; + } + } + }; + + /** + * Description + * @method afterCollisions + * @param {pair[]} pairs + */ + Sleeping.afterCollisions = function(pairs) { + // wake up bodies involved in collisions + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i], + collision = pair.collision, + bodyA = collision.bodyA, + bodyB = collision.bodyB; + + if ((bodyA.isSleeping && bodyB.isSleeping) || bodyA.isStatic || bodyB.isStatic) + continue; + + if (bodyA.isSleeping || bodyB.isSleeping) { + var sleepingBody = (bodyA.isSleeping && !bodyA.isStatic) ? bodyA : bodyB, + movingBody = sleepingBody === bodyA ? bodyB : bodyA; + + if (!sleepingBody.isStatic && movingBody.motion > _motionWakeThreshold) { + Sleeping.set(sleepingBody, false); + } + } + } + }; + + /** + * Description + * @method set + * @param {body} body + * @param {boolean} isSleeping + */ + Sleeping.set = function(body, isSleeping) { + if (isSleeping) { + body.isSleeping = true; + body.sleepCounter = body.sleepThreshold; + + body.positionImpulse.x = 0; + body.positionImpulse.y = 0; + + body.positionPrev.x = body.position.x; + body.positionPrev.y = body.position.y; + + body.anglePrev = body.angle; + body.speed = 0; + body.angularSpeed = 0; + body.motion = 0; + } else { + body.isSleeping = false; + body.sleepCounter = 0; + } + }; + +})(); + +; // End src/core/Sleeping.js + + +// Begin src/factory/Bodies.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Bodies +*/ + +// TODO: true circle bodies + +var Bodies = {}; + +(function() { + + /** + * Description + * @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 = { + position: { x: x, y: y }, + path: 'L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height + }; + + return Body.create(Common.extend({}, rectangle, options)); + }; + + /** + * Description + * @method trapezoid + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + * @param {number} slope + * @param {object} options + * @return {body} A new trapezoid body + */ + Bodies.trapezoid = function(x, y, width, height, slope, options) { + options = options || {}; + + slope *= 0.5; + var roof = (1 - (slope * 2)) * width; + + var x1 = width * slope, + x2 = x1 + roof, + x3 = x2 + x1; + + var trapezoid = { + position: { x: x, y: y }, + path: 'L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0' + }; + + return Body.create(Common.extend({}, trapezoid, options)); + }; + + /** + * Description + * @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 || {}; + + // 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; + + // flag for better rendering + options.circleRadius = radius; + + return Bodies.polygon(x, y, sides, radius, options); + }; + + /** + * Description + * @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 = { + position: { x: x, y: y }, + path: path + }; + + return Body.create(Common.extend({}, polygon, options)); + }; + +})(); + +; // End src/factory/Bodies.js + + +// Begin src/factory/Composites.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Composites +*/ + +var Composites = {}; + +(function() { + + /** + * Description + * @method stack + * @param {number} xx + * @param {number} yy + * @param {number} columns + * @param {number} rows + * @param {number} columnGap + * @param {number} rowGap + * @param {function} callback + * @return {composite} A new composite containing objects created in the callback + */ + Composites.stack = function(xx, yy, columns, rows, columnGap, rowGap, callback) { + var stack = Composite.create(), + x = xx, + y = yy, + lastBody, + i = 0; + + for (var row = 0; row < rows; row++) { + var maxHeight = 0; + + for (var column = 0; column < columns; column++) { + var body = callback(x, y, column, row, lastBody, i); + + if (body) { + var bodyHeight = body.bounds.max.y - body.bounds.min.y, + bodyWidth = body.bounds.max.x - body.bounds.min.x; + + if (bodyHeight > maxHeight) + maxHeight = bodyHeight; + + Body.translate(body, { x: bodyWidth * 0.5, y: bodyHeight * 0.5 }); + + x = body.bounds.max.x + columnGap; + + Composite.addBody(stack, body); + + lastBody = body; + i += 1; + } + } + + y += maxHeight + rowGap; + x = xx; + } + + return stack; + }; + + /** + * Description + * @method chain + * @param {composite} composite + * @param {number} xOffsetA + * @param {number} yOffsetA + * @param {number} xOffsetB + * @param {number} yOffsetB + * @param {object} options + * @return {composite} A new composite containing objects chained together with constraints + */ + Composites.chain = function(composite, xOffsetA, yOffsetA, xOffsetB, yOffsetB, options) { + var bodies = composite.bodies; + + for (var i = 1; i < bodies.length; i++) { + var bodyA = bodies[i - 1], + bodyB = bodies[i], + bodyAHeight = bodyA.bounds.max.y - bodyA.bounds.min.y, + bodyAWidth = bodyA.bounds.max.x - bodyA.bounds.min.x, + bodyBHeight = bodyB.bounds.max.y - bodyB.bounds.min.y, + bodyBWidth = bodyB.bounds.max.x - bodyB.bounds.min.x; + + var defaults = { + bodyA: bodyA, + pointA: { x: bodyAWidth * xOffsetA, y: bodyAHeight * yOffsetA }, + bodyB: bodyB, + pointB: { x: bodyBWidth * xOffsetB, y: bodyBHeight * yOffsetB } + }; + + var constraint = Common.extend(defaults, options); + + Composite.addConstraint(composite, Constraint.create(constraint)); + } + + return composite; + }; + + /** + * Description + * @method pyramid + * @param {number} xx + * @param {number} yy + * @param {number} columns + * @param {number} rows + * @param {number} columnGap + * @param {number} rowGap + * @param {function} callback + * @return {composite} A new composite containing objects created in the callback + */ + Composites.pyramid = function(xx, yy, columns, rows, columnGap, rowGap, callback) { + return Composites.stack(xx, yy, columns, rows, columnGap, rowGap, function(x, y, column, row, lastBody, i) { + var actualRows = Math.min(rows, Math.ceil(columns / 2)), + lastBodyWidth = lastBody ? lastBody.bounds.max.x - lastBody.bounds.min.x : 0; + + if (row > actualRows) + return; + + // reverse row order + row = actualRows - row; + + var start = row, + end = columns - 1 - row; + + if (column < start || column > end) + return; + + // retroactively fix the first body's position, since width was unknown + if (i === 1) { + Body.translate(lastBody, { x: (column + (columns % 2 === 1 ? 1 : -1)) * lastBodyWidth, y: 0 }); + } + + var xOffset = lastBody ? column * lastBodyWidth : 0; + + return callback(xx + xOffset + column * columnGap, y, column, row, lastBody, i); + }); + }; + + /** + * Description + * @method newtonsCradle + * @param {number} xx + * @param {number} yy + * @param {number} number + * @param {number} size + * @param {number} length + * @return {composite} A new composite newtonsCradle body + */ + Composites.newtonsCradle = function(xx, yy, number, size, length) { + var newtonsCradle = Composite.create(); + + for (var i = 0; i < number; i++) { + var separation = 1.9, + circle = Bodies.circle(xx + i * (size * separation), yy + length, size, + { restitution: 1, friction: 0, frictionAir: 0.0001, slop: 0.01 }), + constraint = Constraint.create({ pointA: { x: xx + i * (size * separation), y: yy }, bodyB: circle }); + + Composite.addBody(newtonsCradle, circle); + Composite.addConstraint(newtonsCradle, constraint); + } + + return newtonsCradle; + }; + + /** + * Description + * @method car + * @param {number} xx + * @param {number} yy + * @param {number} width + * @param {number} height + * @param {number} wheelSize + * @return {composite} A new composite car body + */ + Composites.car = function(xx, yy, width, height, wheelSize) { + var groupId = Body.nextGroupId(), + wheelBase = -20, + wheelAOffset = -width * 0.5 + wheelBase, + wheelBOffset = width * 0.5 - wheelBase, + wheelYOffset = 0; + + var car = Composite.create(), + body = Bodies.trapezoid(xx, yy, width, height, 0.3, { groupId: groupId, friction: 0.01 }); + + var wheelA = Bodies.circle(xx + wheelAOffset, yy + wheelYOffset, wheelSize, { + groupId: groupId, + restitution: 0.5, + friction: 0.9, + density: 0.01 + }); + + var wheelB = Bodies.circle(xx + wheelBOffset, yy + wheelYOffset, wheelSize, { + groupId: groupId, + restitution: 0.5, + friction: 0.9, + density: 0.01 + }); + + var axelA = Constraint.create({ + bodyA: body, + pointA: { x: wheelAOffset, y: wheelYOffset }, + bodyB: wheelA, + stiffness: 0.5 + }); + + var axelB = Constraint.create({ + bodyA: body, + pointA: { x: wheelBOffset, y: wheelYOffset }, + bodyB: wheelB, + stiffness: 0.5 + }); + + Composite.addBody(car, body); + Composite.addBody(car, wheelA); + Composite.addBody(car, wheelB); + Composite.addConstraint(car, axelA); + Composite.addConstraint(car, axelB); + + return car; + }; + +})(); + +; // End src/factory/Composites.js + + +// Begin src/geometry/Axes.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Axes +*/ + +var Axes = {}; + +(function() { + + /** + * Description + * @method fromVertices + * @param {vertices} vertices + * @return {axes} A new axes from the given vertices + */ + Axes.fromVertices = function(vertices) { + var axes = {}; + + // find the unique axes, using edge normal gradients + for (var i = 0; i < vertices.length; i++) { + var j = (i + 1) % vertices.length, + normal = Vector.normalise({ + x: vertices[j].y - vertices[i].y, + y: vertices[i].x - vertices[j].x + }), + gradient = (normal.y === 0) ? Infinity : (normal.x / normal.y); + + // limit precision + gradient = gradient.toFixed(3).toString(); + + axes[gradient] = normal; + } + + return Common.values(axes); + }; + + /** + * Description + * @method rotate + * @param {axes} axes + * @param {number} angle + */ + Axes.rotate = function(axes, angle) { + if (angle === 0) + return; + + var cos = Math.cos(angle), + sin = Math.sin(angle); + + for (var i = 0; i < axes.length; i++) { + var axis = axes[i], + xx; + xx = axis.x * cos - axis.y * sin; + axis.y = axis.x * sin + axis.y * cos; + axis.x = xx; + } + }; + +})(); + +; // End src/geometry/Axes.js + + +// Begin src/geometry/Bounds.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Bounds +*/ + +var Bounds = {}; + +(function() { + + /** + * Description + * @method create + * @param {vertices} vertices + * @return {bounds} A new bounds object + */ + Bounds.create = function(vertices) { + var bounds = { + min: { x: 0, y: 0 }, + max: { x: 0, y: 0 } + }; + Bounds.update(bounds, vertices); + return bounds; + }; + + /** + * Description + * @method update + * @param {bounds} bounds + * @param {vertices} vertices + * @param {vector} velocity + */ + Bounds.update = function(bounds, vertices, velocity) { + bounds.min.x = Number.MAX_VALUE; + bounds.max.x = Number.MIN_VALUE; + bounds.min.y = Number.MAX_VALUE; + bounds.max.y = Number.MIN_VALUE; + + for (var i = 0; i < vertices.length; i++) { + var vertex = vertices[i]; + if (vertex.x > bounds.max.x) bounds.max.x = vertex.x; + if (vertex.x < bounds.min.x) bounds.min.x = vertex.x; + if (vertex.y > bounds.max.y) bounds.max.y = vertex.y; + if (vertex.y < bounds.min.y) bounds.min.y = vertex.y; + } + + if (velocity) { + if (velocity.x > 0) { + bounds.max.x += velocity.x; + } else { + bounds.min.x += velocity.x; + } + + if (velocity.y > 0) { + bounds.max.y += velocity.y; + } else { + bounds.min.y += velocity.y; + } + } + }; + + /** + * Description + * @method contains + * @param {bounds} bounds + * @param {vector} point + * @return {boolean} True if the bounds contain the point, otherwise false + */ + Bounds.contains = function(bounds, point) { + return point.x >= bounds.min.x && point.x <= bounds.max.x + && point.y >= bounds.min.y && point.y <= bounds.max.y; + }; + + /** + * Description + * @method overlaps + * @param {bounds} boundsA + * @param {bounds} boundsB + * @return {boolean} True if the bounds overlap, otherwise false + */ + Bounds.overlaps = function(boundsA, boundsB) { + return (boundsA.min.x <= boundsB.max.x && boundsA.max.x >= boundsB.min.x + && boundsA.max.y >= boundsB.min.y && boundsA.min.y <= boundsB.max.y); + }; + +})(); + +; // End src/geometry/Bounds.js + + +// Begin src/geometry/Vector.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Vector +*/ + +// TODO: consider params for reusing vector objects + +var Vector = {}; + +(function() { + + /** + * Description + * @method magnitude + * @param {vector} vector + * @return {number} The magnitude of the vector + */ + Vector.magnitude = function(vector) { + return Math.sqrt((vector.x * vector.x) + (vector.y * vector.y)); + }; + + /** + * Description + * @method magnitudeSquared + * @param {vector} vector + * @return {number} The squared magnitude of the vector + */ + Vector.magnitudeSquared = function(vector) { + return (vector.x * vector.x) + (vector.y * vector.y); + }; + + /** + * Description + * @method rotate + * @param {vector} vector + * @param {number} angle + * @return {vector} A new vector rotated + */ + Vector.rotate = function(vector, angle) { + var cos = Math.cos(angle), sin = Math.sin(angle); + return { + x: vector.x * cos - vector.y * sin, + y: vector.x * sin + vector.y * cos + }; + }; + + /** + * Description + * @method rotateAbout + * @param {vector} vector + * @param {number} angle + * @param {vector} point + * @return {vector} A new vector rotated about the point + */ + Vector.rotateAbout = function(vector, angle, point) { + var cos = Math.cos(angle), sin = Math.sin(angle); + return { + x: point.x + ((vector.x - point.x) * cos - (vector.y - point.y) * sin), + y: point.y + ((vector.x - point.x) * sin + (vector.y - point.y) * cos) + }; + }; + + /** + * Description + * @method normalise + * @param {vector} vector + * @return {vector} A new vector normalised + */ + Vector.normalise = function(vector) { + var magnitude = Vector.magnitude(vector); + if (magnitude === 0) + return { x: 0, y: 0 }; + return { x: vector.x / magnitude, y: vector.y / magnitude }; + }; + + /** + * Description + * @method dot + * @param {vector} vectorA + * @param {vector} vectorB + * @return {number} The dot product of the two vectors + */ + Vector.dot = function(vectorA, vectorB) { + return (vectorA.x * vectorB.x) + (vectorA.y * vectorB.y); + }; + + /** + * Description + * @method cross + * @param {vector} vectorA + * @param {vector} vectorB + * @return {number} The cross product of the two vectors + */ + Vector.cross = function(vectorA, vectorB) { + return (vectorA.x * vectorB.y) - (vectorA.y * vectorB.x); + }; + + /** + * Description + * @method add + * @param {vector} vectorA + * @param {vector} vectorB + * @return {vector} A new vector added + */ + Vector.add = function(vectorA, vectorB) { + return { x: vectorA.x + vectorB.x, y: vectorA.y + vectorB.y }; + }; + + /** + * Description + * @method sub + * @param {vector} vectorA + * @param {vector} vectorB + * @return {vector} A new vector subtracted + */ + Vector.sub = function(vectorA, vectorB) { + return { x: vectorA.x - vectorB.x, y: vectorA.y - vectorB.y }; + }; + + /** + * Description + * @method mult + * @param {vector} vector + * @param {number} scalar + * @return {vector} A new vector multiplied by scalar + */ + Vector.mult = function(vector, scalar) { + return { x: vector.x * scalar, y: vector.y * scalar }; + }; + + /** + * Description + * @method div + * @param {vector} vector + * @param {number} scalar + * @return {vector} A new vector divided by scalar + */ + Vector.div = function(vector, scalar) { + return { x: vector.x / scalar, y: vector.y / scalar }; + }; + + /** + * Description + * @method perp + * @param {vector} vector + * @param {bool} negate + * @return {vector} The perpendicular vector + */ + Vector.perp = function(vector, negate) { + negate = negate === true ? -1 : 1; + return { x: negate * -vector.y, y: negate * vector.x }; + }; + + /** + * Description + * @method neg + * @param {vector} vector + * @return {vector} The negated vector + */ + Vector.neg = function(vector) { + return { x: -vector.x, y: -vector.y }; + }; + +})(); + +; // End src/geometry/Vector.js + + +// Begin src/geometry/Vertices.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Vertices +*/ + +// TODO: convex decomposition - http://mnbayazit.com/406/bayazit + +var Vertices = {}; + +(function() { + + /** + * Description + * @method create + * @param {vertices} vertices + * @param {body} body + */ + Vertices.create = function(vertices, body) { + for (var i = 0; i < vertices.length; i++) { + vertices[i].index = i; + vertices[i].body = body; + } + }; + + /** + * Description + * @method fromPath + * @param {string} path + * @return {vertices} vertices + */ + Vertices.fromPath = function(path) { + var pathPattern = /L\s*([\-\d\.]*)\s*([\-\d\.]*)/ig, + vertices = []; + + path.replace(pathPattern, function(match, x, y) { + vertices.push({ x: parseFloat(x, 10), y: parseFloat(y, 10) }); + }); + + return vertices; + }; + + /** + * Description + * @method centre + * @param {vertices} vertices + * @return {vector} The centre point + */ + Vertices.centre = function(vertices) { + var cx = 0, cy = 0; + + for (var i = 0; i < vertices.length; i++) { + cx += vertices[i].x; + cy += vertices[i].y; + } + + return { x: cx / vertices.length, y: cy / vertices.length }; + }; + + /** + * Description + * @method area + * @param {vertices} vertices + * @return {number} The area + */ + Vertices.area = function(vertices) { + var area = 0, + j = vertices.length - 1; + + for (var i = 0; i < vertices.length; i++) { + area += (vertices[j].x - vertices[i].x) * (vertices[j].y + vertices[i].y); + j = i; + } + + return Math.abs(area) / 2; + }; + + /** + * Description + * @method inertia + * @param {vertices} vertices + * @param {number} mass + * @return {number} The polygon's moment of inertia, using second moment of area + */ + Vertices.inertia = function(vertices, mass) { + var numerator = 0, + denominator = 0, + v = vertices, + cross, + j; + + // find the polygon's moment of inertia, using second moment of area + // http://www.physicsforums.com/showthread.php?t=25293 + for (var n = 0; n < v.length; n++) { + j = (n + 1) % v.length; + cross = Math.abs(Vector.cross(v[j], v[n])); + numerator += cross * (Vector.dot(v[j], v[j]) + Vector.dot(v[j], v[n]) + Vector.dot(v[n], v[n])); + denominator += cross; + } + + return (mass / 6) * (numerator / denominator); + }; + + /** + * Description + * @method translate + * @param {vertices} vertices + * @param {vector} vector + * @param {number} scalar + */ + Vertices.translate = function(vertices, vector, scalar) { + var i; + if (scalar) { + for (i = 0; i < vertices.length; i++) { + vertices[i].x += vector.x * scalar; + vertices[i].y += vector.y * scalar; + } + } else { + for (i = 0; i < vertices.length; i++) { + vertices[i].x += vector.x; + vertices[i].y += vector.y; + } + } + }; + + /** + * Description + * @method rotate + * @param {vertices} vertices + * @param {number} angle + * @param {vector} point + */ + Vertices.rotate = function(vertices, angle, point) { + if (angle === 0) + return; + + var cos = Math.cos(angle), + sin = Math.sin(angle); + + for (var i = 0; i < vertices.length; i++) { + var vertice = vertices[i], + dx = vertice.x - point.x, + dy = vertice.y - point.y; + + vertice.x = point.x + (dx * cos - dy * sin); + vertice.y = point.y + (dx * sin + dy * cos); + } + }; + + /** + * Description + * @method contains + * @param {vertices} vertices + * @param {vector} point + * @return {boolean} True if the vertices contains point, otherwise false + */ + Vertices.contains = function(vertices, point) { + for (var i = 0; i < vertices.length; i++) { + var vertice = vertices[i], + nextVertice = vertices[(i + 1) % vertices.length]; + if ((point.x - vertice.x) * (nextVertice.y - vertice.y) + (point.y - vertice.y) * (vertice.x - nextVertice.x) > 0) { + return false; + } + } + + return true; + }; + +})(); + +; // End src/geometry/Vertices.js + + +// Begin src/render/Gui.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Gui +*/ + +var Gui = {}; + +(function() { + + /** + * Description + * @method create + * @param {engine} engine + * @param {object} options + * @return {gui} A container for a configured dat.gui + */ + Gui.create = function(engine, options) { + var _datGuiSupported = window.dat && window.localStorage, + _serializer; + + if (!_datGuiSupported) { + console.log("Could not create GUI. Check dat.gui library is loaded first."); + return; + } + + var datGui = new dat.GUI(options); + + if (Resurrect) { + _serializer = new Resurrect({ prefix: '$' }); + _serializer.parse = _serializer.resurrect; + } else { + _serializer = JSON; + } + + var gui = { + datGui: datGui, + amount: 1, + size: 40, + sides: 4, + density: 0.001, + restitution: 0, + friction: 0.1, + frictionAir: 0.01 + }; + + var funcs = { + + addBody: function() { + var options = { + density: gui.density, + friction: gui.friction, + frictionAir: gui.frictionAir, + restitution: gui.restitution + }; + + for (var i = 0; i < gui.amount; i++) { + World.addBody(engine.world, Bodies.polygon(120 + i * gui.size + i * 50, 200, gui.sides, gui.size, options)); + } + }, + + clear: function() { + World.clear(engine.world, true); + Engine.clear(engine); + }, + + save: function() { + if (localStorage && _serializer) { + localStorage.setItem('world', _serializer.stringify(engine.world)); + } + }, + + load: function() { + var loadedWorld; + + if (localStorage && _serializer) { + loadedWorld = _serializer.parse(localStorage.getItem('world')); + } + + if (loadedWorld) { + Engine.merge(engine, { world: loadedWorld }); + } + } + }; + + var metrics = datGui.addFolder('Metrics'); + metrics.add(engine.timing, 'fps').listen(); + metrics.add(engine.timing, 'delta').listen(); + metrics.add(engine.timing, 'correction').listen(); + metrics.add(engine.metrics, 'bodies').listen(); + metrics.add(engine.metrics, 'collisions').listen(); + metrics.add(engine.metrics, 'pairs').listen(); + metrics.add(engine.metrics, 'broadEff').listen(); + metrics.add(engine.metrics, 'midEff').listen(); + metrics.add(engine.metrics, 'narrowEff').listen(); + metrics.close(); + + var controls = datGui.addFolder('Add Body'); + controls.add(gui, 'amount', 1, 5).step(1); + controls.add(gui, 'size', 5, 150).step(1); + controls.add(gui, 'sides', 1, 8).step(1); + controls.add(gui, 'density', 0.0001, 0.01).step(0.001); + controls.add(gui, 'friction', 0, 1).step(0.05); + controls.add(gui, 'frictionAir', 0, gui.frictionAir * 10).step(gui.frictionAir / 10); + controls.add(gui, 'restitution', 0, 1).step(0.1); + controls.add(funcs, 'addBody'); + controls.open(); + + var worldGui = datGui.addFolder('World'); + worldGui.add(funcs, 'load'); + worldGui.add(funcs, 'save'); + worldGui.add(funcs, 'clear'); + worldGui.open(); + + var gravity = worldGui.addFolder('Gravity'); + gravity.add(engine.world.gravity, 'x', -1, 1).step(0.01); + gravity.add(engine.world.gravity, 'y', -1, 1).step(0.01); + gravity.open(); + + var physics = datGui.addFolder('Engine'); + physics.add(engine, 'enableSleeping'); + physics.add(engine.broadphase, 'current', ['grid', 'bruteForce']); + physics.add(engine, 'timeScale', 0.1, 2).step(0.1); + physics.add(engine, 'velocityIterations', 1, 10).step(1); + physics.add(engine, 'positionIterations', 1, 10).step(1); + physics.add(engine, 'enabled'); + physics.open(); + + var render = datGui.addFolder('Render'); + render.add(engine.render.options, 'wireframes'); + render.add(engine.render.options, 'showDebug'); + render.add(engine.render.options, 'showPositions'); + render.add(engine.render.options, 'showBroadphase'); + render.add(engine.render.options, 'showBounds'); + render.add(engine.render.options, 'showVelocity'); + render.add(engine.render.options, 'showCollisions'); + render.add(engine.render.options, 'showAxes'); + render.add(engine.render.options, 'showAngleIndicator'); + render.add(engine.render.options, 'showSleeping'); + render.add(engine.render.options, 'showIds'); + render.add(engine.render.options, 'showShadows'); + render.add(engine.render.options, 'enabled'); + render.open(); + + //datGui.remember(world) + + return gui; + }; + + /** + * Description + * @method update + * @param {gui} gui + * @param {datGui} datGui + */ + Gui.update = function(gui, datGui) { + var i; + datGui = datGui || gui.datGui; + + for (i in datGui.__folders) { + Gui.update(gui, datGui.__folders[i]); + } + + for (i in datGui.__controllers) { + var controller = datGui.__controllers[i]; + if (controller.updateDisplay) + controller.updateDisplay(); + } + }; + + /** + * Description + * @method closeAll + * @param {gui} gui + */ + Gui.closeAll = function(gui) { + var datGui = gui.datGui; + + for (var i in datGui.__folders) { + datGui.__folders[i].close(); + } + }; + +})(); + +; // End src/render/Gui.js + + +// Begin src/render/Render.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Render +*/ + +// TODO: viewports +// TODO: two.js, pixi.js + +var Render = {}; + +(function() { + + /** + * Description + * @method create + * @param {object} options + * @return {render} A new renderer + */ + Render.create = function(options) { + var defaults = { + controller: Render, + options: { + width: 800, + height: 600, + background: '#fafafa', + wireframeBackground: '#222', + enabled: true, + wireframes: true, + showSleeping: true, + showDebug: false, + showBroadphase: false, + showBounds: false, + showVelocity: false, + showCollisions: false, + showAxes: false, + showPositions: false, + showAngleIndicator: false, + showIds: false, + showShadows: false + } + }; + + var render = Common.extend(defaults, options); + + render.canvas = render.canvas || _createCanvas(render.options.width, render.options.height); + render.context = render.canvas.getContext('2d'); + + return render; + }; + + /** + * Description + * @method world + * @param {engine} engine + */ + Render.world = function(engine) { + var render = engine.render, + world = engine.world, + canvas = render.canvas, + context = render.context, + options = render.options, + i; + + if (options.wireframes) { + context.fillStyle = options.wireframeBackground; + } else { + context.fillStyle = options.background; + } + + context.fillRect(0, 0, canvas.width, canvas.height); + + if (options.showShadows && !options.wireframes) + for (i = 0; i < world.bodies.length; i++) + Render.bodyShadow(engine, world.bodies[i], context); + + for (i = 0; i < world.bodies.length; i++) + Render.body(engine, world.bodies[i], context); + + for (i = 0; i < world.constraints.length; i++) + Render.constraint(world.constraints[i], context); + + if (options.showCollisions) + for (i = 0; i < engine.pairsList.length; i++) + Render.collision(engine, engine.pairsList[i], context); + + if (options.showBroadphase && engine.broadphase.current === 'grid') + Render.grid(engine, engine.broadphase[engine.broadphase.current].instance, context); + + if (options.showDebug) + Render.debug(engine, context); + }; + + /** + * Description + * @method debug + * @param {engine} engine + * @param {RenderingContext} context + */ + Render.debug = function(engine, context) { + var c = context, + world = engine.world, + render = engine.render, + options = render.options, + space = " "; + + if (engine.timing.timestamp - (render.debugTimestamp || 0) >= 500) { + var text = ""; + text += "delta: " + engine.timing.delta.toFixed(3) + space; + text += "fps: " + Math.round(engine.timing.fps) + space; + text += "correction: " + engine.timing.correction.toFixed(3) + space; + text += "bodies: " + world.bodies.length + space; + + if (engine.broadphase.controller === Grid) + text += "buckets: " + engine.metrics.buckets + space; + + text += "\n"; + + text += "collisions: " + engine.metrics.collisions + space; + text += "pairs: " + engine.pairs.length + space; + text += "broad: " + engine.metrics.broadEff + space; + text += "mid: " + engine.metrics.midEff + space; + text += "narrow: " + engine.metrics.narrowEff + space; + render.debugString = text; + render.debugTimestamp = engine.timing.timestamp; + } + + if (render.debugString) { + c.font = "12px Arial"; + + if (options.wireframes) { + c.fillStyle = 'rgba(255,255,255,0.5)'; + } else { + c.fillStyle = 'rgba(0,0,0,0.5)'; + } + + var split = render.debugString.split('\n'); + + for (var i = 0; i < split.length; i++) { + c.fillText(split[i], 50, 50 + i * 18); + } + } + }; + + /** + * Description + * @method constraint + * @param {constraint} constraint + * @param {RenderingContext} context + */ + Render.constraint = function(constraint, context) { + var bodyA = constraint.bodyA, + bodyB = constraint.bodyB, + c = context; + + if (!constraint.pointA || !constraint.pointB) + return; + + if (bodyA) { + c.beginPath(); + c.moveTo(bodyA.position.x + constraint.pointA.x, bodyA.position.y + constraint.pointA.y); + } else { + c.beginPath(); + c.moveTo(constraint.pointA.x, constraint.pointA.y); + } + + if (bodyB) { + c.lineTo(bodyB.position.x + constraint.pointB.x, bodyB.position.y + constraint.pointB.y); + } else { + c.lineTo(constraint.pointB.x, constraint.pointB.y); + } + + c.lineWidth = constraint.lineWidth; + c.strokeStyle = constraint.strokeStyle; + c.stroke(); + }; + + /** + * Description + * @method bodyShadow + * @param {engine} engine + * @param {body} body + * @param {RenderingContext} context + */ + Render.bodyShadow = function(engine, body, context) { + var c = context, + render = engine.render; + + if (body.circleRadius) { + c.beginPath(); + c.arc(body.position.x, body.position.y, body.circleRadius, 0, 2 * Math.PI); + c.closePath(); + } else { + c.beginPath(); + c.moveTo(body.vertices[0].x, body.vertices[0].y); + for (var j = 1; j < body.vertices.length; j++) { + c.lineTo(body.vertices[j].x, body.vertices[j].y); + } + c.closePath(); + } + + var distanceX = body.position.x - render.options.width * 0.5, + distanceY = body.position.y - render.options.height * 0.2, + distance = Math.abs(distanceX) + Math.abs(distanceY); + + c.shadowColor = 'rgba(0,0,0,0.15)'; + c.shadowOffsetX = 0.05 * distanceX; + c.shadowOffsetY = 0.05 * distanceY; + c.shadowBlur = 1 + 12 * Math.min(1, distance / 1000); + + c.fill(); + + c.shadowColor = null; + c.shadowOffsetX = null; + c.shadowOffsetY = null; + c.shadowBlur = null; + }; + + /** + * Description + * @method body + * @param {engine} engine + * @param {body} body + * @param {RenderingContext} context + */ + Render.body = function(engine, body, context) { + var c = context, + render = engine.render, + options = render.options; + + // body bounds + if (options.showBounds) { + c.beginPath(); + c.rect(body.bounds.min.x, body.bounds.min.y, body.bounds.max.x - body.bounds.min.x, body.bounds.max.y - body.bounds.min.y); + c.lineWidth = 1; + if (options.wireframes) { + c.strokeStyle = 'rgba(255,255,255,0.08)'; + } else { + c.strokeStyle = 'rgba(0,0,0,0.1)'; + } + c.stroke(); + } + + // body polygon + if (body.circleRadius) { + c.beginPath(); + c.arc(body.position.x, body.position.y, body.circleRadius, 0, 2 * Math.PI); + c.closePath(); + } else { + c.beginPath(); + c.moveTo(body.vertices[0].x, body.vertices[0].y); + for (var j = 1; j < body.vertices.length; j++) { + c.lineTo(body.vertices[j].x, body.vertices[j].y); + } + c.closePath(); + } + + if (!options.wireframes) { + c.fillStyle = body.fillStyle; + if (options.showSleeping && body.isSleeping) + c.fillStyle = Common.shadeColor(body.fillStyle, 50); + c.lineWidth = body.lineWidth; + c.strokeStyle = body.strokeStyle; + c.fill(); + c.stroke(); + } else { + c.lineWidth = 1; + c.strokeStyle = '#bbb'; + if (options.showSleeping && body.isSleeping) + c.strokeStyle = 'rgba(255,255,255,0.2)'; + c.stroke(); + } + + // angle indicator + if (options.showAngleIndicator && !options.showAxes) { + c.beginPath(); + c.moveTo(body.position.x, body.position.y); + c.lineTo((body.vertices[0].x + body.vertices[body.vertices.length-1].x) / 2, + (body.vertices[0].y + body.vertices[body.vertices.length-1].y) / 2); + c.lineWidth = 1; + if (options.wireframes) { + c.strokeStyle = 'indianred'; + } else { + c.strokeStyle = body.strokeStyle; + } + c.stroke(); + } + + // axes + if (options.showAxes) { + for (var i = 0; i < body.axes.length; i++) { + var axis = body.axes[i]; + c.beginPath(); + c.moveTo(body.position.x, body.position.y); + c.lineTo(body.position.x + axis.x * 20, body.position.y + axis.y * 20); + c.lineWidth = 1; + if (options.wireframes) { + c.strokeStyle = 'indianred'; + } else { + c.strokeStyle = body.strokeStyle; + } + c.stroke(); + } + } + + // positions + if (options.showPositions) { + c.beginPath(); + c.arc(body.position.x, body.position.y, 3, 0, 2 * Math.PI, false); + if (options.wireframes) { + c.fillStyle = 'indianred'; + } else { + c.fillStyle = 'rgba(0,0,0,0.5)'; + } + c.fill(); + c.beginPath(); + c.arc(body.positionPrev.x, body.positionPrev.y, 2, 0, 2 * Math.PI, false); + c.fillStyle = 'rgba(255,165,0,0.8)'; + c.fill(); + } + + // body velocity vector + if (options.showVelocity) { + c.beginPath(); + c.moveTo(body.position.x, body.position.y); + c.lineTo(body.position.x + (body.position.x - body.positionPrev.x) * 2, body.position.y + (body.position.y - body.positionPrev.y) * 2); + c.lineWidth = 3; + c.strokeStyle = 'cornflowerblue'; + c.stroke(); + } + + // body id + if (options.showIds) { + c.font = "12px Arial"; + c.fillStyle = 'rgba(255,255,255,0.5)'; + c.fillText(body.id, body.position.x + 10, body.position.y - 10); + } + }; + + /** + * Description + * @method collision + * @param {engine} engine + * @param {pair} pair + * @param {RenderingContext} context + */ + Render.collision = function(engine, pair, context) { + var c = context, + collision = pair.collision, + options = engine.render.options; + + for (var i = 0; i < pair.activeContacts.length; i++) { + var contact = pair.activeContacts[i], + vertex = contact.vertex; + c.beginPath(); + //c.arc(vertex.x, vertex.y, 2.5, 0, 2 * Math.PI, false); + c.rect(vertex.x - 1.5, vertex.y - 1.5, 3.5, 3.5); + if (options.wireframes) { + c.fillStyle = 'rgba(255,255,255,0.7)'; + } else { + c.fillStyle = 'orange'; + } + c.fill(); + } + + if (pair.activeContacts.length > 0) { + var normalPosX = pair.activeContacts[0].vertex.x, + normalPosY = pair.activeContacts[0].vertex.y; + + if (pair.activeContacts.length === 2) { + normalPosX = (pair.activeContacts[0].vertex.x + pair.activeContacts[1].vertex.x) / 2; + normalPosY = (pair.activeContacts[0].vertex.y + pair.activeContacts[1].vertex.y) / 2; + } + + // collision normal + c.beginPath(); + c.moveTo(normalPosX - collision.normal.x * 8, normalPosY - collision.normal.y * 8); + c.lineWidth = 1; + c.lineTo(normalPosX, normalPosY); + if (options.wireframes) { + c.strokeStyle = 'rgba(255,165,0,0.7)'; + } else { + c.strokeStyle = 'orange'; + } + c.stroke(); + } + }; + + /** + * Description + * @method grid + * @param {engine} engine + * @param {grid} grid + * @param {RenderingContext} context + */ + Render.grid = function(engine, grid, context) { + var c = context, + options = engine.render.options; + + c.lineWidth = 1; + + if (options.wireframes) { + c.strokeStyle = 'rgba(255,180,0,0.1)'; + } else { + c.strokeStyle = 'rgba(255,180,0,0.5)'; + } + + var bucketKeys = Common.keys(grid.buckets); + + for (var i = 0; i < bucketKeys.length; i++) { + var bucketId = bucketKeys[i]; + + if (grid.buckets[bucketId].length < 2) + continue; + + var region = bucketId.split(','); + c.beginPath(); + c.rect(0.5 + parseInt(region[0], 10) * grid.bucketWidth, + 0.5 + parseInt(region[1], 10) * grid.bucketHeight, + grid.bucketWidth, + grid.bucketHeight); + c.stroke(); + } + }; + + /** + * Description + * @method _createCanvas + * @private + * @param {} width + * @param {} height + * @return canvas + */ + var _createCanvas = function(width, height) { + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.oncontextmenu = function() { return false; }; + canvas.onselectstart = function() { return false; }; + return canvas; + }; + +})(); + +; // End src/render/Render.js + + +// aliases + +World.addComposite = Composite.add; +World.addBody = Composite.addBody; +World.addConstraint = Composite.addConstraint; + +// exports + +Matter.Body = Body; +Matter.Composite = Composite; +Matter.World = World; +Matter.Contact = Contact; +Matter.Detector = Detector; +Matter.Grid = Grid; +Matter.Manager = Manager; +Matter.Pair = Pair; +Matter.Resolver = Resolver; +Matter.SAT = SAT; +Matter.Constraint = Constraint; +Matter.MouseConstraint = MouseConstraint; +Matter.Common = Common; +Matter.Engine = Engine; +Matter.Metrics = Metrics; +Matter.Mouse = Mouse; +Matter.Sleeping = Sleeping; +Matter.Bodies = Bodies; +Matter.Composites = Composites; +Matter.Axes = Axes; +Matter.Bounds = Bounds; +Matter.Vector = Vector; +Matter.Vertices = Vertices; +Matter.Gui = Gui; +Matter.Render = Render; + +// CommonJS module +if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = Matter; + } + exports.Matter = Matter; +} + +// AMD module +if (typeof define === 'function' && define.amd) { + define('Matter', [], function () { + return Matter; + }); +} + +// browser +if (typeof window === 'object' && typeof window.document === 'object') { + window.Matter = Matter; +} + +// End Matter namespace closure + +})(); \ No newline at end of file diff --git a/build/matter-dev.min.js b/build/matter-dev.min.js new file mode 100644 index 0000000..8a6460d --- /dev/null +++ b/build/matter-dev.min.js @@ -0,0 +1,8 @@ +/** +* matter-dev.min.js 0.5.0 2014-03-07 +* http://brm.io/matter-js/ +* License: MIT +*/ + +!function(){var a={},b={};!function(){var a=0,c=1;b.create=function(a){var c={id:b.nextId(),angle:0,position:{x:0,y:0},force:{x:0,y:0},torque:0,positionImpulse:{x:0,y:0},speed:0,angularSpeed:0,velocity:{x:0,y:0},angularVelocity:0,isStatic:!1,isSleeping:!1,motion:0,sleepThreshold:60,density:.001,restitution:0,friction:.1,frictionAir:.01,path:"L 0 0 L 40 0 L 40 40 L 0 40",fillStyle:a.isStatic?"#eeeeee":n.choose(["#556270","#4ECDC4","#C7F464","#FF6B6B","#C44D58"]),lineWidth:1.5,groupId:0,slop:.05},d=n.extend(c,a);return b.updateProperties(d),d},b.nextId=function(){return a++},b.nextGroupId=function(){return c++},b.updateProperties=function(a){a.vertices=a.vertices||x.fromPath(a.path),a.axes=a.axes||u.fromVertices(a.vertices),a.area=x.area(a.vertices),a.bounds=v.create(a.vertices),a.mass=a.mass||a.density*a.area,a.inverseMass=1/a.mass,a.inertia=a.inertia||x.inertia(a.vertices,a.mass),a.inverseInertia=1/a.inertia,a.positionPrev=a.positionPrev||{x:a.position.x,y:a.position.y},a.anglePrev=a.anglePrev||a.angle,a.strokeStyle=a.strokeStyle||n.shadeColor(a.fillStyle,-20),x.create(a.vertices,a);var b=x.centre(a.vertices);x.translate(a.vertices,a.position),x.translate(a.vertices,b,-1),x.rotate(a.vertices,a.angle,a.position),u.rotate(a.axes,a.angle),v.update(a.bounds,a.vertices,a.velocity),a.isStatic&&(a.restitution=0,a.friction=1,a.mass=a.inertia=a.density=1/0,a.inverseMass=a.inverseInertia=0,a.lineWidth=1),r.set(a,a.isSleeping)},b.resetForcesAll=function(a,b){for(var c=0;ce.max.x||g.bounds.max.ye.max.y||b.update(g,c,d)}},b.update=function(a,b,c){var d=b*b,e=1-a.frictionAir,f=a.position.x-a.positionPrev.x,g=a.position.y-a.positionPrev.y;a.velocity.x=f*e*c+a.force.x/a.mass*d,a.velocity.y=g*e*c+a.force.y/a.mass*d,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.position.x+=a.velocity.x,a.position.y+=a.velocity.y,a.angularVelocity=(a.angle-a.anglePrev)*e*c+a.torque/a.inertia*d,a.anglePrev=a.angle,a.angle+=a.angularVelocity,a.speed=w.magnitude(a.velocity),a.angularSpeed=Math.abs(a.angularVelocity),x.translate(a.vertices,a.velocity),x.rotate(a.vertices,a.angularVelocity,a.position),u.rotate(a.axes,a.angularVelocity),v.update(a.bounds,a.vertices,a.velocity)},b.applyForce=function(a,b,c){a.force.x+=c.x,a.force.y+=c.y;var d={x:b.x-a.position.x,y:b.y-a.position.y};a.torque+=(d.x*c.y-d.y*c.x)*a.inverseInertia},b.translate=function(a,b){a.positionPrev.x+=b.x,a.positionPrev.y+=b.y,a.position.x+=b.x,a.position.y+=b.y,x.translate(a.vertices,b),v.update(a.bounds,a.vertices,a.velocity)},b.rotate=function(a,b){a.anglePrev+=b,a.angle+=b,x.rotate(a.vertices,b,a.position),u.rotate(a.axes,b),v.update(a.bounds,a.vertices,a.velocity)}}();var c={};!function(){c.create=function(a){return n.extend({bodies:[],constraints:[],composites:[]},a)},c.add=function(a,b){return a.bodies&&b.bodies&&(a.bodies=a.bodies.concat(b.bodies)),a.constraints&&b.constraints&&(a.constraints=a.constraints.concat(b.constraints)),a.composites&&b.composites&&(a.composites=a.composites.concat(b.composites)),a},c.addBody=function(a,b){return a.bodies=a.bodies||[],a.bodies.push(b),a},c.addConstraint=function(a,b){return a.constraints=a.constraints||[],a.constraints.push(b),a}}();var d={};!function(){d.create=function(a){var b={gravity:{x:0,y:1},bodies:[],constraints:[],bounds:{min:{x:0,y:0},max:{x:800,y:600}}};return n.extend(b,a)},d.clear=function(a,b){a.bodies=b?a.bodies.filter(function(a){return a.isStatic}):[],a.constraints=[]}}();var e={};!function(){e.create=function(a){return{id:e.id(a),vertex:a,normalImpulse:0,tangentImpulse:0}},e.id=function(a){return a.body.id+"_"+a.index}}();var f={};!function(){f.collisions=function(a,b){for(var c=[],d=0;dq.bounds.width||u.bounds.max.y<0||u.bounds.min.y>q.bounds.height)){var v=b(c,u);if(!u.region||v.id!==u.region.id||k){s.broadphaseTests+=1,(!u.region||k)&&(u.region=v);var w=a(v,u.region);for(m=w.startCol;m<=w.endCol;m++)for(n=w.startRow;n<=w.endRow;n++){p=d(m,n),o=r[p];var x=m>=v.startCol&&m<=v.endCol&&n>=v.startRow&&n<=v.endRow,y=m>=u.region.startCol&&m<=u.region.endCol&&n>=u.region.startRow&&n<=u.region.endRow;!x&&y&&y&&o&&h(c,o,u),(u.region===v||x&&!y||k)&&(o||(o=e(r,p)),f(c,o,u))}u.region=v,t=!0}}}t&&(c.pairsList=j(c))},g.clear=function(a){a.buckets={},a.pairs={},a.pairsList=[]};var a=function(a,b){var d=Math.min(a.startCol,b.startCol),e=Math.max(a.endCol,b.endCol),f=Math.min(a.startRow,b.startRow),g=Math.max(a.endRow,b.endRow);return c(d,e,f,g)},b=function(a,b){var d=b.bounds,e=Math.floor(d.min.x/a.bucketWidth),f=Math.floor(d.max.x/a.bucketWidth),g=Math.floor(d.min.y/a.bucketHeight),h=Math.floor(d.max.y/a.bucketHeight);return c(e,f,g,h)},c=function(a,b,c,d){return{id:a+","+b+","+c+","+d,startCol:a,endCol:b,startRow:c,endRow:d}},d=function(a,b){return a+","+b},e=function(a,b){var c=a[b]=[];return c},f=function(a,b,c){for(var d=0;d0?g-1:0}}else b.splice(d,1)}},i=function(a,b){return a.id0&&d.push(c);return d}}();var h={};!function(){var a=500;h.updatePairs=function(a,b,c,d,e){var f;for(f=0;fa&&(delete b[g.id],f=!0)}return f}}();var i={};!function(){i.create=function(a){var b=a.bodyA,c=a.bodyB,d={id:i.id(b,c),contacts:{},activeContacts:[],separation:0,isActive:!0,timestamp:n.now(),inverseMass:b.inverseMass+c.inverseMass,friction:Math.min(b.friction,c.friction),restitution:Math.max(b.restitution,c.restitution),slop:Math.max(b.slop,c.slop)};return i.update(d,a),d},i.update=function(a,b){var c=a.contacts,d=b.supports,f=[];if(a.collision=b,b.collided){for(var g=0;gB*e.friction&&(C=B*e.friction*z);var D=w.cross(q,i),E=w.cross(r,i),F=l/(e.inverseMass+g.inverseInertia*D*D+h.inverseInertia*E*E);if(A*=F,C*=F,0>v&&v*v>a)o.normalImpulse=0,o.tangentImpulse=0;else{var G=o.normalImpulse;o.normalImpulse=Math.min(o.normalImpulse+A,0),A=o.normalImpulse-G;var H=o.tangentImpulse;o.tangentImpulse=n.clamp(o.tangentImpulse+C,-y,y),C=o.tangentImpulse-H}c.x=i.x*A+j.x*C,c.y=i.y*A+j.y*C,g.isStatic||g.isSleeping||(g.positionPrev.x+=c.x*g.inverseMass,g.positionPrev.y+=c.y*g.inverseMass,g.anglePrev+=w.cross(q,c)*g.inverseInertia),h.isStatic||h.isSleeping||(h.positionPrev.x-=c.x*h.inverseMass,h.positionPrev.y-=c.y*h.inverseMass,h.anglePrev-=w.cross(r,c)*h.inverseInertia)}}}}}();var k={};!function(){k.collides=function(b,d){var e,f,g,h={collided:!1,bodyA:b,bodyB:d};if(e=a(b.vertices,d.vertices,b.axes),0===e.overlap)return h;if(f=a(d.vertices,b.vertices,d.axes),0===f.overlap)return h;e.overlap0&&(h.normal=w.neg(h.normal)),h.tangent=w.perp(h.normal),h.penetration={x:h.normal.x*h.depth,y:h.normal.y*h.depth};var i=c(b,d,h.normal),j=[i[0]];if(x.contains(b.vertices,i[1]))j.push(i[1]);else{var k=c(d,b,w.neg(h.normal));x.contains(d.vertices,k[0])&&j.push(k[0]),j.length<2&&x.contains(d.vertices,k[1])&&j.push(k[1])}return h.supports=j,h.supportCorrected=w.sub(i[0],h.penetration),h};var a=function(a,c,d){for(var e,f,g={},h={},i={overlap:Number.MAX_VALUE},j=0;j=e)return{overlap:0};ee?e=g:d>g&&(d=g)}a.min=d,a.max=e},c=function(a,b,c){for(var d,e,f=Number.MAX_VALUE,g={x:0,y:0},h=b.vertices,i=a.position,j=h[0],k=h[1],l=0;ld&&(f=d,j=e);var m=j.index-1>=0?j.index-1:h.length-1;e=h[m],g.x=e.x-i.x,g.y=e.y-i.y,f=-w.dot(c,g),k=e;var n=(j.index+1)%h.length;return e=h[n],g.x=e.x-i.x,g.y=e.y-i.y,d=-w.dot(c,g),f>d&&(f=d,k=e),[j,k]}}();var l={};!function(){var a=1e-6;l.create=function(b){var c=b;c.bodyA&&!c.pointA&&(c.pointA={x:0,y:0}),c.bodyB&&!c.pointB&&(c.pointB={x:0,y:0});var d=c.bodyA?w.add(c.bodyA.position,c.pointA):c.pointA,e=c.bodyB?w.add(c.bodyB.position,c.pointB):c.pointB,f=w.magnitude(w.sub(d,e));return c.length=c.length||f||a,c.lineWidth=c.lineWidth||2,c.strokeStyle=c.strokeStyle||"#666",c.stiffness=c.stiffness||1,c.angularStiffness=c.angularStiffness||0,c.angleA=c.bodyA?c.bodyA.angle:c.angleA,c.angleB=c.bodyB?c.bodyB.angle:c.angleB,c},l.updateAll=function(a){for(var b=0;b0&&(C=0);var D,E={x:z.x*C,y:z.y*C};c&&!c.isStatic&&(D=w.cross(m,E)*c.inverseInertia*(1-b.angularStiffness),r.set(c,!1),D=n.clamp(D,-.01,.01),c.position.x-=A.x,c.position.y-=A.y,c.angle+=D,x.translate(c.vertices,A,-1),x.rotate(c.vertices,D,c.position),u.rotate(c.axes,D),v.update(c.bounds,c.vertices,c.velocity)),d&&!d.isStatic&&(D=w.cross(o,E)*d.inverseInertia*(1-b.angularStiffness),r.set(d,!1),D=n.clamp(D,-.01,.01),d.position.x+=A.x,d.position.y+=A.y,d.angle-=D,x.translate(d.vertices,A),x.rotate(d.vertices,-D,d.position),u.rotate(d.axes,-D),v.update(d.bounds,d.vertices,d.velocity))}}}();var m={};!function(){m.create=function(a){var b=l.create({pointA:a.position,pointB:{x:0,y:0},length:.01,stiffness:.1,angularStiffness:1,strokeStyle:"lightgreen",lineWidth:3});return{mouse:a,dragBody:null,dragPoint:null,constraints:[b]}},m.update=function(a,b){var c=a.mouse,d=a.constraints[0];if(0===c.button||2===c.button){if(!d.bodyB)for(var e=0;e>16)+d,f=(c>>8&255)+d,g=(255&c)+d;return"#"+(16777216+65536*(255>e?1>e?0:e:255)+256*(255>f?1>f?0:f:255)+(255>g?1>g?0:g:255)).toString(16).slice(1)},n.shuffle=function(a){for(var b=a.length-1;b>0;b--){var c=Math.floor(Math.random()*(b+1)),d=a[b];a[b]=a[c],a[c]=d}return a},n.choose=function(a){return a[Math.floor(Math.random()*a.length)]},n.isElement=function(a){try{return a instanceof HTMLElement}catch(b){return"object"==typeof a&&1===a.nodeType&&"object"==typeof a.style&&"object"==typeof a.ownerDocument}},n.clamp=function(a,b,c){return b>a?b:a>c?c:a},n.sign=function(a){return 0>a?-1:1},n.now=Date.now||function(){return+new Date},n.random=function(a,b){return a+Math.random()*(b-a)}}();var o={};!function(){var a=60,c=8,e=1e3/a,i=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(function(){a(n.now())},e)};o.create=function(b,c){var h={enabled:!0,positionIterations:6,velocityIterations:4,constraintIterations:1,pairs:{},pairsList:[],enableSleeping:!1,timeScale:1,input:{},timing:{fps:a,timestamp:0,delta:e,correction:1,deltaMin:1e3/a,deltaMax:1e3/(.5*a)}},i=n.extend(h,c);return i=n.isElement(b)?i||{}:b,(!i.render||i.render&&!i.render.controller)&&(i.render=z.create(i.render),n.isElement(b)&&b.appendChild(i.render.canvas)),i.world=d.create(i.world),i.metrics=i.metrics||p.create(),i.input.mouse=i.input.mouse||q.create(i.render.canvas),i.mouseConstraint=i.mouseConstraint||m.create(i.input.mouse),d.addComposite(i.world,i.mouseConstraint),i.broadphase=i.broadphase||{current:"grid",grid:{controller:g,instance:g.create(),detector:f.collisions},bruteForce:{detector:f.bruteForce}},i.events={tick:function(a){o.update(a,a.timing.delta,a.timing.correction)},render:function(a){a.render.options.enabled&&a.render.controller.world(a)}},i},o.run=function(a){var b,d=a.timing,f=0,g=0,h=[];!function j(k){i(j),a.enabled&&(b=k-d.timestamp||e,h.push(b),h=h.slice(-c),b=Math.min.apply(null,h),b=ba.timing.deltaMax?a.timing.deltaMax:b,d.timestamp=k,d.correction=b/d.delta,d.delta=b,g+=1,k-f>=1e3&&(d.fps=g*((k-f)/1e3),f=k,g=0),a.events.tick(a),a.events.render(a))}()},o.update=function(a,c,d){var e,f=a.world,g=a.broadphase[a.broadphase.current],i=[];for(b.resetForcesAll(f.bodies,f.gravity),p.reset(a.metrics),m.update(a.mouseConstraint,f.bodies,a.input),b.updateAll(f.bodies,c*a.timeScale,d,f.bounds),e=0;e0&&e.motion=e.sleepThreshold&&r.set(e,!0)):e.sleepCounter>0&&(e.sleepCounter-=1)}},r.afterCollisions=function(b){for(var c=0;ca&&r.set(h,!1)}}},r.set=function(a,b){b?(a.isSleeping=!0,a.sleepCounter=a.sleepThreshold,a.positionImpulse.x=0,a.positionImpulse.y=0,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.anglePrev=a.angle,a.speed=0,a.angularSpeed=0,a.motion=0):(a.isSleeping=!1,a.sleepCounter=0)}}();var s={};!function(){s.rectangle=function(a,c,d,e,f){f=f||{};var g={position:{x:a,y:c},path:"L 0 0 L "+d+" 0 L "+d+" "+e+" L 0 "+e};return b.create(n.extend({},g,f))},s.trapezoid=function(a,c,d,e,f,g){g=g||{},f*=.5;var h=(1-2*f)*d,i=d*f,j=i+h,k=j+i,l={position:{x:a,y:c},path:"L 0 0 L "+i+" "+-e+" L "+j+" "+-e+" L "+k+" 0"};return b.create(n.extend({},l,g))},s.circle=function(a,b,c,d,e){d=d||{},e=e||25;var f=Math.ceil(Math.max(10,Math.min(e,c)));return f%2===1&&(f+=1),d.circleRadius=c,s.polygon(a,b,f,c,d)},s.polygon=function(a,c,d,e,f){if(f=f||{},3>d)return s.circle(a,c,e,f);for(var g=2*Math.PI/d,h="",i=.5*g,j=0;d>j;j+=1){var k=i+j*g,l=Math.cos(k)*e,m=Math.sin(k)*e;h+="L "+l.toFixed(3)+" "+m.toFixed(3)+" "}var o={position:{x:a,y:c},path:h};return b.create(n.extend({},o,f))}}();var t={};!function(){t.stack=function(a,d,e,f,g,h,i){for(var j,k=c.create(),l=a,m=d,n=0,o=0;f>o;o++){for(var p=0,q=0;e>q;q++){var r=i(l,m,q,o,j,n);if(r){var s=r.bounds.max.y-r.bounds.min.y,t=r.bounds.max.x-r.bounds.min.x;s>p&&(p=s),b.translate(r,{x:.5*t,y:.5*s}),l=r.bounds.max.x+g,c.addBody(k,r),j=r,n+=1}}m+=p+h,l=a}return k},t.chain=function(a,b,d,e,f,g){for(var h=a.bodies,i=1;im)){j=m-j;var o=j,p=d-1-j;if(!(o>i||i>p)){1===l&&b.translate(k,{x:(i+(d%2===1?1:-1))*n,y:0});var q=k?i*n:0;return h(a+q+i*f,g,i,j,k,l)}}})},t.newtonsCradle=function(a,b,d,e,f){for(var g=c.create(),h=0;d>h;h++){var i=1.9,j=s.circle(a+h*e*i,b+f,e,{restitution:1,friction:0,frictionAir:1e-4,slop:.01}),k=l.create({pointA:{x:a+h*e*i,y:b},bodyB:j});c.addBody(g,j),c.addConstraint(g,k)}return g},t.car=function(a,d,e,f,g){var h=b.nextGroupId(),i=-20,j=.5*-e+i,k=.5*e-i,m=0,n=c.create(),o=s.trapezoid(a,d,e,f,.3,{groupId:h,friction:.01}),p=s.circle(a+j,d+m,g,{groupId:h,restitution:.5,friction:.9,density:.01}),q=s.circle(a+k,d+m,g,{groupId:h,restitution:.5,friction:.9,density:.01}),r=l.create({bodyA:o,pointA:{x:j,y:m},bodyB:p,stiffness:.5}),t=l.create({bodyA:o,pointA:{x:k,y:m},bodyB:q,stiffness:.5});return c.addBody(n,o),c.addBody(n,p),c.addBody(n,q),c.addConstraint(n,r),c.addConstraint(n,t),n}}();var u={};!function(){u.fromVertices=function(a){for(var b={},c=0;ca.max.x&&(a.max.x=e.x),e.xa.max.y&&(a.max.y=e.y),e.y0?a.max.x+=c.x:a.min.x+=c.x,c.y>0?a.max.y+=c.y:a.min.y+=c.y)},v.contains=function(a,b){return b.x>=a.min.x&&b.x<=a.max.x&&b.y>=a.min.y&&b.y<=a.max.y},v.overlaps=function(a,b){return a.min.x<=b.max.x&&a.max.x>=b.min.x&&a.max.y>=b.min.y&&a.min.y<=b.max.y}}();var w={};!function(){w.magnitude=function(a){return Math.sqrt(a.x*a.x+a.y*a.y)},w.magnitudeSquared=function(a){return a.x*a.x+a.y*a.y},w.rotate=function(a,b){var c=Math.cos(b),d=Math.sin(b);return{x:a.x*c-a.y*d,y:a.x*d+a.y*c}},w.rotateAbout=function(a,b,c){var d=Math.cos(b),e=Math.sin(b);return{x:c.x+((a.x-c.x)*d-(a.y-c.y)*e),y:c.y+((a.x-c.x)*e+(a.y-c.y)*d)}},w.normalise=function(a){var b=w.magnitude(a);return 0===b?{x:0,y:0}:{x:a.x/b,y:a.y/b}},w.dot=function(a,b){return a.x*b.x+a.y*b.y},w.cross=function(a,b){return a.x*b.y-a.y*b.x},w.add=function(a,b){return{x:a.x+b.x,y:a.y+b.y}},w.sub=function(a,b){return{x:a.x-b.x,y:a.y-b.y}},w.mult=function(a,b){return{x:a.x*b,y:a.y*b}},w.div=function(a,b){return{x:a.x/b,y:a.y/b}},w.perp=function(a,b){return b=b===!0?-1:1,{x:b*-a.y,y:b*a.x}},w.neg=function(a){return{x:-a.x,y:-a.y}}}();var x={};!function(){x.create=function(a,b){for(var c=0;c0)return!1}return!0}}();var y={};!function(){y.create=function(a,b){var c,e=window.dat&&window.localStorage;if(!e)return void console.log("Could not create GUI. Check dat.gui library is loaded first.");var f=new dat.GUI(b);Resurrect?(c=new Resurrect({prefix:"$"}),c.parse=c.resurrect):c=JSON;var g={datGui:f,amount:1,size:40,sides:4,density:.001,restitution:0,friction:.1,frictionAir:.01},h={addBody:function(){for(var b={density:g.density,friction:g.friction,frictionAir:g.frictionAir,restitution:g.restitution},c=0;c=500){var i=""; +i+="delta: "+a.timing.delta.toFixed(3)+h,i+="fps: "+Math.round(a.timing.fps)+h,i+="correction: "+a.timing.correction.toFixed(3)+h,i+="bodies: "+d.bodies.length+h,a.broadphase.controller===g&&(i+="buckets: "+a.metrics.buckets+h),i+="\n",i+="collisions: "+a.metrics.collisions+h,i+="pairs: "+a.pairs.length+h,i+="broad: "+a.metrics.broadEff+h,i+="mid: "+a.metrics.midEff+h,i+="narrow: "+a.metrics.narrowEff+h,e.debugString=i,e.debugTimestamp=a.timing.timestamp}if(e.debugString){c.font="12px Arial",c.fillStyle=f.wireframes?"rgba(255,255,255,0.5)":"rgba(0,0,0,0.5)";for(var j=e.debugString.split("\n"),k=0;k0){var j=b.activeContacts[0].vertex.x,k=b.activeContacts[0].vertex.y;2===b.activeContacts.length&&(j=(b.activeContacts[0].vertex.x+b.activeContacts[1].vertex.x)/2,k=(b.activeContacts[0].vertex.y+b.activeContacts[1].vertex.y)/2),d.beginPath(),d.moveTo(j-8*e.normal.x,k-8*e.normal.y),d.lineWidth=1,d.lineTo(j,k),d.strokeStyle=f.wireframes?"rgba(255,165,0,0.7)":"orange",d.stroke()}},z.grid=function(a,b,c){var d=c,e=a.render.options;d.lineWidth=1,d.strokeStyle=e.wireframes?"rgba(255,180,0,0.1)":"rgba(255,180,0,0.5)";for(var f=n.keys(b.buckets),g=0;g + + + + + + + + + + + Matter.js Demo (Dev. Build) + + +
+

Matter.js Demo (Dev. Build)

+

Mobile Demo · Project page · GitHub

+
+ + +
+
+
+ + \ No newline at end of file diff --git a/demo/js/lib/matter-dev.js b/demo/js/lib/matter-dev.js new file mode 100644 index 0000000..b596e36 --- /dev/null +++ b/demo/js/lib/matter-dev.js @@ -0,0 +1,4213 @@ +/** +* matter-dev.js 0.5.0 2014-03-07 +* http://brm.io/matter-js/ +* License: MIT +*/ + +/** + * The MIT License (MIT) + * + * Copyright (c) 2014 Liam Brummitt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +(function() { + +var Matter = {}; + +// Begin Matter namespace closure + +// All Matter modules are included below during build +// Outro.js then closes at the end of the file + + +// Begin src/body/Body.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Body +*/ + +var Body = {}; + +(function() { + + var _nextId = 0, + _nextGroupId = 1; + + /** + * Description to be written. + * @method create + * @param {} options + * @return {body} body + */ + Body.create = function(options) { + var defaults = { + id: Body.nextId(), + angle: 0, + position: { x: 0, y: 0 }, + force: { x: 0, y: 0 }, + torque: 0, + positionImpulse: { x: 0, y: 0 }, + speed: 0, + angularSpeed: 0, + velocity: { x: 0, y: 0 }, + angularVelocity: 0, + isStatic: false, + isSleeping: false, + motion: 0, + sleepThreshold: 60, + density: 0.001, + restitution: 0, + friction: 0.1, + frictionAir: 0.01, + path: 'L 0 0 L 40 0 L 40 40 L 0 40', + fillStyle: options.isStatic ? '#eeeeee' : Common.choose(['#556270', '#4ECDC4', '#C7F464', '#FF6B6B', '#C44D58']), + lineWidth: 1.5, + groupId: 0, + slop: 0.05 + }; + + var body = Common.extend(defaults, options); + + Body.updateProperties(body); + + return body; + }; + + /** + * Description + * @method nextId + * @return {Number} Unique bodyID + */ + Body.nextId = function() { + return _nextId++; + }; + + /** + * Description + * @method nextGroupId + * @return {Number} Unique groupID + */ + Body.nextGroupId = function() { + return _nextGroupId++; + }; + + /** + * Description + * @method updateProperties + * @param {body} body + */ + Body.updateProperties = function(body) { + // calculated properties + body.vertices = body.vertices || Vertices.fromPath(body.path); + body.axes = body.axes || Axes.fromVertices(body.vertices); + body.area = Vertices.area(body.vertices); + body.bounds = Bounds.create(body.vertices); + body.mass = body.mass || (body.density * body.area); + body.inverseMass = 1 / body.mass; + body.inertia = body.inertia || Vertices.inertia(body.vertices, body.mass); + body.inverseInertia = 1 / body.inertia; + body.positionPrev = body.positionPrev || { x: body.position.x, y: body.position.y }; + body.anglePrev = body.anglePrev || body.angle; + body.strokeStyle = body.strokeStyle || Common.shadeColor(body.fillStyle, -20); + + // update geometry + Vertices.create(body.vertices, body); + var centre = Vertices.centre(body.vertices); + Vertices.translate(body.vertices, body.position); + Vertices.translate(body.vertices, centre, -1); + Vertices.rotate(body.vertices, body.angle, body.position); + Axes.rotate(body.axes, body.angle); + Bounds.update(body.bounds, body.vertices, body.velocity); + + if (body.isStatic) { + body.restitution = 0; + body.friction = 1; + body.mass = body.inertia = body.density = Infinity; + body.inverseMass = body.inverseInertia = 0; + body.lineWidth = 1; + } + + Sleeping.set(body, body.isSleeping); + }; + + /** + * Description + * @method resetForcesAll + * @param {body[]} bodies + * @param {vector} gravity + */ + Body.resetForcesAll = function(bodies, gravity) { + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i]; + + if (body.isStatic || body.isSleeping) + continue; + + // reset force buffers + body.force.x = 0; + body.force.y = 0; + body.torque = 0; + + // apply gravity + body.force.y += body.mass * gravity.y * 0.001; + body.force.x += body.mass * gravity.x * 0.001; + } + }; + + /** + * Description + * @method updateAll + * @param {body[]} bodies + * @param {number} deltaTime + * @param {number} correction + * @param {bounds} worldBounds + */ + Body.updateAll = function(bodies, deltaTime, correction, worldBounds) { + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i]; + + if (body.isStatic || body.isSleeping) + continue; + + // don't update out of world bodies + // TODO: viewports + if (body.bounds.max.x < worldBounds.min.x || body.bounds.min.x > worldBounds.max.x + || body.bounds.max.y < worldBounds.min.y || body.bounds.min.y > worldBounds.max.y) + continue; + + Body.update(body, deltaTime, correction); + } + }; + + /** + * Description + * @method update + * @param {body} body + * @param {number} deltaTime + * @param {number} correction + */ + Body.update = function(body, deltaTime, correction) { + var deltaTimeSquared = deltaTime * deltaTime; + + // from the previous step + var frictionAir = 1 - body.frictionAir, + velocityPrevX = body.position.x - body.positionPrev.x, + velocityPrevY = body.position.y - body.positionPrev.y; + + // update velocity with verlet integration + body.velocity.x = (velocityPrevX * frictionAir * correction) + (body.force.x / body.mass) * deltaTimeSquared; + body.velocity.y = (velocityPrevY * frictionAir * correction) + (body.force.y / body.mass) * deltaTimeSquared; + + body.positionPrev.x = body.position.x; + body.positionPrev.y = body.position.y; + body.position.x += body.velocity.x; + body.position.y += body.velocity.y; + + // update angular velocity with verlet integration + body.angularVelocity = ((body.angle - body.anglePrev) * frictionAir * correction) + (body.torque / body.inertia) * deltaTimeSquared; + body.anglePrev = body.angle; + body.angle += body.angularVelocity; + + // track speed and acceleration + body.speed = Vector.magnitude(body.velocity); + body.angularSpeed = Math.abs(body.angularVelocity); + + // transform the body geometry + Vertices.translate(body.vertices, body.velocity); + Vertices.rotate(body.vertices, body.angularVelocity, body.position); + Axes.rotate(body.axes, body.angularVelocity); + Bounds.update(body.bounds, body.vertices, body.velocity); + }; + + /** + * Description + * @method applyForce + * @param {body} body + * @param {vector} position + * @param {vector} force + */ + Body.applyForce = function(body, position, force) { + body.force.x += force.x; + body.force.y += force.y; + var offset = { x: position.x - body.position.x, y: position.y - body.position.y }; + body.torque += (offset.x * force.y - offset.y * force.x) * body.inverseInertia; + }; + + /** + * Description + * @method translate + * @param {body} body + * @param {vector} translation + */ + Body.translate = function(body, translation) { + body.positionPrev.x += translation.x; + body.positionPrev.y += translation.y; + body.position.x += translation.x; + body.position.y += translation.y; + Vertices.translate(body.vertices, translation); + Bounds.update(body.bounds, body.vertices, body.velocity); + }; + + /** + * Description + * @method rotate + * @param {body} body + * @param {number} angle + */ + Body.rotate = function(body, angle) { + body.anglePrev += angle; + body.angle += angle; + Vertices.rotate(body.vertices, angle, body.position); + Axes.rotate(body.axes, angle); + Bounds.update(body.bounds, body.vertices, body.velocity); + }; + +})(); + +; // End src/body/Body.js + + +// Begin src/body/Composite.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Composite +*/ + +// TODO: composite translate, rotate + +var Composite = {}; + +(function() { + + /** + * Description + * @method create + * @param {} options + * @return {composite} A new composite + */ + Composite.create = function(options) { + return Common.extend({ bodies: [], constraints: [], composites: [] }, options); + }; + + /** + * Description + * @method add + * @param {composite} compositeA + * @param {composite} compositeB + * @return {composite} The original compositeA with the objects from compositeB added + */ + Composite.add = function(compositeA, compositeB) { + if (compositeA.bodies && compositeB.bodies) + compositeA.bodies = compositeA.bodies.concat(compositeB.bodies); + + if (compositeA.constraints && compositeB.constraints) + compositeA.constraints = compositeA.constraints.concat(compositeB.constraints); + + if (compositeA.composites && compositeB.composites) + compositeA.composites = compositeA.composites.concat(compositeB.composites); + + return compositeA; + }; + + /** + * Description + * @method addBody + * @param {composite} composite + * @param {body} body + * @return {composite} The original composite with the body added + */ + Composite.addBody = function(composite, body) { + composite.bodies = composite.bodies || []; + composite.bodies.push(body); + return composite; + }; + + /** + * Description + * @method addConstraint + * @param {composite} composite + * @param {constraint} constraint + * @return {composite} The original composite with the constraint added + */ + Composite.addConstraint = function(composite, constraint) { + composite.constraints = composite.constraints || []; + composite.constraints.push(constraint); + return composite; + }; + +})(); + +; // End src/body/Composite.js + + +// Begin src/body/World.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class World +*/ + +var World = {}; + +(function() { + + /** + * Description + * @method create + * @constructor + * @param {} options + * @return {world} A new world + */ + World.create = function(options) { + var defaults = { + gravity: { x: 0, y: 1 }, + bodies: [], + constraints: [], + bounds: { + min: { x: 0, y: 0 }, + max: { x: 800, y: 600 } + } + }; + + return Common.extend(defaults, options); + }; + + /** + * Description + * @method clear + * @param {world} world + * @param {boolean} keepStatic + */ + World.clear = function(world, keepStatic) { + world.bodies = keepStatic ? world.bodies.filter(function(body) { return body.isStatic; }) : []; + world.constraints = []; + }; + + // World is a Composite body + // see src/module/Outro.js for these aliases: + + /** + * An alias for Composite.add since World is also a Composite (see Outro.js) + * @method addComposite + * @param {world} world + * @param {composite} composite + * @return {world} The original world with the objects from composite added + */ + + /** + * An alias for Composite.addBody since World is also a Composite (see Outro.js) + * @method addBody + * @param {world} world + * @param {body} body + * @return {world} The original world with the body added + */ + + /** + * An alias for Composite.addConstraint since World is also a Composite (see Outro.js) + * @method addConstraint + * @param {world} world + * @param {constraint} constraint + * @return {world} The original world with the constraint added + */ + +})(); + +; // End src/body/World.js + + +// Begin src/collision/Contact.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Contact +*/ + +var Contact = {}; + +(function() { + + /** + * Description + * @method create + * @param {vertex} vertex + * @return {contact} A new contact + */ + Contact.create = function(vertex) { + return { + id: Contact.id(vertex), + vertex: vertex, + normalImpulse: 0, + tangentImpulse: 0 + }; + }; + + /** + * Description + * @method id + * @param {vertex} vertex + * @return {Number} Unique contactID + */ + Contact.id = function(vertex) { + return vertex.body.id + '_' + vertex.index; + }; + +})(); + +; // End src/collision/Contact.js + + +// Begin src/collision/Detector.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Detector +*/ + +// TODO: speculative contacts + +var Detector = {}; + +(function() { + + /** + * Description + * @method collisions + * @param {pair[]} pairs + * @param {metrics} metrics + * @return {array} collisions + */ + Detector.collisions = function(pairs, metrics) { + var collisions = []; + + for (var i = 0; i < pairs.length; i++) { + var bodyA = pairs[i][0], + bodyB = pairs[i][1]; + + // NOTE: could share a function for the below, but may drop performance? + + if (bodyA.groupId && bodyB.groupId && bodyA.groupId === bodyB.groupId) + continue; + + if ((bodyA.isStatic || bodyA.isSleeping) && (bodyB.isStatic || bodyB.isSleeping)) + continue; + + metrics.midphaseTests += 1; + + // mid phase + if (Bounds.overlaps(bodyA.bounds, bodyB.bounds)) { + + // narrow phase + var collision = SAT.collides(bodyA, bodyB); + + metrics.narrowphaseTests += 1; + + if (collision.collided) { + collisions.push(collision); + metrics.narrowDetections += 1; + } + } + } + + return collisions; + }; + + /** + * Description + * @method bruteForce + * @param {body[]} bodies + * @param {metrics} metrics + * @return {array} collisions + */ + Detector.bruteForce = function(bodies, metrics) { + var collisions = []; + + for (var i = 0; i < bodies.length; i++) { + for (var j = i + 1; j < bodies.length; j++) { + var bodyA = bodies[i], + bodyB = bodies[j]; + + // NOTE: could share a function for the below, but may drop performance? + + if (bodyA.groupId && bodyB.groupId && bodyA.groupId === bodyB.groupId) + continue; + + if ((bodyA.isStatic || bodyA.isSleeping) && (bodyB.isStatic || bodyB.isSleeping)) + continue; + + metrics.midphaseTests += 1; + + // mid phase + if (Bounds.overlaps(bodyA.bounds, bodyB.bounds)) { + + // narrow phase + var collision = SAT.collides(bodyA, bodyB); + + metrics.narrowphaseTests += 1; + + if (collision.collided) { + collisions.push(collision); + metrics.narrowDetections += 1; + } + } + } + } + + return collisions; + }; + +})(); + +; // End src/collision/Detector.js + + +// Begin src/collision/Grid.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Grid +*/ + +var Grid = {}; + +(function() { + + /** + * Description + * @method create + * @param {number} bucketWidth + * @param {number} bucketHeight + * @return {grid} A new grid + */ + Grid.create = function(bucketWidth, bucketHeight) { + return { + buckets: {}, + pairs: {}, + pairsList: [], + bucketWidth: bucketWidth || 48, + bucketHeight: bucketHeight || 48 + }; + }; + + /** + * Description + * @method update + * @param {grid} grid + * @param {body[]} bodies + * @param {engine} engine + * @param {boolean} forceUpdate + */ + Grid.update = function(grid, bodies, engine, forceUpdate) { + var i, col, row, + world = engine.world, + buckets = grid.buckets, + bucket, + bucketId, + metrics = engine.metrics, + gridChanged = false; + + metrics.broadphaseTests = 0; + + for (i = 0; i < bodies.length; i++) { + var body = bodies[i]; + + if (body.isSleeping) + continue; + + // don't update out of world bodies + if (body.bounds.max.x < 0 || body.bounds.min.x > world.bounds.width + || body.bounds.max.y < 0 || body.bounds.min.y > world.bounds.height) + continue; + + var newRegion = _getRegion(grid, body); + + // if the body has changed grid region + if (!body.region || newRegion.id !== body.region.id || forceUpdate) { + + metrics.broadphaseTests += 1; + + if (!body.region || forceUpdate) + body.region = newRegion; + + var union = _regionUnion(newRegion, body.region); + + // update grid buckets affected by region change + // iterate over the union of both regions + for (col = union.startCol; col <= union.endCol; col++) { + for (row = union.startRow; row <= union.endRow; row++) { + bucketId = _getBucketId(col, row); + bucket = buckets[bucketId]; + + var isInsideNewRegion = (col >= newRegion.startCol && col <= newRegion.endCol + && row >= newRegion.startRow && row <= newRegion.endRow); + + var isInsideOldRegion = (col >= body.region.startCol && col <= body.region.endCol + && row >= body.region.startRow && row <= body.region.endRow); + + // remove from old region buckets + if (!isInsideNewRegion && isInsideOldRegion) { + if (isInsideOldRegion) { + if (bucket) + _bucketRemoveBody(grid, bucket, body); + } + } + + // add to new region buckets + if (body.region === newRegion || (isInsideNewRegion && !isInsideOldRegion) || forceUpdate) { + if (!bucket) + bucket = _createBucket(buckets, bucketId); + _bucketAddBody(grid, bucket, body); + } + } + } + + // set the new region + body.region = newRegion; + + // flag changes so we can update pairs + gridChanged = true; + } + } + + // update pairs list only if pairs changed (i.e. a body changed region) + if (gridChanged) + grid.pairsList = _createActivePairsList(grid); + }; + + /** + * Description + * @method clear + * @param {grid} grid + */ + Grid.clear = function(grid) { + grid.buckets = {}; + grid.pairs = {}; + grid.pairsList = []; + }; + + /** + * Description + * @method _regionUnion + * @private + * @param {} regionA + * @param {} regionB + * @return CallExpression + */ + var _regionUnion = function(regionA, regionB) { + var startCol = Math.min(regionA.startCol, regionB.startCol), + endCol = Math.max(regionA.endCol, regionB.endCol), + startRow = Math.min(regionA.startRow, regionB.startRow), + endRow = Math.max(regionA.endRow, regionB.endRow); + + return _createRegion(startCol, endCol, startRow, endRow); + }; + + /** + * Description + * @method _getRegion + * @private + * @param {} grid + * @param {} body + * @return CallExpression + */ + var _getRegion = function(grid, body) { + var bounds = body.bounds, + startCol = Math.floor(bounds.min.x / grid.bucketWidth), + endCol = Math.floor(bounds.max.x / grid.bucketWidth), + startRow = Math.floor(bounds.min.y / grid.bucketHeight), + endRow = Math.floor(bounds.max.y / grid.bucketHeight); + + return _createRegion(startCol, endCol, startRow, endRow); + }; + + /** + * Description + * @method _createRegion + * @private + * @param {} startCol + * @param {} endCol + * @param {} startRow + * @param {} endRow + * @return ObjectExpression + */ + var _createRegion = function(startCol, endCol, startRow, endRow) { + return { + id: startCol + ',' + endCol + ',' + startRow + ',' + endRow, + startCol: startCol, + endCol: endCol, + startRow: startRow, + endRow: endRow + }; + }; + + /** + * Description + * @method _getBucketId + * @private + * @param {} column + * @param {} row + * @return BinaryExpression + */ + var _getBucketId = function(column, row) { + return column + ',' + row; + }; + + /** + * Description + * @method _createBucket + * @private + * @param {} buckets + * @param {} bucketId + * @return bucket + */ + var _createBucket = function(buckets, bucketId) { + var bucket = buckets[bucketId] = []; + return bucket; + }; + + /** + * Description + * @method _bucketAddBody + * @private + * @param {} grid + * @param {} bucket + * @param {} body + */ + var _bucketAddBody = function(grid, bucket, body) { + // add new pairs + for (var i = 0; i < bucket.length; i++) { + var bodyB = bucket[i]; + + if (body.id === bodyB.id || (body.isStatic && bodyB.isStatic)) + continue; + + // keep track of the number of buckets the pair exists in + // important for Grid.update to work + var pairId = _getPairId(body, bodyB); + if (grid.pairs[pairId]) { + grid.pairs[pairId][2] += 1; + } else { + grid.pairs[pairId] = [body, bodyB, 1]; + } + } + + // add to bodies (after pairs, otherwise pairs with self) + bucket.push(body); + }; + + /** + * Description + * @method _bucketRemoveBody + * @private + * @param {} grid + * @param {} bucket + * @param {} body + */ + var _bucketRemoveBody = function(grid, bucket, body) { + // remove from bodies + for (var i = 0; i < bucket.length; i++) { + var bodyB = bucket[i]; + + // when position of body in bucket array is found, remove it + if (bodyB.id === body.id) { + bucket.splice(i, 1); + continue; + } else { + // keep track of the number of buckets the pair exists in + // important for Grid.update to work + var pairId = _getPairId(body, bodyB); + if (grid.pairs[pairId]) { + var pairCount = grid.pairs[pairId][2]; + grid.pairs[pairId][2] = pairCount > 0 ? pairCount - 1 : 0; + } + } + } + }; + + /** + * Description + * @method _getPairId + * @private + * @param {} bodyA + * @param {} bodyB + */ + var _getPairId = function(bodyA, bodyB) { + if (bodyA.id < bodyB.id) { + return bodyA.id + ',' + bodyB.id; + } else { + return bodyB.id + ',' + bodyA.id; + } + }; + + /** + * Description + * @method _createActivePairsList + * @private + * @param {} grid + * @return pairs + */ + var _createActivePairsList = function(grid) { + var pairKeys, + pair, + pairs = []; + + // grid.pairs is used as a hashmap + pairKeys = Common.keys(grid.pairs); + + // iterate over grid.pairs + for (var k = 0; k < pairKeys.length; k++) { + pair = grid.pairs[pairKeys[k]]; + + // if pair exists in at least one bucket + // it is a pair that needs further collision testing so push it + if (pair[2] > 0) + pairs.push(pair); + } + + return pairs; + }; + +})(); + +; // End src/collision/Grid.js + + +// Begin src/collision/Manager.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Manager +*/ + +var Manager = {}; + +(function() { + + var _pairMaxIdleLife = 500; + + /** + * Description + * @method updatePairs + * @param {object} pairs + * @param {pair[]} pairsList + * @param {pair[]} candidatePairs + * @param {metrics} metrics + * @param {detector} detector + * @return {bool} pairsUpdated flag + */ + Manager.updatePairs = function(pairs, pairsList, candidatePairs, metrics, detector) { + var i; + + // first set all pairs inactive + for (i = 0; i < pairsList.length; i++) { + var pair = pairsList[i]; + Pair.setActive(pair, false); + } + + // detect collisions in current step + var pairsUpdated = false, + collisions = detector(candidatePairs, metrics); + + // set collision pairs to active, or create if pair is new + for (i = 0; i < collisions.length; i++) { + var collision = collisions[i], + pairId = Pair.id(collision.bodyA, collision.bodyB); + + if (pairId in pairs) { + Pair.update(pairs[pairId], collision); + } else { + pairs[pairId] = Pair.create(collision); + pairsUpdated = true; + } + } + + return pairsUpdated; + }; + + /** + * Description + * @method removeOldPairs + * @param {object} pairs + * @param {pair[]} pairsList + * @return {bool} pairsRemoved flag + */ + Manager.removeOldPairs = function(pairs, pairsList) { + var timeNow = Common.now(), + pairsRemoved = false, + i; + + for (i = 0; i < pairsList.length; i++) { + var pair = pairsList[i], + collision = pair.collision; + + // never remove sleeping pairs + if (collision.bodyA.isSleeping || collision.bodyB.isSleeping) { + pair.timestamp = timeNow; + continue; + } + + // if pair is inactive for too long, remove it + if (timeNow - pair.timestamp > _pairMaxIdleLife) { + delete pairs[pair.id]; + pairsRemoved = true; + } + } + + return pairsRemoved; + }; + +})(); + +; // End src/collision/Manager.js + + +// Begin src/collision/Pair.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Pair +*/ + +var Pair = {}; + +(function() { + + /** + * Description + * @method create + * @param {collision} collision + * @return {pair} A new pair + */ + Pair.create = function(collision) { + var bodyA = collision.bodyA, + bodyB = collision.bodyB; + + var pair = { + id: Pair.id(bodyA, bodyB), + contacts: {}, + activeContacts: [], + separation: 0, + isActive: true, + timestamp: Common.now(), + inverseMass: bodyA.inverseMass + bodyB.inverseMass, + friction: Math.min(bodyA.friction, bodyB.friction), + restitution: Math.max(bodyA.restitution, bodyB.restitution), + slop: Math.max(bodyA.slop, bodyB.slop) + }; + + Pair.update(pair, collision); + + return pair; + }; + + /** + * Description + * @method update + * @param {pair} pair + * @param {collision} collision + */ + Pair.update = function(pair, collision) { + var contacts = pair.contacts, + supports = collision.supports, + activeContacts = []; + + pair.collision = collision; + + if (collision.collided) { + for (var i = 0; i < supports.length; i++) { + var support = supports[i], + contactId = Contact.id(support); + + if (contactId in contacts) { + activeContacts.push(contacts[contactId]); + } else { + activeContacts.push(contacts[contactId] = Contact.create(support)); + } + } + + pair.activeContacts = activeContacts; + pair.separation = collision.depth; + Pair.setActive(pair, true); + } else { + Pair.setActive(pair, false); + } + }; + + /** + * Description + * @method setActive + * @param {pair} pair + * @param {bool} isActive + */ + Pair.setActive = function(pair, isActive) { + if (isActive) { + pair.isActive = true; + pair.timestamp = Common.now(); + } else { + pair.isActive = false; + pair.activeContacts = []; + } + }; + + /** + * Description + * @method id + * @param {body} bodyA + * @param {body} bodyB + * @return {number} Unique pairId + */ + Pair.id = function(bodyA, bodyB) { + if (bodyA.id < bodyB.id) { + return bodyA.id + '_' + bodyB.id; + } else { + return bodyB.id + '_' + bodyA.id; + } + }; + +})(); + +; // End src/collision/Pair.js + + +// Begin src/collision/Resolver.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Resolver +*/ + +var Resolver = {}; + +(function() { + + var _restingThresh = 4, + _positionDampen = 0.2, + _positionWarming = 0.6; + + /** + * Description + * @method solvePosition + * @param {pair[]} pairs + */ + Resolver.solvePosition = function(pairs) { + var i, + pair, + collision, + bodyA, + bodyB, + vertex, + vertexCorrected, + normal, + bodyBtoA; + + // find impulses required to resolve penetration + for (i = 0; i < pairs.length; i++) { + pair = pairs[i]; + + if (!pair.isActive) + continue; + + collision = pair.collision; + bodyA = collision.bodyA; + bodyB = collision.bodyB; + vertex = collision.supports[0]; + vertexCorrected = collision.supportCorrected; + normal = collision.normal; + + // get current separation between body edges involved in collision + bodyBtoA = Vector.sub(Vector.add(bodyB.positionImpulse, vertex), + Vector.add(bodyA.positionImpulse, vertexCorrected)); + + pair.separation = Vector.dot(normal, bodyBtoA); + } + + for (i = 0; i < pairs.length; i++) { + pair = pairs[i]; + + if (!pair.isActive) + continue; + + collision = pair.collision; + bodyA = collision.bodyA; + bodyB = collision.bodyB; + normal = collision.normal; + positionImpulse = (pair.separation * _positionDampen) - pair.slop; + + if (bodyA.isStatic || bodyB.isStatic) + positionImpulse *= 2; + + if (!(bodyA.isStatic || bodyA.isSleeping)) { + bodyA.positionImpulse.x += normal.x * positionImpulse; + bodyA.positionImpulse.y += normal.y * positionImpulse; + } + + if (!(bodyB.isStatic || bodyB.isSleeping)) { + bodyB.positionImpulse.x -= normal.x * positionImpulse; + bodyB.positionImpulse.y -= normal.y * positionImpulse; + } + } + }; + + /** + * Description + * @method postSolvePosition + * @param {body[]} bodies + */ + Resolver.postSolvePosition = function(bodies) { + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i]; + + if (body.positionImpulse.x !== 0 || body.positionImpulse.y !== 0) { + // move the body without changing velocity + body.position.x += body.positionImpulse.x; + body.position.y += body.positionImpulse.y; + body.positionPrev.x += body.positionImpulse.x; + body.positionPrev.y += body.positionImpulse.y; + + // update body geometry + Vertices.translate(body.vertices, body.positionImpulse); + Bounds.update(body.bounds, body.vertices, body.velocity); + + // dampen accumulator to warm the next step + body.positionImpulse.x *= _positionWarming; + body.positionImpulse.y *= _positionWarming; + } + } + }; + + /** + * Description + * @method preSolveVelocity + * @param {pair[]} pairs + */ + Resolver.preSolveVelocity = function(pairs) { + var impulse = {}, + i, + j, + pair, + contacts, + collision, + bodyA, + bodyB, + normal, + tangent, + contact, + contactVertex, + normalImpulse, + tangentImpulse, + offset; + + for (i = 0; i < pairs.length; i++) { + pair = pairs[i]; + + if (!pair.isActive) + continue; + + contacts = pair.activeContacts; + collision = pair.collision; + bodyA = collision.bodyA; + bodyB = collision.bodyB; + normal = collision.normal; + tangent = collision.tangent; + + // resolve each contact + for (j = 0; j < contacts.length; j++) { + contact = contacts[j]; + contactVertex = contact.vertex; + normalImpulse = contact.normalImpulse; + tangentImpulse = contact.tangentImpulse; + + // total impulse from contact + impulse.x = (normal.x * normalImpulse) + (tangent.x * tangentImpulse); + impulse.y = (normal.y * normalImpulse) + (tangent.y * tangentImpulse); + + // apply impulse from contact + if (!(bodyA.isStatic || bodyA.isSleeping)) { + offset = Vector.sub(contactVertex, bodyA.position); + bodyA.positionPrev.x += impulse.x * bodyA.inverseMass; + bodyA.positionPrev.y += impulse.y * bodyA.inverseMass; + bodyA.anglePrev += Vector.cross(offset, impulse) * bodyA.inverseInertia; + } + + if (!(bodyB.isStatic || bodyB.isSleeping)) { + offset = Vector.sub(contactVertex, bodyB.position); + bodyB.positionPrev.x -= impulse.x * bodyB.inverseMass; + bodyB.positionPrev.y -= impulse.y * bodyB.inverseMass; + bodyB.anglePrev -= Vector.cross(offset, impulse) * bodyB.inverseInertia; + } + } + } + }; + + /** + * Description + * @method solveVelocity + * @param {pair[]} pairs + */ + Resolver.solveVelocity = function(pairs) { + var impulse = {}; + + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i]; + + if (!pair.isActive) + continue; + + var collision = pair.collision, + bodyA = collision.bodyA, + bodyB = collision.bodyB, + normal = collision.normal, + tangent = collision.tangent, + contacts = pair.activeContacts, + contactShare = 1 / contacts.length; + + // update body velocities + bodyA.velocity.x = bodyA.position.x - bodyA.positionPrev.x; + bodyA.velocity.y = bodyA.position.y - bodyA.positionPrev.y; + bodyB.velocity.x = bodyB.position.x - bodyB.positionPrev.x; + bodyB.velocity.y = bodyB.position.y - bodyB.positionPrev.y; + bodyA.angularVelocity = bodyA.angle - bodyA.anglePrev; + bodyB.angularVelocity = bodyB.angle - bodyB.anglePrev; + + // resolve each contact + for (var j = 0; j < contacts.length; j++) { + var contact = contacts[j], + contactVertex = contact.vertex, + offsetA = Vector.sub(contactVertex, bodyA.position), + offsetB = Vector.sub(contactVertex, bodyB.position), + velocityPointA = Vector.add(bodyA.velocity, Vector.mult(Vector.perp(offsetA), bodyA.angularVelocity)), + velocityPointB = Vector.add(bodyB.velocity, Vector.mult(Vector.perp(offsetB), bodyB.angularVelocity)), + relativeVelocity = Vector.sub(velocityPointA, velocityPointB), + normalVelocity = Vector.dot(normal, relativeVelocity); + + var tangentVelocity = Vector.dot(tangent, relativeVelocity), + tangentSpeed = Math.abs(tangentVelocity), + tangentVelocityDirection = Common.sign(tangentVelocity); + + // raw impulses + var normalImpulse = (1 + pair.restitution) * normalVelocity, + normalForce = Common.clamp(pair.separation + normalVelocity, 0, 1); + + // coulomb friction + var tangentImpulse = tangentVelocity; + if (tangentSpeed > normalForce * pair.friction) + tangentImpulse = normalForce * pair.friction * tangentVelocityDirection; + + // modify impulses accounting for mass, inertia and offset + var oAcN = Vector.cross(offsetA, normal), + oBcN = Vector.cross(offsetB, normal), + share = contactShare / (pair.inverseMass + bodyA.inverseInertia * oAcN * oAcN + bodyB.inverseInertia * oBcN * oBcN); + normalImpulse *= share; + tangentImpulse *= share; + + // handle high velocity and resting collisions separately + if (normalVelocity < 0 && normalVelocity * normalVelocity > _restingThresh) { + // high velocity so clear cached contact impulse + contact.normalImpulse = 0; + contact.tangentImpulse = 0; + } else { + // solve resting collision constraints using Erin Catto's method (GDC08) + + // impulse constraint, tends to 0 + var contactNormalImpulse = contact.normalImpulse; + contact.normalImpulse = Math.min(contact.normalImpulse + normalImpulse, 0); + normalImpulse = contact.normalImpulse - contactNormalImpulse; + + // tangent impulse, tends to -maxFriction or maxFriction + var contactTangentImpulse = contact.tangentImpulse; + contact.tangentImpulse = Common.clamp(contact.tangentImpulse + tangentImpulse, -tangentSpeed, tangentSpeed); + tangentImpulse = contact.tangentImpulse - contactTangentImpulse; + } + + // total impulse from contact + impulse.x = (normal.x * normalImpulse) + (tangent.x * tangentImpulse); + impulse.y = (normal.y * normalImpulse) + (tangent.y * tangentImpulse); + + // apply impulse from contact + if (!(bodyA.isStatic || bodyA.isSleeping)) { + bodyA.positionPrev.x += impulse.x * bodyA.inverseMass; + bodyA.positionPrev.y += impulse.y * bodyA.inverseMass; + bodyA.anglePrev += Vector.cross(offsetA, impulse) * bodyA.inverseInertia; + } + + if (!(bodyB.isStatic || bodyB.isSleeping)) { + bodyB.positionPrev.x -= impulse.x * bodyB.inverseMass; + bodyB.positionPrev.y -= impulse.y * bodyB.inverseMass; + bodyB.anglePrev -= Vector.cross(offsetB, impulse) * bodyB.inverseInertia; + } + } + } + }; + +})(); + +; // End src/collision/Resolver.js + + +// Begin src/collision/SAT.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class SAT +*/ + + +// TODO: true circles and curves +// TODO: cache the previously found axis and body, and start there first for faster early out + +var SAT = {}; + +(function() { + + /** + * Description + * @method collides + * @param {body} bodyA + * @param {body} bodyB + * @return {collision} collision + */ + SAT.collides = function(bodyA, bodyB) { + var overlapAB, + overlapBA, + minOverlap, + collision = { collided: false, bodyA: bodyA, bodyB: bodyB}; + + overlapAB = _overlapAxes(bodyA.vertices, bodyB.vertices, bodyA.axes); + + if (overlapAB.overlap === 0) + return collision; + + overlapBA = _overlapAxes(bodyB.vertices, bodyA.vertices, bodyB.axes); + + if (overlapBA.overlap === 0) + return collision; + + if (overlapAB.overlap < overlapBA.overlap) { + minOverlap = overlapAB; + } else { + minOverlap = overlapBA; + collision.bodyA = bodyB; + collision.bodyB = bodyA; + } + + collision.collided = true; + collision.normal = minOverlap.axis; + collision.depth = minOverlap.overlap; + + bodyA = collision.bodyA; + bodyB = collision.bodyB; + + // ensure normal is facing away from bodyA + if (Vector.dot(collision.normal, Vector.sub(bodyB.position, bodyA.position)) > 0) + collision.normal = Vector.neg(collision.normal); + + collision.tangent = Vector.perp(collision.normal); + + collision.penetration = { + x: collision.normal.x * collision.depth, + y: collision.normal.y * collision.depth + }; + + // find support points, there is always either exactly one or two + var verticesB = _findSupports(bodyA, bodyB, collision.normal), + supports = [verticesB[0]]; + + if (Vertices.contains(bodyA.vertices, verticesB[1])) { + supports.push(verticesB[1]); + } else { + var verticesA = _findSupports(bodyB, bodyA, Vector.neg(collision.normal)); + + if (Vertices.contains(bodyB.vertices, verticesA[0])) { + supports.push(verticesA[0]); + } + + if (supports.length < 2 && Vertices.contains(bodyB.vertices, verticesA[1])) { + supports.push(verticesA[1]); + } + } + + collision.supports = supports; + collision.supportCorrected = Vector.sub(verticesB[0], collision.penetration); + + return collision; + }; + + /** + * Description + * @method _overlapAxes + * @private + * @param {} verticesA + * @param {} verticesB + * @param {} axes + * @return result + */ + var _overlapAxes = function(verticesA, verticesB, axes) { + var projectionA = {}, + projectionB = {}, + result = { overlap: Number.MAX_VALUE }, + overlap, + axis; + + for (var i = 0; i < axes.length; i++) { + axis = axes[i]; + + _projectToAxis(projectionA, verticesA, axis); + _projectToAxis(projectionB, verticesB, axis); + + overlap = projectionA.min < projectionB.min + ? projectionA.max - projectionB.min + : projectionB.max - projectionA.min; + + if (overlap <= 0) + return { overlap: 0 }; + + if (overlap < result.overlap) { + result.overlap = overlap; + result.axis = axis; + } + } + + return result; + }; + + /** + * Description + * @method _projectToAxis + * @private + * @param {} projection + * @param {} vertices + * @param {} axis + */ + var _projectToAxis = function(projection, vertices, axis) { + var min = Vector.dot(vertices[0], axis), + max = min; + + for (var i = 1; i < vertices.length; i += 1) { + var dot = Vector.dot(vertices[i], axis); + + if (dot > max) { + max = dot; + } else if (dot < min) { + min = dot; + } + } + + projection.min = min; + projection.max = max; + }; + + /** + * Description + * @method _findSupports + * @private + * @param {} bodyA + * @param {} bodyB + * @param {} normal + * @return ArrayExpression + */ + var _findSupports = function(bodyA, bodyB, normal) { + var nearestDistance = Number.MAX_VALUE, + vertexToBody = { x: 0, y: 0 }, + vertices = bodyB.vertices, + bodyAPosition = bodyA.position, + distance, + vertex, + vertexA = vertices[0], + vertexB = vertices[1]; + + // find closest vertex on bodyB + for (var i = 0; i < vertices.length; i++) { + vertex = vertices[i]; + vertexToBody.x = vertex.x - bodyAPosition.x; + vertexToBody.y = vertex.y - bodyAPosition.y; + distance = -Vector.dot(normal, vertexToBody); + + if (distance < nearestDistance) { + nearestDistance = distance; + vertexA = vertex; + } + } + + // find next closest vertex using the two connected to it + var prevIndex = vertexA.index - 1 >= 0 ? vertexA.index - 1 : vertices.length - 1; + vertex = vertices[prevIndex]; + vertexToBody.x = vertex.x - bodyAPosition.x; + vertexToBody.y = vertex.y - bodyAPosition.y; + nearestDistance = -Vector.dot(normal, vertexToBody); + vertexB = vertex; + + var nextIndex = (vertexA.index + 1) % vertices.length; + vertex = vertices[nextIndex]; + vertexToBody.x = vertex.x - bodyAPosition.x; + vertexToBody.y = vertex.y - bodyAPosition.y; + distance = -Vector.dot(normal, vertexToBody); + if (distance < nearestDistance) { + nearestDistance = distance; + vertexB = vertex; + } + + return [vertexA, vertexB]; + }; + +})(); + +; // End src/collision/SAT.js + + +// Begin src/constraint/Constraint.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Constraint +*/ + +// TODO: fix instabillity issues with torque +// TODO: linked constraints +// TODO: breakable constraints +// TODO: collidable constraints +// TODO: allow constrained bodies to sleep +// TODO: handle 0 length constraints properly +// TODO: impulse caching and warming + +var Constraint = {}; + +(function() { + + var _minLength = 0.000001; + + /** + * Description + * @method create + * @param {} options + * @return {constraint} constraint + */ + Constraint.create = function(options) { + var constraint = options; + + // if bodies defined but no points, use body centre + if (constraint.bodyA && !constraint.pointA) + constraint.pointA = { x: 0, y: 0 }; + if (constraint.bodyB && !constraint.pointB) + constraint.pointB = { x: 0, y: 0 }; + + // calculate static length using initial world space points + var initialPointA = constraint.bodyA ? Vector.add(constraint.bodyA.position, constraint.pointA) : constraint.pointA, + initialPointB = constraint.bodyB ? Vector.add(constraint.bodyB.position, constraint.pointB) : constraint.pointB, + length = Vector.magnitude(Vector.sub(initialPointA, initialPointB)); + + constraint.length = constraint.length || length || _minLength; + + // option defaults + constraint.lineWidth = constraint.lineWidth || 2; + constraint.strokeStyle = constraint.strokeStyle || '#666'; + constraint.stiffness = constraint.stiffness || 1; + constraint.angularStiffness = constraint.angularStiffness || 0; + constraint.angleA = constraint.bodyA ? constraint.bodyA.angle : constraint.angleA; + constraint.angleB = constraint.bodyB ? constraint.bodyB.angle : constraint.angleB; + + return constraint; + }; + + /** + * Description + * @method updateAll + * @param {constraint[]} constraints + */ + Constraint.updateAll = function(constraints) { + for (var i = 0; i < constraints.length; i++) { + Constraint.update(constraints[i]); + } + }; + + /** + * Description + * @method update + * @param {constraint} constraint + */ + Constraint.update = function(constraint) { + var bodyA = constraint.bodyA, + bodyB = constraint.bodyB, + pointA = constraint.pointA, + pointB = constraint.pointB; + + // update reference angle + if (bodyA && !bodyA.isStatic) { + constraint.pointA = Vector.rotate(pointA, bodyA.angle - constraint.angleA); + constraint.angleA = bodyA.angle; + } + + // update reference angle + if (bodyB && !bodyB.isStatic) { + constraint.pointB = Vector.rotate(pointB, bodyB.angle - constraint.angleB); + constraint.angleB = bodyB.angle; + } + + var pointAWorld = pointA, + pointBWorld = pointB; + + if (bodyA) pointAWorld = Vector.add(bodyA.position, pointA); + if (bodyB) pointBWorld = Vector.add(bodyB.position, pointB); + + if (!pointAWorld || !pointBWorld) + return; + + var delta = Vector.sub(pointAWorld, pointBWorld), + currentLength = Vector.magnitude(delta); + + // prevent singularity + if (currentLength === 0) + currentLength = _minLength; + + // solve distance constraint with Gauss-Siedel method + var difference = (currentLength - constraint.length) / currentLength, + normal = Vector.div(delta, currentLength), + force = Vector.mult(delta, difference * 0.5 * constraint.stiffness); + + var velocityPointA, + velocityPointB, + offsetA, + offsetB, + oAn, + oBn, + bodyADenom, + bodyBDenom; + + if (bodyA && !bodyA.isStatic) { + // point body offset + offsetA = { + x: pointAWorld.x - bodyA.position.x + force.x, + y: pointAWorld.y - bodyA.position.y + force.y + }; + + // update velocity + bodyA.velocity.x = bodyA.position.x - bodyA.positionPrev.x; + bodyA.velocity.y = bodyA.position.y - bodyA.positionPrev.y; + bodyA.angularVelocity = bodyA.angle - bodyA.anglePrev; + + // find point velocity and body mass + velocityPointA = Vector.add(bodyA.velocity, Vector.mult(Vector.perp(offsetA), bodyA.angularVelocity)); + oAn = Vector.dot(offsetA, normal); + bodyADenom = bodyA.inverseMass + bodyA.inverseInertia * oAn * oAn; + } else { + velocityPointA = { x: 0, y: 0 }; + bodyADenom = bodyA ? bodyA.inverseMass : 0; + } + + if (bodyB && !bodyB.isStatic) { + // point body offset + offsetB = { + x: pointBWorld.x - bodyB.position.x - force.x, + y: pointBWorld.y - bodyB.position.y - force.y + }; + + // update velocity + bodyB.velocity.x = bodyB.position.x - bodyB.positionPrev.x; + bodyB.velocity.y = bodyB.position.y - bodyB.positionPrev.y; + bodyB.angularVelocity = bodyB.angle - bodyB.anglePrev; + + // find point velocity and body mass + velocityPointB = Vector.add(bodyB.velocity, Vector.mult(Vector.perp(offsetB), bodyB.angularVelocity)); + oBn = Vector.dot(offsetB, normal); + bodyBDenom = bodyB.inverseMass + bodyB.inverseInertia * oBn * oBn; + } else { + velocityPointB = { x: 0, y: 0 }; + bodyBDenom = bodyB ? bodyB.inverseMass : 0; + } + + var relativeVelocity = Vector.sub(velocityPointB, velocityPointA), + normalImpulse = Vector.dot(normal, relativeVelocity) / (bodyADenom + bodyBDenom); + + if (normalImpulse > 0) normalImpulse = 0; + + var normalVelocity = { + x: normal.x * normalImpulse, + y: normal.y * normalImpulse + }; + + var torque; + + if (bodyA && !bodyA.isStatic) { + torque = Vector.cross(offsetA, normalVelocity) * bodyA.inverseInertia * (1 - constraint.angularStiffness); + + Sleeping.set(bodyA, false); + + // clamp to prevent instabillity + // TODO: solve this properlly + torque = Common.clamp(torque, -0.01, 0.01); + + // apply forces + bodyA.position.x -= force.x; + bodyA.position.y -= force.y; + bodyA.angle += torque; + + // update geometry + Vertices.translate(bodyA.vertices, force, -1); + Vertices.rotate(bodyA.vertices, torque, bodyA.position); + Axes.rotate(bodyA.axes, torque); + Bounds.update(bodyA.bounds, bodyA.vertices, bodyA.velocity); + } + + if (bodyB && !bodyB.isStatic) { + torque = Vector.cross(offsetB, normalVelocity) * bodyB.inverseInertia * (1 - constraint.angularStiffness); + + Sleeping.set(bodyB, false); + + // clamp to prevent instabillity + // TODO: solve this properlly + torque = Common.clamp(torque, -0.01, 0.01); + + // apply forces + bodyB.position.x += force.x; + bodyB.position.y += force.y; + bodyB.angle -= torque; + + // update geometry + Vertices.translate(bodyB.vertices, force); + Vertices.rotate(bodyB.vertices, -torque, bodyB.position); + Axes.rotate(bodyB.axes, -torque); + Bounds.update(bodyB.bounds, bodyB.vertices, bodyB.velocity); + } + + }; + +})(); + +; // End src/constraint/Constraint.js + + +// Begin src/constraint/MouseConstraint.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class MouseConstraint +*/ + +var MouseConstraint = {}; + +(function() { + + /** + * Description + * @method create + * @param {mouse} mouse + * @return {MouseConstraint} A new MouseConstraint + */ + MouseConstraint.create = function(mouse) { + var constraint = Constraint.create({ + pointA: mouse.position, + pointB: { x: 0, y: 0 }, + length: 0.01, + stiffness: 0.1, + angularStiffness: 1, + strokeStyle: 'lightgreen', + lineWidth: 3 + }); + + return { + mouse: mouse, + dragBody: null, + dragPoint: null, + constraints: [constraint] + }; + }; + + /** + * Description + * @method update + * @param {MouseConstraint} mouseConstraint + * @param {body[]} bodies + */ + MouseConstraint.update = function(mouseConstraint, bodies) { + var mouse = mouseConstraint.mouse, + constraint = mouseConstraint.constraints[0]; + + if (mouse.button === 0 || mouse.button === 2) { + if (!constraint.bodyB) { + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i]; + if (Bounds.contains(body.bounds, mouse.position) + && Vertices.contains(body.vertices, mouse.position)) { + constraint.pointA = mouse.position; + constraint.bodyB = body; + constraint.pointB = { x: mouse.position.x - body.position.x, y: mouse.position.y - body.position.y }; + constraint.angleB = body.angle; + Sleeping.set(body, false); + } + } + } + } else { + constraint.bodyB = null; + constraint.pointB = null; + } + + if (constraint.bodyB) { + Sleeping.set(constraint.bodyB, false); + constraint.pointA = mouse.position; + } + }; + +})(); + +; // End src/constraint/MouseConstraint.js + + +// Begin src/core/Common.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Common +*/ + +var Common = {}; + +(function() { + + /** + * Description + * @method extend + * @param {} obj, obj, obj... + * @return {} obj extended + */ + Common.extend = function(obj) { + var args = Array.prototype.slice.call(arguments, 1); + + for (var i = 0; i < args.length; i++) { + var source = args[i]; + + if (source) { + for (var prop in source) { + if (source[prop].constructor === Object) { + if (!obj[prop] || obj[prop].constructor === Object) { + obj[prop] = obj[prop] || {}; + Common.extend(obj[prop], source[prop]); + } else { + obj[prop] = source[prop]; + } + } else { + obj[prop] = source[prop]; + } + } + } + } + + return obj; + }; + + /** + * Description + * @method keys + * @param {} obj + * @return {string[]} keys + */ + Common.keys = function(obj) { + if (Object.keys) + return Object.keys(obj); + + // avoid hasOwnProperty for performance + var keys = []; + for (var key in obj) + keys.push(key); + return keys; + }; + + /** + * Description + * @method values + * @param {} obj + * @return {array} Array of the objects property values + */ + Common.values = function(obj) { + var values = []; + + if (Object.keys) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + values.push(obj[keys[i]]); + } + return values; + } + + // avoid hasOwnProperty for performance + for (var key in obj) + values.push(obj[key]); + return values; + }; + + /** + * Description + * @method shadeColor + * @param {string} color + * @param {number} percent + * @return {string} A hex colour string made by lightening or darkening color by percent + */ + Common.shadeColor = function(color, percent) { + // http://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color + var colorInteger = parseInt(color.slice(1),16), + amount = Math.round(2.55 * percent), + R = (colorInteger >> 16) + amount, + B = (colorInteger >> 8 & 0x00FF) + amount, + G = (colorInteger & 0x0000FF) + amount; + return "#" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R :255) * 0x10000 + + (B < 255 ? B < 1 ? 0 : B : 255) * 0x100 + + (G < 255 ? G < 1 ? 0 : G : 255)).toString(16).slice(1); + }; + + /** + * Description + * @method shuffle + * @param {array} array + * @return {array} array shuffled randomly + */ + Common.shuffle = function(array) { + for (var i = array.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + return array; + }; + + /** + * Description + * @method choose + * @param {array} choices + * @return {object} A random choice object from the array + */ + Common.choose = function(choices) { + return choices[Math.floor(Math.random() * choices.length)]; + }; + + /** + * Description + * @method isElement + * @param {object} obj + * @return {boolean} True if the object is a HTMLElement, otherwise false + */ + Common.isElement = function(obj) { + // http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object + try { + return obj instanceof HTMLElement; + } + catch(e){ + return (typeof obj==="object") && + (obj.nodeType===1) && (typeof obj.style === "object") && + (typeof obj.ownerDocument ==="object"); + } + }; + + /** + * Description + * @method clamp + * @param {number} value + * @param {number} min + * @param {number} max + * @return {number} The value clamped between min and max inclusive + */ + Common.clamp = function(value, min, max) { + if (value < min) + return min; + if (value > max) + return max; + return value; + }; + + /** + * Description + * @method sign + * @param {number} value + * @return {number} -1 if negative, +1 if 0 or positive + */ + Common.sign = function(value) { + return value < 0 ? -1 : 1; + }; + + /** + * Description + * @method now + * @return {Date} the current DateTime + */ + Common.now = Date.now || function() { + // http://stackoverflow.com/questions/221294/how-do-you-get-a-timestamp-in-javascript + return +(new Date()); + }; + + /** + * Description + * @method random + * @param {number} min + * @param {number} max + * @return {number} A random number between min and max inclusive + */ + Common.random = function(min, max) { + return min + Math.random() * (max - min); + }; + +})(); + +; // End src/core/Common.js + + +// Begin src/core/Engine.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Engine +*/ + +// TODO: multiple event handlers, before & after handlers +// TODO: viewports +// TODO: frameskipping + +var Engine = {}; + +(function() { + + var _fps = 60, + _deltaSampleSize = 8, + _delta = 1000 / _fps; + + var _requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame + || window.mozRequestAnimationFrame || window.msRequestAnimationFrame + || function(callback){ window.setTimeout(function() { callback(Common.now()); }, _delta); }; + + /** + * Description + * @method create + * @param {HTMLElement} element + * @param {object} options + * @return {engine} engine + */ + Engine.create = function(element, options) { + var defaults = { + enabled: true, + positionIterations: 6, + velocityIterations: 4, + constraintIterations: 1, + pairs: {}, + pairsList: [], + enableSleeping: false, + timeScale: 1, + input: {}, + timing: { + fps: _fps, + timestamp: 0, + delta: _delta, + correction: 1, + deltaMin: 1000 / _fps, + deltaMax: 1000 / (_fps * 0.5) + } + }; + + var engine = Common.extend(defaults, options); + engine = Common.isElement(element) ? (engine || {}) : element; + + // create default renderer only if no custom renderer set + // but still allow engine.render.engine to pass through if set + if (!engine.render || (engine.render && !engine.render.controller)) { + engine.render = Render.create(engine.render); + if (Common.isElement(element)) + element.appendChild(engine.render.canvas); + } + + engine.world = World.create(engine.world); + engine.metrics = engine.metrics || Metrics.create(); + engine.input.mouse = engine.input.mouse || Mouse.create(engine.render.canvas); + engine.mouseConstraint = engine.mouseConstraint || MouseConstraint.create(engine.input.mouse); + World.addComposite(engine.world, engine.mouseConstraint); + + engine.broadphase = engine.broadphase || { + current: 'grid', + grid: { + controller: Grid, + instance: Grid.create(), + detector: Detector.collisions + }, + bruteForce: { + detector: Detector.bruteForce + } + }; + + engine.events = { + tick: function(engine) { + Engine.update(engine, engine.timing.delta, engine.timing.correction); + }, + render: function(engine) { + if (engine.render.options.enabled) + engine.render.controller.world(engine); + } + }; + + return engine; + }; + + /** + * Description + * @method run + * @param {engine} engine + */ + Engine.run = function(engine) { + var timing = engine.timing, + delta, + counterTimestamp = 0, + frameCounter = 0, + deltaHistory = []; + + (function render(timestamp){ + _requestAnimationFrame(render); + + if (!engine.enabled) + return; + + delta = (timestamp - timing.timestamp) || _delta; + + // optimistically filter delta over a few frames, to improve stability + deltaHistory.push(delta); + deltaHistory = deltaHistory.slice(-_deltaSampleSize); + delta = Math.min.apply(null, deltaHistory); + + // limit delta + delta = delta < engine.timing.deltaMin ? engine.timing.deltaMin : delta; + delta = delta > engine.timing.deltaMax ? engine.timing.deltaMax : delta; + + timing.timestamp = timestamp; + timing.correction = delta / timing.delta; + timing.delta = delta; + + frameCounter += 1; + + if (timestamp - counterTimestamp >= 1000) { + timing.fps = frameCounter * ((timestamp - counterTimestamp) / 1000); + counterTimestamp = timestamp; + frameCounter = 0; + } + + engine.events.tick(engine); + engine.events.render(engine); + })(); + }; + + /** + * Description + * @method update + * @param {engine} engine + * @param {number} delta + * @param {number} correction + * @return engine + */ + Engine.update = function(engine, delta, correction) { + var world = engine.world, + broadphase = engine.broadphase[engine.broadphase.current], + broadphasePairs = [], + i; + + Body.resetForcesAll(world.bodies, world.gravity); + Metrics.reset(engine.metrics); + + MouseConstraint.update(engine.mouseConstraint, world.bodies, engine.input); + Body.updateAll(world.bodies, delta * engine.timeScale, correction, world.bounds); + + for (i = 0; i < engine.constraintIterations; i++) { + Constraint.updateAll(world.constraints); + } + + if (broadphase.controller) { + broadphase.controller.update(broadphase.instance, world.bodies, engine); + broadphasePairs = broadphase.instance.pairsList; + } else { + broadphasePairs = world.bodies; + } + + var pairsUpdated = Manager.updatePairs(engine.pairs, engine.pairsList, broadphasePairs, engine.metrics, broadphase.detector), + pairsRemoved = Manager.removeOldPairs(engine.pairs, engine.pairsList); + + if (pairsUpdated || pairsRemoved) + engine.pairsList = Common.values(engine.pairs); + + // wake up bodies involved in collisions + if (engine.enableSleeping) + Sleeping.afterCollisions(engine.pairsList); + + // iteratively resolve velocity between collisions + Resolver.preSolveVelocity(engine.pairsList); + for (i = 0; i < engine.velocityIterations; i++) { + Resolver.solveVelocity(engine.pairsList); + } + + // iteratively resolve position between collisions + for (i = 0; i < engine.positionIterations; i++) { + Resolver.solvePosition(engine.pairsList); + } + Resolver.postSolvePosition(world.bodies); + + if (engine.enableSleeping) + Sleeping.update(world.bodies); + + Metrics.update(engine.metrics, engine); + + return engine; + }; + + /** + * Description + * @method merge + * @param {engine} engineA + * @param {engine} engineB + */ + Engine.merge = function(engineA, engineB) { + Common.extend(engineA, engineB); + + if (engineB.world) { + engineA.world = engineB.world; + + Engine.clear(engineA); + + var bodies = engineA.world.bodies; + + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i]; + Sleeping.set(body, false); + body.id = Body.nextId(); + } + + var broadphase = engineA.broadphase[engineA.broadphase.current]; + + if (broadphase.controller === Grid) { + var grid = broadphase.instance; + Grid.clear(grid); + Grid.update(grid, engineA.world.bodies, engineA, true); + } + } + }; + + /** + * Description + * @method clear + * @param {engine} engine + */ + Engine.clear = function(engine) { + var world = engine.world; + + engine.pairs = {}; + engine.pairsList = []; + + World.addComposite(engine.world, engine.mouseConstraint); + + var broadphase = engine.broadphase[engine.broadphase.current]; + + if (broadphase.controller === Grid) + Grid.clear(broadphase.instance); + + if (broadphase.controller) { + broadphase.controller.update(broadphase.instance, world.bodies, engine, true); + } + }; + +})(); + +; // End src/core/Engine.js + + +// Begin src/core/Metrics.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Metrics +*/ + +var Metrics = {}; + +(function() { + + /** + * Description + * @method create + * @return {metrics} A new metrics + */ + Metrics.create = function() { + return { + narrowDetections: 0, + narrowphaseTests: 0, + midphaseTests: 0, + broadphaseTests: 0, + narrowEff: 0.0001, + midEff: 0.0001, + broadEff: 0.0001, + collisions: 0, + buckets: 0, + bodies: 0, + pairs: 0 + }; + }; + + /** + * Description + * @method reset + * @param {metrics} metrics + */ + Metrics.reset = function(metrics) { + metrics.narrowDetections = 0; + metrics.narrowphaseTests = 0; + metrics.midphaseTests = 0; + metrics.broadphaseTests = 0; + metrics.narrowEff = 0; + metrics.midEff = 0; + metrics.broadEff = 0; + metrics.collisions = 0; + metrics.buckets = 0; + metrics.pairs = 0; + metrics.bodies = 0; + }; + + /** + * Description + * @method update + * @param {metrics} metrics + * @param {engine} engine + */ + Metrics.update = function(metrics, engine) { + var world = engine.world, + broadphase = engine.broadphase[engine.broadphase.current]; + + metrics.collisions = metrics.narrowDetections; + metrics.pairs = engine.pairsList.length; + metrics.bodies = world.bodies.length; + metrics.midEff = (metrics.narrowDetections / (metrics.midphaseTests || 1)).toFixed(2); + metrics.narrowEff = (metrics.narrowDetections / (metrics.narrowphaseTests || 1)).toFixed(2); + metrics.broadEff = (1 - (metrics.broadphaseTests / (world.bodies.length || 1))).toFixed(2); + if (broadphase.instance) + metrics.buckets = Common.keys(broadphase.instance.buckets).length; + }; + +})(); + +; // End src/core/Metrics.js + + +// Begin src/core/Mouse.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Mouse +*/ + +var Mouse; + +(function() { + + /** + * Description + * @param {HTMLElement} element + */ + Mouse = function(element) { + var mouse = this; + + element = element || document.body; + + this.position = { x: 0, y: 0 }; + this.mousedownPosition = { x: 0, y: 0 }; + this.mouseupPosition = { x: 0, y: 0 }; + this.button = -1; + + var mousemove = function(event) { + var position = _getRelativeMousePosition(event, element), + touches = event.changedTouches; + + if (touches) { + mouse.button = 0; + event.preventDefault(); + } + + mouse.position = position; + }; + + var mousedown = function(event) { + var position = _getRelativeMousePosition(event, element), + touches = event.changedTouches; + + if (touches) { + mouse.button = 0; + event.preventDefault(); + } else { + mouse.button = event.button; + } + + mouse.position = mouse.mousedownPosition = position; + }; + + var mouseup = function(event) { + var position = _getRelativeMousePosition(event, element), + touches = event.changedTouches; + + if (touches) { + event.preventDefault(); + } + + mouse.button = -1; + + mouse.position = mouse.mouseupPosition = position; + }; + + element.addEventListener('mousemove', mousemove); + element.addEventListener('mousedown', mousedown); + element.addEventListener('mouseup', mouseup); + + element.addEventListener('touchmove', mousemove); + element.addEventListener('touchstart', mousedown); + element.addEventListener('touchend', mouseup); + }; + + /** + * Description + * @method create + * @param {HTMLElement} element + * @return {mouse} A new mouse + */ + Mouse.create = function(element) { + return new Mouse(element); + }; + + /** + * Description + * @method _getRelativeMousePosition + * @private + * @param {} event + * @param {} element + * @return ObjectExpression + */ + var _getRelativeMousePosition = function(event, element) { + var elementBounds = element.getBoundingClientRect(), + scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft, + scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop, + touches = event.changedTouches, + x, y; + + if (touches) { + x = touches[0].pageX - elementBounds.left - scrollX; + y = touches[0].pageY - elementBounds.top - scrollY; + } else { + x = event.pageX - elementBounds.left - scrollX; + y = event.pageY - elementBounds.top - scrollY; + } + + return { + x: x / (element.clientWidth / element.width), + y: y / (element.clientHeight / element.height) + }; + }; + +})(); + +; // End src/core/Mouse.js + + +// Begin src/core/Sleeping.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Sleeping +*/ + +var Sleeping = {}; + +(function() { + + var _motionWakeThreshold = 0.18, + _motionSleepThreshold = 0.08, + _minBias = 0.9; + + /** + * Description + * @method update + * @param {body[]} bodies + */ + Sleeping.update = function(bodies) { + // update bodies sleeping status + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i], + motion = body.speed * body.speed + body.angularSpeed * body.angularSpeed; + + var minMotion = Math.min(body.motion, motion), + maxMotion = Math.max(body.motion, motion); + + // biased average motion estimation between frames + body.motion = _minBias * minMotion + (1 - _minBias) * maxMotion; + + if (body.sleepThreshold > 0 && body.motion < _motionSleepThreshold) { + body.sleepCounter += 1; + + if (body.sleepCounter >= body.sleepThreshold) + Sleeping.set(body, true); + } else if (body.sleepCounter > 0) { + body.sleepCounter -= 1; + } + } + }; + + /** + * Description + * @method afterCollisions + * @param {pair[]} pairs + */ + Sleeping.afterCollisions = function(pairs) { + // wake up bodies involved in collisions + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i], + collision = pair.collision, + bodyA = collision.bodyA, + bodyB = collision.bodyB; + + if ((bodyA.isSleeping && bodyB.isSleeping) || bodyA.isStatic || bodyB.isStatic) + continue; + + if (bodyA.isSleeping || bodyB.isSleeping) { + var sleepingBody = (bodyA.isSleeping && !bodyA.isStatic) ? bodyA : bodyB, + movingBody = sleepingBody === bodyA ? bodyB : bodyA; + + if (!sleepingBody.isStatic && movingBody.motion > _motionWakeThreshold) { + Sleeping.set(sleepingBody, false); + } + } + } + }; + + /** + * Description + * @method set + * @param {body} body + * @param {boolean} isSleeping + */ + Sleeping.set = function(body, isSleeping) { + if (isSleeping) { + body.isSleeping = true; + body.sleepCounter = body.sleepThreshold; + + body.positionImpulse.x = 0; + body.positionImpulse.y = 0; + + body.positionPrev.x = body.position.x; + body.positionPrev.y = body.position.y; + + body.anglePrev = body.angle; + body.speed = 0; + body.angularSpeed = 0; + body.motion = 0; + } else { + body.isSleeping = false; + body.sleepCounter = 0; + } + }; + +})(); + +; // End src/core/Sleeping.js + + +// Begin src/factory/Bodies.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Bodies +*/ + +// TODO: true circle bodies + +var Bodies = {}; + +(function() { + + /** + * Description + * @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 = { + position: { x: x, y: y }, + path: 'L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height + }; + + return Body.create(Common.extend({}, rectangle, options)); + }; + + /** + * Description + * @method trapezoid + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + * @param {number} slope + * @param {object} options + * @return {body} A new trapezoid body + */ + Bodies.trapezoid = function(x, y, width, height, slope, options) { + options = options || {}; + + slope *= 0.5; + var roof = (1 - (slope * 2)) * width; + + var x1 = width * slope, + x2 = x1 + roof, + x3 = x2 + x1; + + var trapezoid = { + position: { x: x, y: y }, + path: 'L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0' + }; + + return Body.create(Common.extend({}, trapezoid, options)); + }; + + /** + * Description + * @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 || {}; + + // 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; + + // flag for better rendering + options.circleRadius = radius; + + return Bodies.polygon(x, y, sides, radius, options); + }; + + /** + * Description + * @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 = { + position: { x: x, y: y }, + path: path + }; + + return Body.create(Common.extend({}, polygon, options)); + }; + +})(); + +; // End src/factory/Bodies.js + + +// Begin src/factory/Composites.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Composites +*/ + +var Composites = {}; + +(function() { + + /** + * Description + * @method stack + * @param {number} xx + * @param {number} yy + * @param {number} columns + * @param {number} rows + * @param {number} columnGap + * @param {number} rowGap + * @param {function} callback + * @return {composite} A new composite containing objects created in the callback + */ + Composites.stack = function(xx, yy, columns, rows, columnGap, rowGap, callback) { + var stack = Composite.create(), + x = xx, + y = yy, + lastBody, + i = 0; + + for (var row = 0; row < rows; row++) { + var maxHeight = 0; + + for (var column = 0; column < columns; column++) { + var body = callback(x, y, column, row, lastBody, i); + + if (body) { + var bodyHeight = body.bounds.max.y - body.bounds.min.y, + bodyWidth = body.bounds.max.x - body.bounds.min.x; + + if (bodyHeight > maxHeight) + maxHeight = bodyHeight; + + Body.translate(body, { x: bodyWidth * 0.5, y: bodyHeight * 0.5 }); + + x = body.bounds.max.x + columnGap; + + Composite.addBody(stack, body); + + lastBody = body; + i += 1; + } + } + + y += maxHeight + rowGap; + x = xx; + } + + return stack; + }; + + /** + * Description + * @method chain + * @param {composite} composite + * @param {number} xOffsetA + * @param {number} yOffsetA + * @param {number} xOffsetB + * @param {number} yOffsetB + * @param {object} options + * @return {composite} A new composite containing objects chained together with constraints + */ + Composites.chain = function(composite, xOffsetA, yOffsetA, xOffsetB, yOffsetB, options) { + var bodies = composite.bodies; + + for (var i = 1; i < bodies.length; i++) { + var bodyA = bodies[i - 1], + bodyB = bodies[i], + bodyAHeight = bodyA.bounds.max.y - bodyA.bounds.min.y, + bodyAWidth = bodyA.bounds.max.x - bodyA.bounds.min.x, + bodyBHeight = bodyB.bounds.max.y - bodyB.bounds.min.y, + bodyBWidth = bodyB.bounds.max.x - bodyB.bounds.min.x; + + var defaults = { + bodyA: bodyA, + pointA: { x: bodyAWidth * xOffsetA, y: bodyAHeight * yOffsetA }, + bodyB: bodyB, + pointB: { x: bodyBWidth * xOffsetB, y: bodyBHeight * yOffsetB } + }; + + var constraint = Common.extend(defaults, options); + + Composite.addConstraint(composite, Constraint.create(constraint)); + } + + return composite; + }; + + /** + * Description + * @method pyramid + * @param {number} xx + * @param {number} yy + * @param {number} columns + * @param {number} rows + * @param {number} columnGap + * @param {number} rowGap + * @param {function} callback + * @return {composite} A new composite containing objects created in the callback + */ + Composites.pyramid = function(xx, yy, columns, rows, columnGap, rowGap, callback) { + return Composites.stack(xx, yy, columns, rows, columnGap, rowGap, function(x, y, column, row, lastBody, i) { + var actualRows = Math.min(rows, Math.ceil(columns / 2)), + lastBodyWidth = lastBody ? lastBody.bounds.max.x - lastBody.bounds.min.x : 0; + + if (row > actualRows) + return; + + // reverse row order + row = actualRows - row; + + var start = row, + end = columns - 1 - row; + + if (column < start || column > end) + return; + + // retroactively fix the first body's position, since width was unknown + if (i === 1) { + Body.translate(lastBody, { x: (column + (columns % 2 === 1 ? 1 : -1)) * lastBodyWidth, y: 0 }); + } + + var xOffset = lastBody ? column * lastBodyWidth : 0; + + return callback(xx + xOffset + column * columnGap, y, column, row, lastBody, i); + }); + }; + + /** + * Description + * @method newtonsCradle + * @param {number} xx + * @param {number} yy + * @param {number} number + * @param {number} size + * @param {number} length + * @return {composite} A new composite newtonsCradle body + */ + Composites.newtonsCradle = function(xx, yy, number, size, length) { + var newtonsCradle = Composite.create(); + + for (var i = 0; i < number; i++) { + var separation = 1.9, + circle = Bodies.circle(xx + i * (size * separation), yy + length, size, + { restitution: 1, friction: 0, frictionAir: 0.0001, slop: 0.01 }), + constraint = Constraint.create({ pointA: { x: xx + i * (size * separation), y: yy }, bodyB: circle }); + + Composite.addBody(newtonsCradle, circle); + Composite.addConstraint(newtonsCradle, constraint); + } + + return newtonsCradle; + }; + + /** + * Description + * @method car + * @param {number} xx + * @param {number} yy + * @param {number} width + * @param {number} height + * @param {number} wheelSize + * @return {composite} A new composite car body + */ + Composites.car = function(xx, yy, width, height, wheelSize) { + var groupId = Body.nextGroupId(), + wheelBase = -20, + wheelAOffset = -width * 0.5 + wheelBase, + wheelBOffset = width * 0.5 - wheelBase, + wheelYOffset = 0; + + var car = Composite.create(), + body = Bodies.trapezoid(xx, yy, width, height, 0.3, { groupId: groupId, friction: 0.01 }); + + var wheelA = Bodies.circle(xx + wheelAOffset, yy + wheelYOffset, wheelSize, { + groupId: groupId, + restitution: 0.5, + friction: 0.9, + density: 0.01 + }); + + var wheelB = Bodies.circle(xx + wheelBOffset, yy + wheelYOffset, wheelSize, { + groupId: groupId, + restitution: 0.5, + friction: 0.9, + density: 0.01 + }); + + var axelA = Constraint.create({ + bodyA: body, + pointA: { x: wheelAOffset, y: wheelYOffset }, + bodyB: wheelA, + stiffness: 0.5 + }); + + var axelB = Constraint.create({ + bodyA: body, + pointA: { x: wheelBOffset, y: wheelYOffset }, + bodyB: wheelB, + stiffness: 0.5 + }); + + Composite.addBody(car, body); + Composite.addBody(car, wheelA); + Composite.addBody(car, wheelB); + Composite.addConstraint(car, axelA); + Composite.addConstraint(car, axelB); + + return car; + }; + +})(); + +; // End src/factory/Composites.js + + +// Begin src/geometry/Axes.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Axes +*/ + +var Axes = {}; + +(function() { + + /** + * Description + * @method fromVertices + * @param {vertices} vertices + * @return {axes} A new axes from the given vertices + */ + Axes.fromVertices = function(vertices) { + var axes = {}; + + // find the unique axes, using edge normal gradients + for (var i = 0; i < vertices.length; i++) { + var j = (i + 1) % vertices.length, + normal = Vector.normalise({ + x: vertices[j].y - vertices[i].y, + y: vertices[i].x - vertices[j].x + }), + gradient = (normal.y === 0) ? Infinity : (normal.x / normal.y); + + // limit precision + gradient = gradient.toFixed(3).toString(); + + axes[gradient] = normal; + } + + return Common.values(axes); + }; + + /** + * Description + * @method rotate + * @param {axes} axes + * @param {number} angle + */ + Axes.rotate = function(axes, angle) { + if (angle === 0) + return; + + var cos = Math.cos(angle), + sin = Math.sin(angle); + + for (var i = 0; i < axes.length; i++) { + var axis = axes[i], + xx; + xx = axis.x * cos - axis.y * sin; + axis.y = axis.x * sin + axis.y * cos; + axis.x = xx; + } + }; + +})(); + +; // End src/geometry/Axes.js + + +// Begin src/geometry/Bounds.js + +/** +* _Internal Class_, not generally used outside of the engine's internals. +* +* @class Bounds +*/ + +var Bounds = {}; + +(function() { + + /** + * Description + * @method create + * @param {vertices} vertices + * @return {bounds} A new bounds object + */ + Bounds.create = function(vertices) { + var bounds = { + min: { x: 0, y: 0 }, + max: { x: 0, y: 0 } + }; + Bounds.update(bounds, vertices); + return bounds; + }; + + /** + * Description + * @method update + * @param {bounds} bounds + * @param {vertices} vertices + * @param {vector} velocity + */ + Bounds.update = function(bounds, vertices, velocity) { + bounds.min.x = Number.MAX_VALUE; + bounds.max.x = Number.MIN_VALUE; + bounds.min.y = Number.MAX_VALUE; + bounds.max.y = Number.MIN_VALUE; + + for (var i = 0; i < vertices.length; i++) { + var vertex = vertices[i]; + if (vertex.x > bounds.max.x) bounds.max.x = vertex.x; + if (vertex.x < bounds.min.x) bounds.min.x = vertex.x; + if (vertex.y > bounds.max.y) bounds.max.y = vertex.y; + if (vertex.y < bounds.min.y) bounds.min.y = vertex.y; + } + + if (velocity) { + if (velocity.x > 0) { + bounds.max.x += velocity.x; + } else { + bounds.min.x += velocity.x; + } + + if (velocity.y > 0) { + bounds.max.y += velocity.y; + } else { + bounds.min.y += velocity.y; + } + } + }; + + /** + * Description + * @method contains + * @param {bounds} bounds + * @param {vector} point + * @return {boolean} True if the bounds contain the point, otherwise false + */ + Bounds.contains = function(bounds, point) { + return point.x >= bounds.min.x && point.x <= bounds.max.x + && point.y >= bounds.min.y && point.y <= bounds.max.y; + }; + + /** + * Description + * @method overlaps + * @param {bounds} boundsA + * @param {bounds} boundsB + * @return {boolean} True if the bounds overlap, otherwise false + */ + Bounds.overlaps = function(boundsA, boundsB) { + return (boundsA.min.x <= boundsB.max.x && boundsA.max.x >= boundsB.min.x + && boundsA.max.y >= boundsB.min.y && boundsA.min.y <= boundsB.max.y); + }; + +})(); + +; // End src/geometry/Bounds.js + + +// Begin src/geometry/Vector.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Vector +*/ + +// TODO: consider params for reusing vector objects + +var Vector = {}; + +(function() { + + /** + * Description + * @method magnitude + * @param {vector} vector + * @return {number} The magnitude of the vector + */ + Vector.magnitude = function(vector) { + return Math.sqrt((vector.x * vector.x) + (vector.y * vector.y)); + }; + + /** + * Description + * @method magnitudeSquared + * @param {vector} vector + * @return {number} The squared magnitude of the vector + */ + Vector.magnitudeSquared = function(vector) { + return (vector.x * vector.x) + (vector.y * vector.y); + }; + + /** + * Description + * @method rotate + * @param {vector} vector + * @param {number} angle + * @return {vector} A new vector rotated + */ + Vector.rotate = function(vector, angle) { + var cos = Math.cos(angle), sin = Math.sin(angle); + return { + x: vector.x * cos - vector.y * sin, + y: vector.x * sin + vector.y * cos + }; + }; + + /** + * Description + * @method rotateAbout + * @param {vector} vector + * @param {number} angle + * @param {vector} point + * @return {vector} A new vector rotated about the point + */ + Vector.rotateAbout = function(vector, angle, point) { + var cos = Math.cos(angle), sin = Math.sin(angle); + return { + x: point.x + ((vector.x - point.x) * cos - (vector.y - point.y) * sin), + y: point.y + ((vector.x - point.x) * sin + (vector.y - point.y) * cos) + }; + }; + + /** + * Description + * @method normalise + * @param {vector} vector + * @return {vector} A new vector normalised + */ + Vector.normalise = function(vector) { + var magnitude = Vector.magnitude(vector); + if (magnitude === 0) + return { x: 0, y: 0 }; + return { x: vector.x / magnitude, y: vector.y / magnitude }; + }; + + /** + * Description + * @method dot + * @param {vector} vectorA + * @param {vector} vectorB + * @return {number} The dot product of the two vectors + */ + Vector.dot = function(vectorA, vectorB) { + return (vectorA.x * vectorB.x) + (vectorA.y * vectorB.y); + }; + + /** + * Description + * @method cross + * @param {vector} vectorA + * @param {vector} vectorB + * @return {number} The cross product of the two vectors + */ + Vector.cross = function(vectorA, vectorB) { + return (vectorA.x * vectorB.y) - (vectorA.y * vectorB.x); + }; + + /** + * Description + * @method add + * @param {vector} vectorA + * @param {vector} vectorB + * @return {vector} A new vector added + */ + Vector.add = function(vectorA, vectorB) { + return { x: vectorA.x + vectorB.x, y: vectorA.y + vectorB.y }; + }; + + /** + * Description + * @method sub + * @param {vector} vectorA + * @param {vector} vectorB + * @return {vector} A new vector subtracted + */ + Vector.sub = function(vectorA, vectorB) { + return { x: vectorA.x - vectorB.x, y: vectorA.y - vectorB.y }; + }; + + /** + * Description + * @method mult + * @param {vector} vector + * @param {number} scalar + * @return {vector} A new vector multiplied by scalar + */ + Vector.mult = function(vector, scalar) { + return { x: vector.x * scalar, y: vector.y * scalar }; + }; + + /** + * Description + * @method div + * @param {vector} vector + * @param {number} scalar + * @return {vector} A new vector divided by scalar + */ + Vector.div = function(vector, scalar) { + return { x: vector.x / scalar, y: vector.y / scalar }; + }; + + /** + * Description + * @method perp + * @param {vector} vector + * @param {bool} negate + * @return {vector} The perpendicular vector + */ + Vector.perp = function(vector, negate) { + negate = negate === true ? -1 : 1; + return { x: negate * -vector.y, y: negate * vector.x }; + }; + + /** + * Description + * @method neg + * @param {vector} vector + * @return {vector} The negated vector + */ + Vector.neg = function(vector) { + return { x: -vector.x, y: -vector.y }; + }; + +})(); + +; // End src/geometry/Vector.js + + +// Begin src/geometry/Vertices.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Vertices +*/ + +// TODO: convex decomposition - http://mnbayazit.com/406/bayazit + +var Vertices = {}; + +(function() { + + /** + * Description + * @method create + * @param {vertices} vertices + * @param {body} body + */ + Vertices.create = function(vertices, body) { + for (var i = 0; i < vertices.length; i++) { + vertices[i].index = i; + vertices[i].body = body; + } + }; + + /** + * Description + * @method fromPath + * @param {string} path + * @return {vertices} vertices + */ + Vertices.fromPath = function(path) { + var pathPattern = /L\s*([\-\d\.]*)\s*([\-\d\.]*)/ig, + vertices = []; + + path.replace(pathPattern, function(match, x, y) { + vertices.push({ x: parseFloat(x, 10), y: parseFloat(y, 10) }); + }); + + return vertices; + }; + + /** + * Description + * @method centre + * @param {vertices} vertices + * @return {vector} The centre point + */ + Vertices.centre = function(vertices) { + var cx = 0, cy = 0; + + for (var i = 0; i < vertices.length; i++) { + cx += vertices[i].x; + cy += vertices[i].y; + } + + return { x: cx / vertices.length, y: cy / vertices.length }; + }; + + /** + * Description + * @method area + * @param {vertices} vertices + * @return {number} The area + */ + Vertices.area = function(vertices) { + var area = 0, + j = vertices.length - 1; + + for (var i = 0; i < vertices.length; i++) { + area += (vertices[j].x - vertices[i].x) * (vertices[j].y + vertices[i].y); + j = i; + } + + return Math.abs(area) / 2; + }; + + /** + * Description + * @method inertia + * @param {vertices} vertices + * @param {number} mass + * @return {number} The polygon's moment of inertia, using second moment of area + */ + Vertices.inertia = function(vertices, mass) { + var numerator = 0, + denominator = 0, + v = vertices, + cross, + j; + + // find the polygon's moment of inertia, using second moment of area + // http://www.physicsforums.com/showthread.php?t=25293 + for (var n = 0; n < v.length; n++) { + j = (n + 1) % v.length; + cross = Math.abs(Vector.cross(v[j], v[n])); + numerator += cross * (Vector.dot(v[j], v[j]) + Vector.dot(v[j], v[n]) + Vector.dot(v[n], v[n])); + denominator += cross; + } + + return (mass / 6) * (numerator / denominator); + }; + + /** + * Description + * @method translate + * @param {vertices} vertices + * @param {vector} vector + * @param {number} scalar + */ + Vertices.translate = function(vertices, vector, scalar) { + var i; + if (scalar) { + for (i = 0; i < vertices.length; i++) { + vertices[i].x += vector.x * scalar; + vertices[i].y += vector.y * scalar; + } + } else { + for (i = 0; i < vertices.length; i++) { + vertices[i].x += vector.x; + vertices[i].y += vector.y; + } + } + }; + + /** + * Description + * @method rotate + * @param {vertices} vertices + * @param {number} angle + * @param {vector} point + */ + Vertices.rotate = function(vertices, angle, point) { + if (angle === 0) + return; + + var cos = Math.cos(angle), + sin = Math.sin(angle); + + for (var i = 0; i < vertices.length; i++) { + var vertice = vertices[i], + dx = vertice.x - point.x, + dy = vertice.y - point.y; + + vertice.x = point.x + (dx * cos - dy * sin); + vertice.y = point.y + (dx * sin + dy * cos); + } + }; + + /** + * Description + * @method contains + * @param {vertices} vertices + * @param {vector} point + * @return {boolean} True if the vertices contains point, otherwise false + */ + Vertices.contains = function(vertices, point) { + for (var i = 0; i < vertices.length; i++) { + var vertice = vertices[i], + nextVertice = vertices[(i + 1) % vertices.length]; + if ((point.x - vertice.x) * (nextVertice.y - vertice.y) + (point.y - vertice.y) * (vertice.x - nextVertice.x) > 0) { + return false; + } + } + + return true; + }; + +})(); + +; // End src/geometry/Vertices.js + + +// Begin src/render/Gui.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Gui +*/ + +var Gui = {}; + +(function() { + + /** + * Description + * @method create + * @param {engine} engine + * @param {object} options + * @return {gui} A container for a configured dat.gui + */ + Gui.create = function(engine, options) { + var _datGuiSupported = window.dat && window.localStorage, + _serializer; + + if (!_datGuiSupported) { + console.log("Could not create GUI. Check dat.gui library is loaded first."); + return; + } + + var datGui = new dat.GUI(options); + + if (Resurrect) { + _serializer = new Resurrect({ prefix: '$' }); + _serializer.parse = _serializer.resurrect; + } else { + _serializer = JSON; + } + + var gui = { + datGui: datGui, + amount: 1, + size: 40, + sides: 4, + density: 0.001, + restitution: 0, + friction: 0.1, + frictionAir: 0.01 + }; + + var funcs = { + + addBody: function() { + var options = { + density: gui.density, + friction: gui.friction, + frictionAir: gui.frictionAir, + restitution: gui.restitution + }; + + for (var i = 0; i < gui.amount; i++) { + World.addBody(engine.world, Bodies.polygon(120 + i * gui.size + i * 50, 200, gui.sides, gui.size, options)); + } + }, + + clear: function() { + World.clear(engine.world, true); + Engine.clear(engine); + }, + + save: function() { + if (localStorage && _serializer) { + localStorage.setItem('world', _serializer.stringify(engine.world)); + } + }, + + load: function() { + var loadedWorld; + + if (localStorage && _serializer) { + loadedWorld = _serializer.parse(localStorage.getItem('world')); + } + + if (loadedWorld) { + Engine.merge(engine, { world: loadedWorld }); + } + } + }; + + var metrics = datGui.addFolder('Metrics'); + metrics.add(engine.timing, 'fps').listen(); + metrics.add(engine.timing, 'delta').listen(); + metrics.add(engine.timing, 'correction').listen(); + metrics.add(engine.metrics, 'bodies').listen(); + metrics.add(engine.metrics, 'collisions').listen(); + metrics.add(engine.metrics, 'pairs').listen(); + metrics.add(engine.metrics, 'broadEff').listen(); + metrics.add(engine.metrics, 'midEff').listen(); + metrics.add(engine.metrics, 'narrowEff').listen(); + metrics.close(); + + var controls = datGui.addFolder('Add Body'); + controls.add(gui, 'amount', 1, 5).step(1); + controls.add(gui, 'size', 5, 150).step(1); + controls.add(gui, 'sides', 1, 8).step(1); + controls.add(gui, 'density', 0.0001, 0.01).step(0.001); + controls.add(gui, 'friction', 0, 1).step(0.05); + controls.add(gui, 'frictionAir', 0, gui.frictionAir * 10).step(gui.frictionAir / 10); + controls.add(gui, 'restitution', 0, 1).step(0.1); + controls.add(funcs, 'addBody'); + controls.open(); + + var worldGui = datGui.addFolder('World'); + worldGui.add(funcs, 'load'); + worldGui.add(funcs, 'save'); + worldGui.add(funcs, 'clear'); + worldGui.open(); + + var gravity = worldGui.addFolder('Gravity'); + gravity.add(engine.world.gravity, 'x', -1, 1).step(0.01); + gravity.add(engine.world.gravity, 'y', -1, 1).step(0.01); + gravity.open(); + + var physics = datGui.addFolder('Engine'); + physics.add(engine, 'enableSleeping'); + physics.add(engine.broadphase, 'current', ['grid', 'bruteForce']); + physics.add(engine, 'timeScale', 0.1, 2).step(0.1); + physics.add(engine, 'velocityIterations', 1, 10).step(1); + physics.add(engine, 'positionIterations', 1, 10).step(1); + physics.add(engine, 'enabled'); + physics.open(); + + var render = datGui.addFolder('Render'); + render.add(engine.render.options, 'wireframes'); + render.add(engine.render.options, 'showDebug'); + render.add(engine.render.options, 'showPositions'); + render.add(engine.render.options, 'showBroadphase'); + render.add(engine.render.options, 'showBounds'); + render.add(engine.render.options, 'showVelocity'); + render.add(engine.render.options, 'showCollisions'); + render.add(engine.render.options, 'showAxes'); + render.add(engine.render.options, 'showAngleIndicator'); + render.add(engine.render.options, 'showSleeping'); + render.add(engine.render.options, 'showIds'); + render.add(engine.render.options, 'showShadows'); + render.add(engine.render.options, 'enabled'); + render.open(); + + //datGui.remember(world) + + return gui; + }; + + /** + * Description + * @method update + * @param {gui} gui + * @param {datGui} datGui + */ + Gui.update = function(gui, datGui) { + var i; + datGui = datGui || gui.datGui; + + for (i in datGui.__folders) { + Gui.update(gui, datGui.__folders[i]); + } + + for (i in datGui.__controllers) { + var controller = datGui.__controllers[i]; + if (controller.updateDisplay) + controller.updateDisplay(); + } + }; + + /** + * Description + * @method closeAll + * @param {gui} gui + */ + Gui.closeAll = function(gui) { + var datGui = gui.datGui; + + for (var i in datGui.__folders) { + datGui.__folders[i].close(); + } + }; + +})(); + +; // End src/render/Gui.js + + +// Begin src/render/Render.js + +/** +* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) +* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. +* +* @class Render +*/ + +// TODO: viewports +// TODO: two.js, pixi.js + +var Render = {}; + +(function() { + + /** + * Description + * @method create + * @param {object} options + * @return {render} A new renderer + */ + Render.create = function(options) { + var defaults = { + controller: Render, + options: { + width: 800, + height: 600, + background: '#fafafa', + wireframeBackground: '#222', + enabled: true, + wireframes: true, + showSleeping: true, + showDebug: false, + showBroadphase: false, + showBounds: false, + showVelocity: false, + showCollisions: false, + showAxes: false, + showPositions: false, + showAngleIndicator: false, + showIds: false, + showShadows: false + } + }; + + var render = Common.extend(defaults, options); + + render.canvas = render.canvas || _createCanvas(render.options.width, render.options.height); + render.context = render.canvas.getContext('2d'); + + return render; + }; + + /** + * Description + * @method world + * @param {engine} engine + */ + Render.world = function(engine) { + var render = engine.render, + world = engine.world, + canvas = render.canvas, + context = render.context, + options = render.options, + i; + + if (options.wireframes) { + context.fillStyle = options.wireframeBackground; + } else { + context.fillStyle = options.background; + } + + context.fillRect(0, 0, canvas.width, canvas.height); + + if (options.showShadows && !options.wireframes) + for (i = 0; i < world.bodies.length; i++) + Render.bodyShadow(engine, world.bodies[i], context); + + for (i = 0; i < world.bodies.length; i++) + Render.body(engine, world.bodies[i], context); + + for (i = 0; i < world.constraints.length; i++) + Render.constraint(world.constraints[i], context); + + if (options.showCollisions) + for (i = 0; i < engine.pairsList.length; i++) + Render.collision(engine, engine.pairsList[i], context); + + if (options.showBroadphase && engine.broadphase.current === 'grid') + Render.grid(engine, engine.broadphase[engine.broadphase.current].instance, context); + + if (options.showDebug) + Render.debug(engine, context); + }; + + /** + * Description + * @method debug + * @param {engine} engine + * @param {RenderingContext} context + */ + Render.debug = function(engine, context) { + var c = context, + world = engine.world, + render = engine.render, + options = render.options, + space = " "; + + if (engine.timing.timestamp - (render.debugTimestamp || 0) >= 500) { + var text = ""; + text += "delta: " + engine.timing.delta.toFixed(3) + space; + text += "fps: " + Math.round(engine.timing.fps) + space; + text += "correction: " + engine.timing.correction.toFixed(3) + space; + text += "bodies: " + world.bodies.length + space; + + if (engine.broadphase.controller === Grid) + text += "buckets: " + engine.metrics.buckets + space; + + text += "\n"; + + text += "collisions: " + engine.metrics.collisions + space; + text += "pairs: " + engine.pairs.length + space; + text += "broad: " + engine.metrics.broadEff + space; + text += "mid: " + engine.metrics.midEff + space; + text += "narrow: " + engine.metrics.narrowEff + space; + render.debugString = text; + render.debugTimestamp = engine.timing.timestamp; + } + + if (render.debugString) { + c.font = "12px Arial"; + + if (options.wireframes) { + c.fillStyle = 'rgba(255,255,255,0.5)'; + } else { + c.fillStyle = 'rgba(0,0,0,0.5)'; + } + + var split = render.debugString.split('\n'); + + for (var i = 0; i < split.length; i++) { + c.fillText(split[i], 50, 50 + i * 18); + } + } + }; + + /** + * Description + * @method constraint + * @param {constraint} constraint + * @param {RenderingContext} context + */ + Render.constraint = function(constraint, context) { + var bodyA = constraint.bodyA, + bodyB = constraint.bodyB, + c = context; + + if (!constraint.pointA || !constraint.pointB) + return; + + if (bodyA) { + c.beginPath(); + c.moveTo(bodyA.position.x + constraint.pointA.x, bodyA.position.y + constraint.pointA.y); + } else { + c.beginPath(); + c.moveTo(constraint.pointA.x, constraint.pointA.y); + } + + if (bodyB) { + c.lineTo(bodyB.position.x + constraint.pointB.x, bodyB.position.y + constraint.pointB.y); + } else { + c.lineTo(constraint.pointB.x, constraint.pointB.y); + } + + c.lineWidth = constraint.lineWidth; + c.strokeStyle = constraint.strokeStyle; + c.stroke(); + }; + + /** + * Description + * @method bodyShadow + * @param {engine} engine + * @param {body} body + * @param {RenderingContext} context + */ + Render.bodyShadow = function(engine, body, context) { + var c = context, + render = engine.render; + + if (body.circleRadius) { + c.beginPath(); + c.arc(body.position.x, body.position.y, body.circleRadius, 0, 2 * Math.PI); + c.closePath(); + } else { + c.beginPath(); + c.moveTo(body.vertices[0].x, body.vertices[0].y); + for (var j = 1; j < body.vertices.length; j++) { + c.lineTo(body.vertices[j].x, body.vertices[j].y); + } + c.closePath(); + } + + var distanceX = body.position.x - render.options.width * 0.5, + distanceY = body.position.y - render.options.height * 0.2, + distance = Math.abs(distanceX) + Math.abs(distanceY); + + c.shadowColor = 'rgba(0,0,0,0.15)'; + c.shadowOffsetX = 0.05 * distanceX; + c.shadowOffsetY = 0.05 * distanceY; + c.shadowBlur = 1 + 12 * Math.min(1, distance / 1000); + + c.fill(); + + c.shadowColor = null; + c.shadowOffsetX = null; + c.shadowOffsetY = null; + c.shadowBlur = null; + }; + + /** + * Description + * @method body + * @param {engine} engine + * @param {body} body + * @param {RenderingContext} context + */ + Render.body = function(engine, body, context) { + var c = context, + render = engine.render, + options = render.options; + + // body bounds + if (options.showBounds) { + c.beginPath(); + c.rect(body.bounds.min.x, body.bounds.min.y, body.bounds.max.x - body.bounds.min.x, body.bounds.max.y - body.bounds.min.y); + c.lineWidth = 1; + if (options.wireframes) { + c.strokeStyle = 'rgba(255,255,255,0.08)'; + } else { + c.strokeStyle = 'rgba(0,0,0,0.1)'; + } + c.stroke(); + } + + // body polygon + if (body.circleRadius) { + c.beginPath(); + c.arc(body.position.x, body.position.y, body.circleRadius, 0, 2 * Math.PI); + c.closePath(); + } else { + c.beginPath(); + c.moveTo(body.vertices[0].x, body.vertices[0].y); + for (var j = 1; j < body.vertices.length; j++) { + c.lineTo(body.vertices[j].x, body.vertices[j].y); + } + c.closePath(); + } + + if (!options.wireframes) { + c.fillStyle = body.fillStyle; + if (options.showSleeping && body.isSleeping) + c.fillStyle = Common.shadeColor(body.fillStyle, 50); + c.lineWidth = body.lineWidth; + c.strokeStyle = body.strokeStyle; + c.fill(); + c.stroke(); + } else { + c.lineWidth = 1; + c.strokeStyle = '#bbb'; + if (options.showSleeping && body.isSleeping) + c.strokeStyle = 'rgba(255,255,255,0.2)'; + c.stroke(); + } + + // angle indicator + if (options.showAngleIndicator && !options.showAxes) { + c.beginPath(); + c.moveTo(body.position.x, body.position.y); + c.lineTo((body.vertices[0].x + body.vertices[body.vertices.length-1].x) / 2, + (body.vertices[0].y + body.vertices[body.vertices.length-1].y) / 2); + c.lineWidth = 1; + if (options.wireframes) { + c.strokeStyle = 'indianred'; + } else { + c.strokeStyle = body.strokeStyle; + } + c.stroke(); + } + + // axes + if (options.showAxes) { + for (var i = 0; i < body.axes.length; i++) { + var axis = body.axes[i]; + c.beginPath(); + c.moveTo(body.position.x, body.position.y); + c.lineTo(body.position.x + axis.x * 20, body.position.y + axis.y * 20); + c.lineWidth = 1; + if (options.wireframes) { + c.strokeStyle = 'indianred'; + } else { + c.strokeStyle = body.strokeStyle; + } + c.stroke(); + } + } + + // positions + if (options.showPositions) { + c.beginPath(); + c.arc(body.position.x, body.position.y, 3, 0, 2 * Math.PI, false); + if (options.wireframes) { + c.fillStyle = 'indianred'; + } else { + c.fillStyle = 'rgba(0,0,0,0.5)'; + } + c.fill(); + c.beginPath(); + c.arc(body.positionPrev.x, body.positionPrev.y, 2, 0, 2 * Math.PI, false); + c.fillStyle = 'rgba(255,165,0,0.8)'; + c.fill(); + } + + // body velocity vector + if (options.showVelocity) { + c.beginPath(); + c.moveTo(body.position.x, body.position.y); + c.lineTo(body.position.x + (body.position.x - body.positionPrev.x) * 2, body.position.y + (body.position.y - body.positionPrev.y) * 2); + c.lineWidth = 3; + c.strokeStyle = 'cornflowerblue'; + c.stroke(); + } + + // body id + if (options.showIds) { + c.font = "12px Arial"; + c.fillStyle = 'rgba(255,255,255,0.5)'; + c.fillText(body.id, body.position.x + 10, body.position.y - 10); + } + }; + + /** + * Description + * @method collision + * @param {engine} engine + * @param {pair} pair + * @param {RenderingContext} context + */ + Render.collision = function(engine, pair, context) { + var c = context, + collision = pair.collision, + options = engine.render.options; + + for (var i = 0; i < pair.activeContacts.length; i++) { + var contact = pair.activeContacts[i], + vertex = contact.vertex; + c.beginPath(); + //c.arc(vertex.x, vertex.y, 2.5, 0, 2 * Math.PI, false); + c.rect(vertex.x - 1.5, vertex.y - 1.5, 3.5, 3.5); + if (options.wireframes) { + c.fillStyle = 'rgba(255,255,255,0.7)'; + } else { + c.fillStyle = 'orange'; + } + c.fill(); + } + + if (pair.activeContacts.length > 0) { + var normalPosX = pair.activeContacts[0].vertex.x, + normalPosY = pair.activeContacts[0].vertex.y; + + if (pair.activeContacts.length === 2) { + normalPosX = (pair.activeContacts[0].vertex.x + pair.activeContacts[1].vertex.x) / 2; + normalPosY = (pair.activeContacts[0].vertex.y + pair.activeContacts[1].vertex.y) / 2; + } + + // collision normal + c.beginPath(); + c.moveTo(normalPosX - collision.normal.x * 8, normalPosY - collision.normal.y * 8); + c.lineWidth = 1; + c.lineTo(normalPosX, normalPosY); + if (options.wireframes) { + c.strokeStyle = 'rgba(255,165,0,0.7)'; + } else { + c.strokeStyle = 'orange'; + } + c.stroke(); + } + }; + + /** + * Description + * @method grid + * @param {engine} engine + * @param {grid} grid + * @param {RenderingContext} context + */ + Render.grid = function(engine, grid, context) { + var c = context, + options = engine.render.options; + + c.lineWidth = 1; + + if (options.wireframes) { + c.strokeStyle = 'rgba(255,180,0,0.1)'; + } else { + c.strokeStyle = 'rgba(255,180,0,0.5)'; + } + + var bucketKeys = Common.keys(grid.buckets); + + for (var i = 0; i < bucketKeys.length; i++) { + var bucketId = bucketKeys[i]; + + if (grid.buckets[bucketId].length < 2) + continue; + + var region = bucketId.split(','); + c.beginPath(); + c.rect(0.5 + parseInt(region[0], 10) * grid.bucketWidth, + 0.5 + parseInt(region[1], 10) * grid.bucketHeight, + grid.bucketWidth, + grid.bucketHeight); + c.stroke(); + } + }; + + /** + * Description + * @method _createCanvas + * @private + * @param {} width + * @param {} height + * @return canvas + */ + var _createCanvas = function(width, height) { + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.oncontextmenu = function() { return false; }; + canvas.onselectstart = function() { return false; }; + return canvas; + }; + +})(); + +; // End src/render/Render.js + + +// aliases + +World.addComposite = Composite.add; +World.addBody = Composite.addBody; +World.addConstraint = Composite.addConstraint; + +// exports + +Matter.Body = Body; +Matter.Composite = Composite; +Matter.World = World; +Matter.Contact = Contact; +Matter.Detector = Detector; +Matter.Grid = Grid; +Matter.Manager = Manager; +Matter.Pair = Pair; +Matter.Resolver = Resolver; +Matter.SAT = SAT; +Matter.Constraint = Constraint; +Matter.MouseConstraint = MouseConstraint; +Matter.Common = Common; +Matter.Engine = Engine; +Matter.Metrics = Metrics; +Matter.Mouse = Mouse; +Matter.Sleeping = Sleeping; +Matter.Bodies = Bodies; +Matter.Composites = Composites; +Matter.Axes = Axes; +Matter.Bounds = Bounds; +Matter.Vector = Vector; +Matter.Vertices = Vertices; +Matter.Gui = Gui; +Matter.Render = Render; + +// CommonJS module +if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = Matter; + } + exports.Matter = Matter; +} + +// AMD module +if (typeof define === 'function' && define.amd) { + define('Matter', [], function () { + return Matter; + }); +} + +// browser +if (typeof window === 'object' && typeof window.document === 'object') { + window.Matter = Matter; +} + +// End Matter namespace closure + +})(); \ No newline at end of file