mirror of
https://github.com/liabru/matter-js.git
synced 2024-11-30 10:20:52 -05:00
6360 lines
No EOL
191 KiB
JavaScript
6360 lines
No EOL
191 KiB
JavaScript
/**
|
|
* matter-0.8.0.js 0.8.0-alpha 2014-05-04
|
|
* 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 _nextGroupId = 1;
|
|
|
|
/**
|
|
* Description to be written.
|
|
* @method create
|
|
* @param {} options
|
|
* @return {body} body
|
|
*/
|
|
Body.create = function(options) {
|
|
var defaults = {
|
|
id: Common.nextId(),
|
|
type: 'body',
|
|
label: 'Body',
|
|
angle: 0,
|
|
vertices: Vertices.fromPath('L 0 0 L 40 0 L 40 40 L 0 40'),
|
|
position: { x: 0, y: 0 },
|
|
force: { x: 0, y: 0 },
|
|
torque: 0,
|
|
positionImpulse: { x: 0, y: 0 },
|
|
constraintImpulse: { x: 0, y: 0, angle: 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,
|
|
groupId: 0,
|
|
slop: 0.05,
|
|
timeScale: 1,
|
|
render: {
|
|
visible: true,
|
|
sprite: {
|
|
xScale: 1,
|
|
yScale: 1
|
|
},
|
|
lineWidth: 1.5
|
|
}
|
|
};
|
|
|
|
var body = Common.extend(defaults, options);
|
|
|
|
_initProperties(body);
|
|
|
|
return body;
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method nextGroupId
|
|
* @return {Number} Unique groupID
|
|
*/
|
|
Body.nextGroupId = function() {
|
|
return _nextGroupId++;
|
|
};
|
|
|
|
/**
|
|
* Initialises body properties
|
|
* @method _initProperties
|
|
* @private
|
|
* @param {body} body
|
|
*/
|
|
var _initProperties = function(body) {
|
|
// calculated properties
|
|
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.render.fillStyle = body.render.fillStyle || (body.isStatic ? '#eeeeee' : Common.choose(['#556270', '#4ECDC4', '#C7F464', '#FF6B6B', '#C44D58']));
|
|
body.render.strokeStyle = body.render.strokeStyle || Common.shadeColor(body.render.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);
|
|
|
|
Body.setStatic(body, body.isStatic);
|
|
Sleeping.set(body, body.isSleeping);
|
|
};
|
|
|
|
/**
|
|
* Sets the body as static, including isStatic flag and setting mass and inertia to Infinity
|
|
* @method setStatic
|
|
* @param {bool} isStatic
|
|
*/
|
|
Body.setStatic = function(body, isStatic) {
|
|
body.isStatic = isStatic;
|
|
|
|
if (isStatic) {
|
|
body.restitution = 0;
|
|
body.friction = 1;
|
|
body.mass = body.inertia = body.density = Infinity;
|
|
body.inverseMass = body.inverseInertia = 0;
|
|
body.render.lineWidth = 1;
|
|
|
|
body.positionPrev.x = body.position.x;
|
|
body.positionPrev.y = body.position.y;
|
|
body.anglePrev = body.angle;
|
|
body.angularVelocity = 0;
|
|
body.speed = 0;
|
|
body.angularSpeed = 0;
|
|
body.motion = 0;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method resetForcesAll
|
|
* @param {body[]} bodies
|
|
*/
|
|
Body.resetForcesAll = function(bodies) {
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i];
|
|
|
|
// reset force buffers
|
|
body.force.x = 0;
|
|
body.force.y = 0;
|
|
body.torque = 0;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method applyGravityAll
|
|
* @param {body[]} bodies
|
|
* @param {vector} gravity
|
|
*/
|
|
Body.applyGravityAll = function(bodies, gravity) {
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i];
|
|
|
|
if (body.isStatic || body.isSleeping)
|
|
continue;
|
|
|
|
// 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} timeScale
|
|
* @param {number} correction
|
|
* @param {bounds} worldBounds
|
|
*/
|
|
Body.updateAll = function(bodies, deltaTime, timeScale, 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, timeScale, correction);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method update
|
|
* @param {body} body
|
|
* @param {number} deltaTime
|
|
* @param {number} timeScale
|
|
* @param {number} correction
|
|
*/
|
|
Body.update = function(body, deltaTime, timeScale, correction) {
|
|
var deltaTimeSquared = Math.pow(deltaTime * timeScale * body.timeScale, 2);
|
|
|
|
// from the previous step
|
|
var frictionAir = 1 - body.frictionAir * timeScale * body.timeScale,
|
|
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);
|
|
if (body.angularVelocity !== 0) {
|
|
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);
|
|
};
|
|
|
|
/**
|
|
* Scales the body, including updating physical properties (mass, area, axes, inertia), from a point (default is centre)
|
|
* @method translate
|
|
* @param {body} body
|
|
* @param {number} scaleX
|
|
* @param {number} scaleY
|
|
* @param {vector} point
|
|
*/
|
|
Body.scale = function(body, scaleX, scaleY, point) {
|
|
// scale vertices
|
|
Vertices.scale(body.vertices, scaleX, scaleY, point);
|
|
|
|
// update properties
|
|
body.axes = Axes.fromVertices(body.vertices);
|
|
body.area = Vertices.area(body.vertices);
|
|
body.mass = body.density * body.area;
|
|
body.inverseMass = 1 / body.mass;
|
|
|
|
// update inertia (requires vertices to be at origin)
|
|
Vertices.translate(body.vertices, { x: -body.position.x, y: -body.position.y });
|
|
body.inertia = Vertices.inertia(body.vertices, body.mass);
|
|
body.inverseInertia = 1 / body.inertia;
|
|
Vertices.translate(body.vertices, { x: body.position.x, y: body.position.y });
|
|
|
|
// update bounds
|
|
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({
|
|
id: Common.nextId(),
|
|
type: 'composite',
|
|
parent: null,
|
|
isModified: false,
|
|
bodies: [],
|
|
constraints: [],
|
|
composites: [],
|
|
label: 'Composite'
|
|
}, options);
|
|
};
|
|
|
|
/**
|
|
* Sets the composite's `isModified` flag.
|
|
* If `updateParents` is true, all parents will be set (default: false).
|
|
* If `updateChildren` is true, all children will be set (default: false).
|
|
* @method setModified
|
|
* @param {composite} composite
|
|
* @param {boolean} isModified
|
|
* @param {boolean} updateParents
|
|
* @param {boolean} updateChildren
|
|
*/
|
|
Composite.setModified = function(composite, isModified, updateParents, updateChildren) {
|
|
composite.isModified = isModified;
|
|
|
|
if (updateParents && composite.parent) {
|
|
Composite.setModified(composite.parent, isModified, updateParents, updateChildren);
|
|
}
|
|
|
|
if (updateChildren) {
|
|
for(var i = 0; i < composite.composites.length; i++) {
|
|
var childComposite = composite.composites[i];
|
|
Composite.setModified(childComposite, isModified, updateParents, updateChildren);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Generic add function. Adds one or many body(s), constraint(s) or a composite(s) to the given composite.
|
|
* @method add
|
|
* @param {composite} composite
|
|
* @param {} object
|
|
* @return {composite} The original composite with the objects added
|
|
*/
|
|
Composite.add = function(composite, object) {
|
|
var objects = [].concat(object);
|
|
|
|
for (var i = 0; i < objects.length; i++) {
|
|
var obj = objects[i];
|
|
|
|
switch (obj.type) {
|
|
|
|
case 'body':
|
|
Composite.addBody(composite, obj);
|
|
break;
|
|
case 'constraint':
|
|
Composite.addConstraint(composite, obj);
|
|
break;
|
|
case 'composite':
|
|
Composite.addComposite(composite, obj);
|
|
break;
|
|
case 'mouseConstraint':
|
|
Composite.addConstraint(composite, obj.constraint);
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
return composite;
|
|
};
|
|
|
|
/**
|
|
* Generic remove function. Removes one or many body(s), constraint(s) or a composite(s) to the given composite.
|
|
* Optionally searching its children recursively.
|
|
* @method remove
|
|
* @param {composite} composite
|
|
* @param {} object
|
|
* @param {boolean} deep
|
|
* @return {composite} The original composite with the objects removed
|
|
*/
|
|
Composite.remove = function(composite, object, deep) {
|
|
var objects = [].concat(object);
|
|
|
|
for (var i = 0; i < objects.length; i++) {
|
|
var obj = objects[i];
|
|
|
|
switch (obj.type) {
|
|
|
|
case 'body':
|
|
Composite.removeBody(composite, obj, deep);
|
|
break;
|
|
case 'constraint':
|
|
Composite.removeConstraint(composite, obj, deep);
|
|
break;
|
|
case 'composite':
|
|
Composite.removeComposite(composite, obj, deep);
|
|
break;
|
|
case 'mouseConstraint':
|
|
Composite.removeConstraint(composite, obj.constraint);
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
return composite;
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method addComposite
|
|
* @param {composite} compositeA
|
|
* @param {composite} compositeB
|
|
* @return {composite} The original compositeA with the objects from compositeB added
|
|
*/
|
|
Composite.addComposite = function(compositeA, compositeB) {
|
|
compositeA.composites.push(compositeB);
|
|
compositeB.parent = compositeA;
|
|
Composite.setModified(compositeA, true, true, false);
|
|
return compositeA;
|
|
};
|
|
|
|
/**
|
|
* Removes a composite from the given composite, and optionally searching its children recursively
|
|
* @method removeComposite
|
|
* @param {composite} compositeA
|
|
* @param {composite} compositeB
|
|
* @param {boolean} deep
|
|
* @return {composite} The original compositeA with the composite removed
|
|
*/
|
|
Composite.removeComposite = function(compositeA, compositeB, deep) {
|
|
var position = compositeA.composites.indexOf(compositeB);
|
|
if (position !== -1) {
|
|
Composite.removeCompositeAt(compositeA, position);
|
|
Composite.setModified(compositeA, true, true, false);
|
|
}
|
|
|
|
if (deep) {
|
|
for (var i = 0; i < compositeA.composites.length; i++){
|
|
Composite.removeComposite(compositeA.composites[i], compositeB, true);
|
|
}
|
|
}
|
|
|
|
return compositeA;
|
|
};
|
|
|
|
/**
|
|
* Removes a composite from the given composite
|
|
* @method removeCompositeAt
|
|
* @param {composite} composite
|
|
* @param {number} position
|
|
* @return {composite} The original composite with the composite removed
|
|
*/
|
|
Composite.removeCompositeAt = function(composite, position) {
|
|
composite.composites.splice(position, 1);
|
|
Composite.setModified(composite, true, true, false);
|
|
return composite;
|
|
};
|
|
|
|
/**
|
|
* 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.push(body);
|
|
Composite.setModified(composite, true, true, false);
|
|
return composite;
|
|
};
|
|
|
|
/**
|
|
* Removes a body from the given composite, and optionally searching its children recursively
|
|
* @method removeBody
|
|
* @param {composite} composite
|
|
* @param {body} body
|
|
* @param {boolean} deep
|
|
* @return {composite} The original composite with the body removed
|
|
*/
|
|
Composite.removeBody = function(composite, body, deep) {
|
|
var position = composite.bodies.indexOf(body);
|
|
if (position !== -1) {
|
|
Composite.removeBodyAt(composite, position);
|
|
Composite.setModified(composite, true, true, false);
|
|
}
|
|
|
|
if (deep) {
|
|
for (var i = 0; i < composite.composites.length; i++){
|
|
Composite.removeBody(composite.composites[i], body, true);
|
|
}
|
|
}
|
|
|
|
return composite;
|
|
};
|
|
|
|
/**
|
|
* Removes a body from the given composite
|
|
* @method removeBodyAt
|
|
* @param {composite} composite
|
|
* @param {number} position
|
|
* @return {composite} The original composite with the body removed
|
|
*/
|
|
Composite.removeBodyAt = function(composite, position) {
|
|
composite.bodies.splice(position, 1);
|
|
Composite.setModified(composite, true, true, false);
|
|
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.push(constraint);
|
|
Composite.setModified(composite, true, true, false);
|
|
return composite;
|
|
};
|
|
|
|
/**
|
|
* Removes a constraint from the given composite, and optionally searching its children recursively
|
|
* @method removeConstraint
|
|
* @param {composite} composite
|
|
* @param {constraint} constraint
|
|
* @param {boolean} deep
|
|
* @return {composite} The original composite with the constraint removed
|
|
*/
|
|
Composite.removeConstraint = function(composite, constraint, deep) {
|
|
var position = composite.constraints.indexOf(constraint);
|
|
if (position !== -1) {
|
|
Composite.removeConstraintAt(composite, position);
|
|
}
|
|
|
|
if (deep) {
|
|
for (var i = 0; i < composite.composites.length; i++){
|
|
Composite.removeConstraint(composite.composites[i], constraint, true);
|
|
}
|
|
}
|
|
|
|
return composite;
|
|
};
|
|
|
|
/**
|
|
* Removes a body from the given composite
|
|
* @method removeConstraintAt
|
|
* @param {composite} composite
|
|
* @param {number} position
|
|
* @return {composite} The original composite with the constraint removed
|
|
*/
|
|
Composite.removeConstraintAt = function(composite, position) {
|
|
composite.constraints.splice(position, 1);
|
|
Composite.setModified(composite, true, true, false);
|
|
return composite;
|
|
};
|
|
|
|
/**
|
|
* Removes all bodies, constraints and composites from the given composite
|
|
* Optionally clearing its children recursively
|
|
* @method clear
|
|
* @param {world} world
|
|
* @param {boolean} keepStatic
|
|
* @param {boolean} deep
|
|
*/
|
|
Composite.clear = function(composite, keepStatic, deep) {
|
|
if (deep) {
|
|
for (var i = 0; i < composite.composites.length; i++){
|
|
Composite.clear(composite.composites[i], keepStatic, true);
|
|
}
|
|
}
|
|
|
|
if (keepStatic) {
|
|
composite.bodies = composite.bodies.filter(function(body) { return body.isStatic; });
|
|
} else {
|
|
composite.bodies.length = 0;
|
|
}
|
|
|
|
composite.constraints.length = 0;
|
|
composite.composites.length = 0;
|
|
Composite.setModified(composite, true, true, false);
|
|
|
|
return composite;
|
|
};
|
|
|
|
/**
|
|
* Returns all bodies in the given composite, including all bodies in its children, recursively
|
|
* @method allBodies
|
|
* @param {composite} composite
|
|
* @return {body[]} All the bodies
|
|
*/
|
|
Composite.allBodies = function(composite) {
|
|
var bodies = [].concat(composite.bodies);
|
|
|
|
for (var i = 0; i < composite.composites.length; i++)
|
|
bodies = bodies.concat(Composite.allBodies(composite.composites[i]));
|
|
|
|
return bodies;
|
|
};
|
|
|
|
/**
|
|
* Returns all constraints in the given composite, including all constraints in its children, recursively
|
|
* @method allConstraints
|
|
* @param {composite} composite
|
|
* @return {constraint[]} All the constraints
|
|
*/
|
|
Composite.allConstraints = function(composite) {
|
|
var constraints = [].concat(composite.constraints);
|
|
|
|
for (var i = 0; i < composite.composites.length; i++)
|
|
constraints = constraints.concat(Composite.allConstraints(composite.composites[i]));
|
|
|
|
return constraints;
|
|
};
|
|
|
|
/**
|
|
* Returns all composites in the given composite, including all composites in its children, recursively
|
|
* @method allComposites
|
|
* @param {composite} composite
|
|
* @return {composite[]} All the composites
|
|
*/
|
|
Composite.allComposites = function(composite) {
|
|
var composites = [].concat(composite.composites);
|
|
|
|
for (var i = 0; i < composite.composites.length; i++)
|
|
composites = composites.concat(Composite.allComposites(composite.composites[i]));
|
|
|
|
return composites;
|
|
};
|
|
|
|
/**
|
|
* Searches the composite recursively for an object matching the type and id supplied, null if not found
|
|
* @method get
|
|
* @param {composite} composite
|
|
* @param {number} id
|
|
* @param {string} type
|
|
* @return {object} The requested object, if found
|
|
*/
|
|
Composite.get = function(composite, id, type) {
|
|
var objects,
|
|
object;
|
|
|
|
switch (type) {
|
|
case 'body':
|
|
objects = Composite.allBodies(composite);
|
|
break;
|
|
case 'constraint':
|
|
objects = Composite.allConstraints(composite);
|
|
break;
|
|
case 'composite':
|
|
objects = Composite.allComposites(composite).concat(composite);
|
|
break;
|
|
}
|
|
|
|
if (!objects)
|
|
return null;
|
|
|
|
object = objects.filter(function(object) {
|
|
return object.id.toString() === id.toString();
|
|
});
|
|
|
|
return object.length === 0 ? null : object[0];
|
|
};
|
|
|
|
/**
|
|
* Moves the given object(s) from compositeA to compositeB (equal to a remove followed by an add)
|
|
* @method move
|
|
* @param {compositeA} compositeA
|
|
* @param {object[]} objects
|
|
* @param {compositeB} compositeB
|
|
* @return {composite} Returns compositeA
|
|
*/
|
|
Composite.move = function(compositeA, objects, compositeB) {
|
|
Composite.remove(compositeA, objects);
|
|
Composite.add(compositeB, objects);
|
|
return compositeA;
|
|
};
|
|
|
|
/**
|
|
* Assigns new ids for all objects in the composite, recursively
|
|
* @method rebase
|
|
* @param {composite} composite
|
|
* @return {composite} Returns composite
|
|
*/
|
|
Composite.rebase = function(composite) {
|
|
var objects = Composite.allBodies(composite)
|
|
.concat(Composite.allConstraints(composite))
|
|
.concat(Composite.allComposites(composite));
|
|
|
|
for (var i = 0; i < objects.length; i++) {
|
|
objects[i].id = Common.nextId();
|
|
}
|
|
|
|
Composite.setModified(composite, true, true, false);
|
|
|
|
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 composite = Composite.create();
|
|
|
|
var defaults = {
|
|
label: 'World',
|
|
gravity: { x: 0, y: 1 },
|
|
bounds: {
|
|
min: { x: 0, y: 0 },
|
|
max: { x: 800, y: 600 }
|
|
}
|
|
};
|
|
|
|
return Common.extend(composite, defaults, options);
|
|
};
|
|
|
|
// World is a Composite body
|
|
// see src/module/Outro.js for these aliases:
|
|
|
|
/**
|
|
* An alias for Composite.clear since World is also a Composite (see Outro.js)
|
|
* @method clear
|
|
* @param {world} world
|
|
* @param {boolean} keepStatic
|
|
*/
|
|
|
|
/**
|
|
* 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[]} broadphasePairs
|
|
* @param {engine} engine
|
|
* @return {array} collisions
|
|
*/
|
|
Detector.collisions = function(broadphasePairs, engine) {
|
|
var collisions = [],
|
|
metrics = engine.metrics,
|
|
pairsTable = engine.pairs.table;
|
|
|
|
for (var i = 0; i < broadphasePairs.length; i++) {
|
|
var bodyA = broadphasePairs[i][0],
|
|
bodyB = broadphasePairs[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)) {
|
|
|
|
// find a previous collision we could reuse
|
|
var pairId = Pair.id(bodyA, bodyB),
|
|
pair = pairsTable[pairId],
|
|
previousCollision;
|
|
|
|
if (pair && pair.isActive) {
|
|
previousCollision = pair.collision;
|
|
} else {
|
|
previousCollision = null;
|
|
}
|
|
|
|
// narrow phase
|
|
var collision = SAT.collides(bodyA, bodyB, previousCollision);
|
|
|
|
metrics.narrowphaseTests += 1;
|
|
|
|
if (collision.reused)
|
|
metrics.narrowReuseCount += 1;
|
|
|
|
if (collision.collided) {
|
|
collisions.push(collision);
|
|
metrics.narrowDetections += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return collisions;
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method bruteForce
|
|
* @param {body[]} bodies
|
|
* @param {engine} engine
|
|
* @return {array} collisions
|
|
*/
|
|
Detector.bruteForce = function(bodies, engine) {
|
|
var collisions = [],
|
|
metrics = engine.metrics,
|
|
pairsTable = engine.pairs.table;
|
|
|
|
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)) {
|
|
|
|
// find a previous collision we could reuse
|
|
var pairId = Pair.id(bodyA, bodyB),
|
|
pair = pairsTable[pairId],
|
|
previousCollision;
|
|
|
|
if (pair && pair.isActive) {
|
|
previousCollision = pair.collision;
|
|
} else {
|
|
previousCollision = null;
|
|
}
|
|
|
|
// narrow phase
|
|
var collision = SAT.collides(bodyA, bodyB, previousCollision);
|
|
|
|
metrics.narrowphaseTests += 1;
|
|
|
|
if (collision.reused)
|
|
metrics.narrowReuseCount += 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 && !forceUpdate)
|
|
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 = Pair.id(body, bodyB),
|
|
pair = grid.pairs[pairId];
|
|
|
|
if (pair) {
|
|
pair[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 bucket
|
|
bucket.splice(bucket.indexOf(body), 1);
|
|
|
|
// update pair counts
|
|
for (var i = 0; i < bucket.length; i++) {
|
|
// keep track of the number of buckets the pair exists in
|
|
// important for _createActivePairsList to work
|
|
var bodyB = bucket[i],
|
|
pairId = Pair.id(body, bodyB),
|
|
pair = grid.pairs[pairId];
|
|
|
|
if (pair)
|
|
pair[2] -= 1;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
} else {
|
|
delete grid.pairs[pairKeys[k]];
|
|
}
|
|
}
|
|
|
|
return pairs;
|
|
};
|
|
|
|
})();
|
|
|
|
; // End src/collision/Grid.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, timestamp) {
|
|
var bodyA = collision.bodyA,
|
|
bodyB = collision.bodyB;
|
|
|
|
var pair = {
|
|
id: Pair.id(bodyA, bodyB),
|
|
bodyA: bodyA,
|
|
bodyB: bodyB,
|
|
contacts: {},
|
|
activeContacts: [],
|
|
separation: 0,
|
|
isActive: true,
|
|
timeCreated: timestamp,
|
|
timeUpdated: timestamp,
|
|
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, timestamp);
|
|
|
|
return pair;
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method update
|
|
* @param {pair} pair
|
|
* @param {collision} collision
|
|
*/
|
|
Pair.update = function(pair, collision, timestamp) {
|
|
var contacts = pair.contacts,
|
|
supports = collision.supports,
|
|
activeContacts = pair.activeContacts;
|
|
|
|
pair.collision = collision;
|
|
activeContacts.length = 0;
|
|
|
|
if (collision.collided) {
|
|
for (var i = 0; i < supports.length; i++) {
|
|
var support = supports[i],
|
|
contactId = Contact.id(support),
|
|
contact = contacts[contactId];
|
|
|
|
if (contact) {
|
|
activeContacts.push(contact);
|
|
} else {
|
|
activeContacts.push(contacts[contactId] = Contact.create(support));
|
|
}
|
|
}
|
|
|
|
pair.separation = collision.depth;
|
|
Pair.setActive(pair, true, timestamp);
|
|
} else {
|
|
if (pair.isActive === true)
|
|
Pair.setActive(pair, false, timestamp);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method setActive
|
|
* @param {pair} pair
|
|
* @param {bool} isActive
|
|
*/
|
|
Pair.setActive = function(pair, isActive, timestamp) {
|
|
if (isActive) {
|
|
pair.isActive = true;
|
|
pair.timeUpdated = timestamp;
|
|
} else {
|
|
pair.isActive = false;
|
|
pair.activeContacts.length = 0;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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/Pairs.js
|
|
|
|
/**
|
|
* _Internal Class_, not generally used outside of the engine's internals.
|
|
*
|
|
* @class Pairs
|
|
*/
|
|
|
|
var Pairs = {};
|
|
|
|
(function() {
|
|
|
|
var _pairMaxIdleLife = 1000;
|
|
|
|
/**
|
|
* Creates a new pairs structure
|
|
* @method create
|
|
* @param {object} options
|
|
* @return {pairs} A new pairs structure
|
|
*/
|
|
Pairs.create = function(options) {
|
|
return Common.extend({
|
|
table: {},
|
|
list: [],
|
|
collisionStart: [],
|
|
collisionActive: [],
|
|
collisionEnd: []
|
|
}, options);
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method update
|
|
* @param {object} pairs
|
|
* @param {collision[]} collisions
|
|
*/
|
|
Pairs.update = function(pairs, collisions, timestamp) {
|
|
var pairsList = pairs.list,
|
|
pairsTable = pairs.table,
|
|
collisionStart = pairs.collisionStart,
|
|
collisionEnd = pairs.collisionEnd,
|
|
collisionActive = pairs.collisionActive,
|
|
activePairIds = [],
|
|
collision,
|
|
pairId,
|
|
pair,
|
|
i;
|
|
|
|
// clear collision state arrays, but maintain old reference
|
|
collisionStart.length = 0;
|
|
collisionEnd.length = 0;
|
|
collisionActive.length = 0;
|
|
|
|
for (i = 0; i < collisions.length; i++) {
|
|
collision = collisions[i];
|
|
|
|
if (collision.collided) {
|
|
pairId = Pair.id(collision.bodyA, collision.bodyB);
|
|
activePairIds.push(pairId);
|
|
|
|
pair = pairsTable[pairId];
|
|
|
|
if (pair) {
|
|
// pair already exists (but may or may not be active)
|
|
if (pair.isActive) {
|
|
// pair exists and is active
|
|
collisionActive.push(pair);
|
|
} else {
|
|
// pair exists but was inactive, so a collision has just started again
|
|
collisionStart.push(pair);
|
|
}
|
|
|
|
// update the pair
|
|
Pair.update(pair, collision, timestamp);
|
|
} else {
|
|
// pair did not exist, create a new pair
|
|
pair = Pair.create(collision, timestamp);
|
|
pairsTable[pairId] = pair;
|
|
|
|
// push the new pair
|
|
collisionStart.push(pair);
|
|
pairsList.push(pair);
|
|
}
|
|
}
|
|
}
|
|
|
|
// deactivate previously active pairs that are now inactive
|
|
for (i = 0; i < pairsList.length; i++) {
|
|
pair = pairsList[i];
|
|
if (pair.isActive && activePairIds.indexOf(pair.id) === -1) {
|
|
Pair.setActive(pair, false, timestamp);
|
|
collisionEnd.push(pair);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method removeOld
|
|
* @param {object} pairs
|
|
*/
|
|
Pairs.removeOld = function(pairs, timestamp) {
|
|
var pairsList = pairs.list,
|
|
pairsTable = pairs.table,
|
|
indexesToRemove = [],
|
|
pair,
|
|
collision,
|
|
pairIndex,
|
|
i;
|
|
|
|
for (i = 0; i < pairsList.length; i++) {
|
|
pair = pairsList[i];
|
|
collision = pair.collision;
|
|
|
|
// never remove sleeping pairs
|
|
if (collision.bodyA.isSleeping || collision.bodyB.isSleeping) {
|
|
pair.timeUpdated = timestamp;
|
|
continue;
|
|
}
|
|
|
|
// if pair is inactive for too long, mark it to be removed
|
|
if (timestamp - pair.timeUpdated > _pairMaxIdleLife) {
|
|
indexesToRemove.push(i);
|
|
}
|
|
}
|
|
|
|
// remove marked pairs
|
|
for (i = 0; i < indexesToRemove.length; i++) {
|
|
pairIndex = indexesToRemove[i] - i;
|
|
pair = pairsList[pairIndex];
|
|
delete pairsTable[pair.id];
|
|
pairsList.splice(pairIndex, 1);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Clears the given pairs structure
|
|
* @method create
|
|
* @param {object} options
|
|
* @param {pairs} pairs
|
|
*/
|
|
Pairs.clear = function(pairs) {
|
|
pairs.table = {};
|
|
pairs.list.length = 0;
|
|
pairs.collisionStart.length = 0;
|
|
pairs.collisionActive.length = 0;
|
|
pairs.collisionEnd.length = 0;
|
|
return pairs;
|
|
};
|
|
|
|
})();
|
|
|
|
; // End src/collision/Pairs.js
|
|
|
|
|
|
// Begin src/collision/Query.js
|
|
|
|
/**
|
|
* Functions for performing collision queries
|
|
*
|
|
* @class Query
|
|
*/
|
|
|
|
var Query = {};
|
|
|
|
(function() {
|
|
|
|
/**
|
|
* Casts a ray segment against a set of bodies and returns all collisions, ray width is optional. Intersection points are not provided.
|
|
* @method ray
|
|
* @param {body[]} bodies
|
|
* @param {vector} startPoint
|
|
* @param {vector} endPoint
|
|
* @return {object[]} Collisions
|
|
*/
|
|
Query.ray = function(bodies, startPoint, endPoint, rayWidth) {
|
|
rayWidth = rayWidth || Number.MIN_VALUE;
|
|
|
|
var rayAngle = Vector.angle(startPoint, endPoint),
|
|
rayLength = Vector.magnitude(Vector.sub(startPoint, endPoint)),
|
|
rayX = (endPoint.x + startPoint.x) * 0.5,
|
|
rayY = (endPoint.y + startPoint.y) * 0.5,
|
|
ray = Bodies.rectangle(rayX, rayY, rayLength, rayWidth, { angle: rayAngle }),
|
|
collisions = [];
|
|
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var bodyA = bodies[i];
|
|
|
|
if (Bounds.overlaps(bodyA.bounds, ray.bounds)) {
|
|
var collision = SAT.collides(bodyA, ray);
|
|
if (collision.collided) {
|
|
collision.body = collision.bodyA = collision.bodyB = bodyA;
|
|
collisions.push(collision);
|
|
}
|
|
}
|
|
}
|
|
|
|
return collisions;
|
|
};
|
|
|
|
/**
|
|
* Returns all bodies whose bounds are inside (or outside if set) the given set of bounds, from the given set of bodies
|
|
* @method region
|
|
* @param {body[]} bodies
|
|
* @param {bounds} bounds
|
|
* @param {bool} outside
|
|
* @return {body[]} The bodies matching the query
|
|
*/
|
|
Query.region = function(bodies, bounds, outside) {
|
|
var result = [];
|
|
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i],
|
|
overlaps = Bounds.overlaps(body.bounds, bounds);
|
|
if ((overlaps && !outside) || (!overlaps && outside))
|
|
result.push(body);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
})();
|
|
|
|
; // End src/collision/Query.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
|
|
* @param {number} timeScale
|
|
*/
|
|
Resolver.solvePosition = function(pairs, timeScale) {
|
|
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) * timeScale;
|
|
|
|
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, timeScale) {
|
|
var impulse = {},
|
|
timeScaleSquared = timeScale * timeScale;
|
|
|
|
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 * timeScaleSquared)
|
|
tangentImpulse = normalForce * pair.friction * timeScaleSquared * 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 * timeScaleSquared) {
|
|
// 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
|
|
|
|
var SAT = {};
|
|
|
|
(function() {
|
|
|
|
/**
|
|
* Description
|
|
* @method collides
|
|
* @param {body} bodyA
|
|
* @param {body} bodyB
|
|
* @param {collision} previousCollision
|
|
* @return {collision} collision
|
|
*/
|
|
SAT.collides = function(bodyA, bodyB, previousCollision) {
|
|
var overlapAB,
|
|
overlapBA,
|
|
minOverlap,
|
|
collision,
|
|
prevCol = previousCollision,
|
|
canReusePrevCol = false;
|
|
|
|
if (prevCol) {
|
|
// estimate total motion
|
|
var motion = bodyA.speed * bodyA.speed + bodyA.angularSpeed * bodyA.angularSpeed
|
|
+ bodyB.speed * bodyB.speed + bodyB.angularSpeed * bodyB.angularSpeed;
|
|
|
|
// we may be able to (partially) reuse collision result
|
|
// but only safe if collision was resting
|
|
canReusePrevCol = prevCol && prevCol.collided && motion < 0.2;
|
|
|
|
// reuse collision object
|
|
collision = prevCol;
|
|
} else {
|
|
collision = { collided: false, bodyA: bodyA, bodyB: bodyB };
|
|
}
|
|
|
|
if (prevCol && canReusePrevCol) {
|
|
// if we can reuse the collision result
|
|
// we only need to test the previously found axis
|
|
var axes = [prevCol.bodyA.axes[prevCol.axisNumber]];
|
|
|
|
minOverlap = _overlapAxes(prevCol.bodyA.vertices, prevCol.bodyB.vertices, axes);
|
|
collision.reused = true;
|
|
|
|
if (minOverlap.overlap <= 0) {
|
|
collision.collided = false;
|
|
return collision;
|
|
}
|
|
} else {
|
|
// if we can't reuse a result, perform a full SAT test
|
|
|
|
overlapAB = _overlapAxes(bodyA.vertices, bodyB.vertices, bodyA.axes);
|
|
|
|
if (overlapAB.overlap <= 0) {
|
|
collision.collided = false;
|
|
return collision;
|
|
}
|
|
|
|
overlapBA = _overlapAxes(bodyB.vertices, bodyA.vertices, bodyB.axes);
|
|
|
|
if (overlapBA.overlap <= 0) {
|
|
collision.collided = false;
|
|
return collision;
|
|
}
|
|
|
|
if (overlapAB.overlap < overlapBA.overlap) {
|
|
minOverlap = overlapAB;
|
|
collision.bodyA = bodyA;
|
|
collision.bodyB = bodyB;
|
|
} else {
|
|
minOverlap = overlapBA;
|
|
collision.bodyA = bodyB;
|
|
collision.bodyB = bodyA;
|
|
}
|
|
|
|
// important for reuse later
|
|
collision.axisNumber = minOverlap.axisNumber;
|
|
}
|
|
|
|
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) {
|
|
result.overlap = overlap;
|
|
return result;
|
|
}
|
|
|
|
if (overlap < result.overlap) {
|
|
result.overlap = overlap;
|
|
result.axis = axis;
|
|
result.axisNumber = i;
|
|
}
|
|
}
|
|
|
|
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,
|
|
_minDifference = 0.001;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
// render
|
|
var render = {
|
|
visible: true,
|
|
lineWidth: 2,
|
|
strokeStyle: '#666'
|
|
};
|
|
|
|
constraint.render = Common.extend(render, constraint.render);
|
|
|
|
// option defaults
|
|
constraint.id = constraint.id || Common.nextId();
|
|
constraint.label = constraint.label || 'Constraint';
|
|
constraint.type = 'constraint';
|
|
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 solveAll
|
|
* @param {constraint[]} constraints
|
|
* @param {number} timeScale
|
|
*/
|
|
Constraint.solveAll = function(constraints, timeScale) {
|
|
for (var i = 0; i < constraints.length; i++) {
|
|
Constraint.solve(constraints[i], timeScale);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method solve
|
|
* @param {constraint} constraint
|
|
* @param {number} timeScale
|
|
*/
|
|
Constraint.solve = function(constraint, timeScale) {
|
|
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 * timeScale * timeScale);
|
|
|
|
// if difference is very small, we can skip
|
|
if (Math.abs(1 - (currentLength / constraint.length)) < _minDifference * timeScale)
|
|
return;
|
|
|
|
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);
|
|
|
|
// keep track of applied impulses for post solving
|
|
bodyA.constraintImpulse.x -= force.x;
|
|
bodyA.constraintImpulse.y -= force.y;
|
|
bodyA.constraintImpulse.angle += torque;
|
|
|
|
// apply forces
|
|
bodyA.position.x -= force.x;
|
|
bodyA.position.y -= force.y;
|
|
bodyA.angle += torque;
|
|
}
|
|
|
|
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);
|
|
|
|
// keep track of applied impulses for post solving
|
|
bodyB.constraintImpulse.x += force.x;
|
|
bodyB.constraintImpulse.y += force.y;
|
|
bodyB.constraintImpulse.angle -= torque;
|
|
|
|
// apply forces
|
|
bodyB.position.x += force.x;
|
|
bodyB.position.y += force.y;
|
|
bodyB.angle -= torque;
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Performs body updates required after solving constraints
|
|
* @method postSolveAll
|
|
* @param {body[]} bodies
|
|
*/
|
|
Constraint.postSolveAll = function(bodies) {
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i],
|
|
impulse = body.constraintImpulse;
|
|
|
|
// update geometry and reset
|
|
Vertices.translate(body.vertices, impulse);
|
|
|
|
if (impulse.angle !== 0) {
|
|
Vertices.rotate(body.vertices, impulse.angle, body.position);
|
|
Axes.rotate(body.axes, impulse.angle);
|
|
impulse.angle = 0;
|
|
}
|
|
|
|
Bounds.update(body.bounds, body.vertices);
|
|
|
|
impulse.x = 0;
|
|
impulse.y = 0;
|
|
}
|
|
};
|
|
|
|
})();
|
|
|
|
; // End src/constraint/Constraint.js
|
|
|
|
|
|
// Begin src/constraint/MouseConstraint.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 MouseConstraint
|
|
*/
|
|
|
|
var MouseConstraint = {};
|
|
|
|
(function() {
|
|
|
|
/**
|
|
* Description
|
|
* @method create
|
|
* @param {engine} engine
|
|
* @param {} options
|
|
* @return {MouseConstraint} A new MouseConstraint
|
|
*/
|
|
MouseConstraint.create = function(engine, options) {
|
|
var mouse = engine.input.mouse;
|
|
|
|
var constraint = Constraint.create({
|
|
label: 'Mouse Constraint',
|
|
pointA: mouse.position,
|
|
pointB: { x: 0, y: 0 },
|
|
length: 0.01,
|
|
stiffness: 0.1,
|
|
angularStiffness: 1,
|
|
render: {
|
|
strokeStyle: '#90EE90',
|
|
lineWidth: 3
|
|
}
|
|
});
|
|
|
|
var defaults = {
|
|
type: 'mouseConstraint',
|
|
mouse: mouse,
|
|
dragBody: null,
|
|
dragPoint: null,
|
|
constraint: constraint
|
|
};
|
|
|
|
var mouseConstraint = Common.extend(defaults, options);
|
|
|
|
Events.on(engine, 'tick', function(event) {
|
|
var allBodies = Composite.allBodies(engine.world);
|
|
MouseConstraint.update(mouseConstraint, allBodies);
|
|
});
|
|
|
|
return mouseConstraint;
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method update
|
|
* @param {MouseConstraint} mouseConstraint
|
|
* @param {body[]} bodies
|
|
*/
|
|
MouseConstraint.update = function(mouseConstraint, bodies) {
|
|
var mouse = mouseConstraint.mouse,
|
|
constraint = mouseConstraint.constraint;
|
|
|
|
if (mouse.button === 0) {
|
|
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() {
|
|
|
|
Common._nextId = 0;
|
|
|
|
/**
|
|
* Description
|
|
* @method extend
|
|
* @param {} obj
|
|
* @param {boolean} deep
|
|
* @return {} obj extended
|
|
*/
|
|
Common.extend = function(obj, deep) {
|
|
var argsStart,
|
|
args,
|
|
deepClone;
|
|
|
|
if (typeof deep === 'boolean') {
|
|
argsStart = 2;
|
|
deepClone = deep;
|
|
} else {
|
|
argsStart = 1;
|
|
deepClone = true;
|
|
}
|
|
|
|
args = Array.prototype.slice.call(arguments, argsStart);
|
|
|
|
for (var i = 0; i < args.length; i++) {
|
|
var source = args[i];
|
|
|
|
if (source) {
|
|
for (var prop in source) {
|
|
if (deepClone && source[prop] && source[prop].constructor === Object) {
|
|
if (!obj[prop] || obj[prop].constructor === Object) {
|
|
obj[prop] = obj[prop] || {};
|
|
Common.extend(obj[prop], deepClone, source[prop]);
|
|
} else {
|
|
obj[prop] = source[prop];
|
|
}
|
|
} else {
|
|
obj[prop] = source[prop];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
};
|
|
|
|
/**
|
|
* Creates a new clone of the object, if deep is true references will also be cloned
|
|
* @method clone
|
|
* @param {} obj
|
|
* @param {bool} deep
|
|
* @return {} obj cloned
|
|
*/
|
|
Common.clone = function(obj, deep) {
|
|
return Common.extend({}, deep, 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 {number} the current timestamp (high-res if avaliable)
|
|
*/
|
|
Common.now = function() {
|
|
// http://stackoverflow.com/questions/221294/how-do-you-get-a-timestamp-in-javascript
|
|
// https://gist.github.com/davidwaterston/2982531
|
|
|
|
var perf = window.performance;
|
|
|
|
if (perf) {
|
|
perf.now = perf.now || perf.webkitNow || perf.msNow || perf.oNow || perf.mozNow;
|
|
return +(perf.now());
|
|
}
|
|
|
|
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);
|
|
};
|
|
|
|
/**
|
|
* Converts a CSS hex colour string into an integer
|
|
* @method colorToNumber
|
|
* @param {string} colorString
|
|
* @return {number} An integer representing the CSS hex string
|
|
*/
|
|
Common.colorToNumber = function(colorString) {
|
|
colorString = colorString.replace('#','');
|
|
|
|
if (colorString.length == 3) {
|
|
colorString = colorString.charAt(0) + colorString.charAt(0)
|
|
+ colorString.charAt(1) + colorString.charAt(1)
|
|
+ colorString.charAt(2) + colorString.charAt(2);
|
|
}
|
|
|
|
return parseInt(colorString, 16);
|
|
};
|
|
|
|
/**
|
|
* A wrapper for console.log, for providing errors and warnings
|
|
* @method log
|
|
* @param {string} message
|
|
* @param {string} type
|
|
*/
|
|
Common.log = function(message, type) {
|
|
if (!console || !console.log)
|
|
return;
|
|
|
|
var style;
|
|
|
|
switch (type) {
|
|
|
|
case 'warn':
|
|
style = 'color: coral';
|
|
break;
|
|
case 'error':
|
|
style = 'color: red';
|
|
break;
|
|
|
|
}
|
|
|
|
console.log('%c [Matter] ' + type + ': ' + message, style);
|
|
};
|
|
|
|
/**
|
|
* Returns the next unique sequential ID
|
|
* @method nextId
|
|
* @return {Number} Unique sequential ID
|
|
*/
|
|
Common.nextId = function() {
|
|
return Common._nextId++;
|
|
};
|
|
|
|
})();
|
|
|
|
; // 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: viewports
|
|
|
|
var Engine = {};
|
|
|
|
(function() {
|
|
|
|
var _fps = 60,
|
|
_deltaSampleSize = _fps,
|
|
_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) {
|
|
|
|
// options may be passed as the first (and only) argument
|
|
options = Common.isElement(element) ? options : element;
|
|
element = Common.isElement(element) ? element : null;
|
|
|
|
var defaults = {
|
|
enabled: true,
|
|
positionIterations: 6,
|
|
velocityIterations: 4,
|
|
constraintIterations: 2,
|
|
enableSleeping: false,
|
|
timeScale: 1,
|
|
input: {},
|
|
events: [],
|
|
timing: {
|
|
fps: _fps,
|
|
timestamp: 0,
|
|
delta: _delta,
|
|
correction: 1,
|
|
deltaMin: 1000 / _fps,
|
|
deltaMax: 1000 / (_fps * 0.5),
|
|
timeScale: 1,
|
|
isFixed: false
|
|
},
|
|
render: {
|
|
element: element,
|
|
controller: Render
|
|
}
|
|
};
|
|
|
|
var engine = Common.extend(defaults, options);
|
|
|
|
engine.render = engine.render.controller.create(engine.render);
|
|
engine.world = World.create(engine.world);
|
|
engine.pairs = Pairs.create();
|
|
engine.metrics = engine.metrics || Metrics.create();
|
|
engine.input.mouse = engine.input.mouse || Mouse.create(engine.render.canvas);
|
|
|
|
engine.broadphase = engine.broadphase || {
|
|
current: 'grid',
|
|
grid: {
|
|
controller: Grid,
|
|
instance: Grid.create(),
|
|
detector: Detector.collisions
|
|
},
|
|
bruteForce: {
|
|
detector: Detector.bruteForce
|
|
}
|
|
};
|
|
|
|
return engine;
|
|
};
|
|
|
|
/**
|
|
* An optional utility function that provides a game loop, that handles updating the engine for you.
|
|
* Calls `Engine.update` and `Engine.render` on the `requestAnimationFrame` event automatically.
|
|
* Handles time correction and non-fixed dynamic timing (if enabled).
|
|
* Triggers `beforeTick`, `tick` and `afterTick` events.
|
|
* @method run
|
|
* @param {engine} engine
|
|
*/
|
|
Engine.run = function(engine) {
|
|
var counterTimestamp = 0,
|
|
frameCounter = 0,
|
|
deltaHistory = [],
|
|
timePrev,
|
|
timeScalePrev = 1;
|
|
|
|
(function render(time){
|
|
_requestAnimationFrame(render);
|
|
|
|
if (!engine.enabled)
|
|
return;
|
|
|
|
var timing = engine.timing,
|
|
delta,
|
|
correction;
|
|
|
|
// create an event object
|
|
var event = {
|
|
timestamp: time
|
|
};
|
|
|
|
Events.trigger(engine, 'beforeTick', event);
|
|
|
|
if (timing.isFixed) {
|
|
// fixed timestep
|
|
delta = timing.delta;
|
|
} else {
|
|
// dynamic timestep based on wall clock between calls
|
|
delta = (time - timePrev) || timing.delta;
|
|
timePrev = time;
|
|
|
|
// 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 < timing.deltaMin ? timing.deltaMin : delta;
|
|
delta = delta > timing.deltaMax ? timing.deltaMax : delta;
|
|
|
|
// time correction for delta
|
|
correction = delta / timing.delta;
|
|
|
|
// update engine timing object
|
|
timing.delta = delta;
|
|
}
|
|
|
|
// time correction for time scaling
|
|
if (timeScalePrev !== 0)
|
|
correction *= timing.timeScale / timeScalePrev;
|
|
|
|
if (timing.timeScale === 0)
|
|
correction = 0;
|
|
|
|
timeScalePrev = timing.timeScale;
|
|
|
|
// fps counter
|
|
frameCounter += 1;
|
|
if (time - counterTimestamp >= 1000) {
|
|
timing.fps = frameCounter * ((time - counterTimestamp) / 1000);
|
|
counterTimestamp = time;
|
|
frameCounter = 0;
|
|
}
|
|
|
|
Events.trigger(engine, 'tick', event);
|
|
|
|
// if world has been modified, clear the render scene graph
|
|
if (engine.world.isModified)
|
|
engine.render.controller.clear(engine.render);
|
|
|
|
// update
|
|
Engine.update(engine, delta, correction);
|
|
|
|
// trigger events that may have occured during the step
|
|
_triggerCollisionEvents(engine);
|
|
_triggerMouseEvents(engine);
|
|
|
|
// render
|
|
Engine.render(engine);
|
|
|
|
Events.trigger(engine, 'afterTick', event);
|
|
})();
|
|
};
|
|
|
|
/**
|
|
* Moves the simulation forward in time by `delta` ms. Triggers `beforeUpdate` and `afterUpdate` events.
|
|
* @method update
|
|
* @param {engine} engine
|
|
* @param {number} delta
|
|
* @param {number} correction
|
|
* @return engine
|
|
*/
|
|
Engine.update = function(engine, delta, correction) {
|
|
correction = (typeof correction !== 'undefined') ? correction : 1;
|
|
|
|
var world = engine.world,
|
|
timing = engine.timing,
|
|
broadphase = engine.broadphase[engine.broadphase.current],
|
|
broadphasePairs = [],
|
|
i;
|
|
|
|
// increment timestamp
|
|
timing.timestamp += delta * timing.timeScale;
|
|
timing.correction = correction;
|
|
|
|
// create an event object
|
|
var event = {
|
|
timestamp: engine.timing.timestamp
|
|
};
|
|
|
|
Events.trigger(engine, 'beforeUpdate', event);
|
|
|
|
// get lists of all bodies and constraints, no matter what composites they are in
|
|
var allBodies = Composite.allBodies(world),
|
|
allConstraints = Composite.allConstraints(world);
|
|
|
|
// reset metrics logging
|
|
Metrics.reset(engine.metrics);
|
|
|
|
// if sleeping enabled, call the sleeping controller
|
|
if (engine.enableSleeping)
|
|
Sleeping.update(allBodies);
|
|
|
|
// applies gravity to all bodies
|
|
Body.applyGravityAll(allBodies, world.gravity);
|
|
|
|
// update all body position and rotation by integration
|
|
Body.updateAll(allBodies, delta, timing.timeScale, correction, world.bounds);
|
|
|
|
// update all constraints
|
|
for (i = 0; i < engine.constraintIterations; i++) {
|
|
Constraint.solveAll(allConstraints, timing.timeScale);
|
|
}
|
|
Constraint.postSolveAll(allBodies);
|
|
|
|
// broadphase pass: find potential collision pairs
|
|
if (broadphase.controller) {
|
|
|
|
// if world is dirty, we must flush the whole grid
|
|
if (world.isModified)
|
|
broadphase.controller.clear(broadphase.instance);
|
|
|
|
// update the grid buckets based on current bodies
|
|
broadphase.controller.update(broadphase.instance, allBodies, engine, world.isModified);
|
|
broadphasePairs = broadphase.instance.pairsList;
|
|
} else {
|
|
|
|
// if no broadphase set, we just pass all bodies
|
|
broadphasePairs = allBodies;
|
|
}
|
|
|
|
// narrowphase pass: find actual collisions, then create or update collision pairs
|
|
var collisions = broadphase.detector(broadphasePairs, engine);
|
|
|
|
// update collision pairs
|
|
var pairs = engine.pairs,
|
|
timestamp = timing.timestamp;
|
|
Pairs.update(pairs, collisions, timestamp);
|
|
Pairs.removeOld(pairs, timestamp);
|
|
|
|
// wake up bodies involved in collisions
|
|
if (engine.enableSleeping)
|
|
Sleeping.afterCollisions(pairs.list);
|
|
|
|
// iteratively resolve velocity between collisions
|
|
Resolver.preSolveVelocity(pairs.list);
|
|
for (i = 0; i < engine.velocityIterations; i++) {
|
|
Resolver.solveVelocity(pairs.list, timing.timeScale);
|
|
}
|
|
|
|
// iteratively resolve position between collisions
|
|
for (i = 0; i < engine.positionIterations; i++) {
|
|
Resolver.solvePosition(pairs.list, timing.timeScale);
|
|
}
|
|
Resolver.postSolvePosition(allBodies);
|
|
|
|
// update metrics log
|
|
Metrics.update(engine.metrics, engine);
|
|
|
|
// clear force buffers
|
|
Body.resetForcesAll(allBodies);
|
|
|
|
// clear all composite modified flags
|
|
if (world.isModified)
|
|
Composite.setModified(world, false, false, true);
|
|
|
|
Events.trigger(engine, 'afterUpdate', event);
|
|
|
|
return engine;
|
|
};
|
|
|
|
/**
|
|
* Renders the world by calling its defined renderer `engine.render.controller`. Triggers `beforeRender` and `afterRender` events.
|
|
* @method render
|
|
* @param {engine} engineA
|
|
* @param {engine} engineB
|
|
*/
|
|
Engine.render = function(engine) {
|
|
// create an event object
|
|
var event = {
|
|
timestamp: engine.timing.timestamp
|
|
};
|
|
|
|
Events.trigger(engine, 'beforeRender', event);
|
|
engine.render.controller.world(engine);
|
|
Events.trigger(engine, 'afterRender', event);
|
|
};
|
|
|
|
/**
|
|
* 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 = Composite.allBodies(engineA.world);
|
|
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i];
|
|
Sleeping.set(body, false);
|
|
body.id = Common.nextId();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method clear
|
|
* @param {engine} engine
|
|
*/
|
|
Engine.clear = function(engine) {
|
|
var world = engine.world;
|
|
|
|
Pairs.clear(engine.pairs);
|
|
|
|
var broadphase = engine.broadphase[engine.broadphase.current];
|
|
if (broadphase.controller) {
|
|
var bodies = Composite.allBodies(world);
|
|
broadphase.controller.clear(broadphase.instance);
|
|
broadphase.controller.update(broadphase.instance, bodies, engine, true);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Triggers mouse events
|
|
* @method _triggerMouseEvents
|
|
* @private
|
|
* @param {engine} engine
|
|
*/
|
|
var _triggerMouseEvents = function(engine) {
|
|
var mouse = engine.input.mouse,
|
|
mouseEvents = mouse.sourceEvents;
|
|
|
|
if (mouseEvents.mousemove) {
|
|
Events.trigger(engine, 'mousemove', {
|
|
mouse: mouse
|
|
});
|
|
}
|
|
|
|
if (mouseEvents.mousedown) {
|
|
Events.trigger(engine, 'mousedown', {
|
|
mouse: mouse
|
|
});
|
|
}
|
|
|
|
if (mouseEvents.mouseup) {
|
|
Events.trigger(engine, 'mouseup', {
|
|
mouse: mouse
|
|
});
|
|
}
|
|
|
|
// reset the mouse state ready for the next step
|
|
Mouse.clearSourceEvents(mouse);
|
|
};
|
|
|
|
/**
|
|
* Triggers collision events
|
|
* @method _triggerMouseEvents
|
|
* @private
|
|
* @param {engine} engine
|
|
*/
|
|
var _triggerCollisionEvents = function(engine) {
|
|
var pairs = engine.pairs;
|
|
|
|
if (pairs.collisionStart.length > 0) {
|
|
Events.trigger(engine, 'collisionStart', {
|
|
pairs: pairs.collisionStart
|
|
});
|
|
}
|
|
|
|
if (pairs.collisionActive.length > 0) {
|
|
Events.trigger(engine, 'collisionActive', {
|
|
pairs: pairs.collisionActive
|
|
});
|
|
}
|
|
|
|
if (pairs.collisionEnd.length > 0) {
|
|
Events.trigger(engine, 'collisionEnd', {
|
|
pairs: pairs.collisionEnd
|
|
});
|
|
}
|
|
};
|
|
|
|
/*
|
|
*
|
|
* Events Documentation
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Fired at the start of a tick, before any updates to the engine or timing
|
|
*
|
|
* @event beforeTick
|
|
* @param {} event An event object
|
|
* @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired after engine timing updated, but just before engine state updated
|
|
*
|
|
* @event tick
|
|
* @param {} event An event object
|
|
* @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired just before an update
|
|
*
|
|
* @event beforeUpdate
|
|
* @param {} event An event object
|
|
* @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired after engine update and all collision events
|
|
*
|
|
* @event afterUpdate
|
|
* @param {} event An event object
|
|
* @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired just before rendering
|
|
*
|
|
* @event beforeRender
|
|
* @param {} event An event object
|
|
* @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired after rendering
|
|
*
|
|
* @event afterRender
|
|
* @param {} event An event object
|
|
* @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired after engine update and after rendering
|
|
*
|
|
* @event afterTick
|
|
* @param {} event An event object
|
|
* @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired when the mouse has moved (or a touch moves) during the last step
|
|
*
|
|
* @event mousemove
|
|
* @param {} event An event object
|
|
* @param {mouse} event.mouse The engine's mouse instance
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired when the mouse is down (or a touch has started) during the last step
|
|
*
|
|
* @event mousedown
|
|
* @param {} event An event object
|
|
* @param {mouse} event.mouse The engine's mouse instance
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired when the mouse is up (or a touch has ended) during the last step
|
|
*
|
|
* @event mouseup
|
|
* @param {} event An event object
|
|
* @param {mouse} event.mouse The engine's mouse instance
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired after engine update, provides a list of all pairs that have started to collide in the current tick (if any)
|
|
*
|
|
* @event collisionStart
|
|
* @param {} event An event object
|
|
* @param {} event.pairs List of affected pairs
|
|
* @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired after engine update, provides a list of all pairs that are colliding in the current tick (if any)
|
|
*
|
|
* @event collisionActive
|
|
* @param {} event An event object
|
|
* @param {} event.pairs List of affected pairs
|
|
* @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
/**
|
|
* Fired after engine update, provides a list of all pairs that have ended collision in the current tick (if any)
|
|
*
|
|
* @event collisionEnd
|
|
* @param {} event An event object
|
|
* @param {} event.pairs List of affected pairs
|
|
* @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick
|
|
* @param {} event.source The source object of the event
|
|
* @param {} event.name The name of the event
|
|
*/
|
|
|
|
})();
|
|
|
|
; // End src/core/Engine.js
|
|
|
|
|
|
// Begin src/core/Events.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 Events
|
|
*/
|
|
|
|
var Events = {};
|
|
|
|
(function() {
|
|
|
|
/**
|
|
* Subscribes a callback function to the given object's eventName
|
|
* @method on
|
|
* @param {} object
|
|
* @param {string} eventNames
|
|
* @param {function} callback
|
|
*/
|
|
Events.on = function(object, eventNames, callback) {
|
|
var names = eventNames.split(' '),
|
|
name;
|
|
|
|
for (var i = 0; i < names.length; i++) {
|
|
name = names[i];
|
|
object.events = object.events || {};
|
|
object.events[name] = object.events[name] || [];
|
|
object.events[name].push(callback);
|
|
}
|
|
|
|
return callback;
|
|
};
|
|
|
|
/**
|
|
* Removes the given event callback. If no callback, clears all callbacks in eventNames. If no eventNames, clears all events.
|
|
* @method off
|
|
* @param {} object
|
|
* @param {string} eventNames
|
|
* @param {function} callback
|
|
*/
|
|
Events.off = function(object, eventNames, callback) {
|
|
if (!eventNames) {
|
|
object.events = {};
|
|
return;
|
|
}
|
|
|
|
// handle Events.off(object, callback)
|
|
if (typeof eventNames === 'function') {
|
|
callback = eventNames;
|
|
eventNames = Common.keys(object.events).join(' ');
|
|
}
|
|
|
|
var names = eventNames.split(' ');
|
|
|
|
for (var i = 0; i < names.length; i++) {
|
|
var callbacks = object.events[names[i]],
|
|
newCallbacks = [];
|
|
|
|
if (callback) {
|
|
for (var j = 0; j < callbacks.length; j++) {
|
|
if (callbacks[j] !== callback)
|
|
newCallbacks.push(callbacks[j]);
|
|
}
|
|
}
|
|
|
|
object.events[names[i]] = newCallbacks;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Fires all the callbacks subscribed to the given object's eventName, in the order they subscribed, if any
|
|
* @method trigger
|
|
* @param {} object
|
|
* @param {string} eventNames
|
|
* @param {} event
|
|
*/
|
|
Events.trigger = function(object, eventNames, event) {
|
|
var names,
|
|
name,
|
|
callbacks,
|
|
eventClone;
|
|
|
|
if (object.events) {
|
|
if (!event)
|
|
event = {};
|
|
|
|
names = eventNames.split(' ');
|
|
|
|
for (var i = 0; i < names.length; i++) {
|
|
name = names[i];
|
|
callbacks = object.events[name];
|
|
|
|
if (callbacks) {
|
|
eventClone = Common.clone(event, false);
|
|
eventClone.name = name;
|
|
eventClone.source = object;
|
|
|
|
for (var j = 0; j < callbacks.length; j++) {
|
|
callbacks[j].apply(object, [eventClone]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
})();
|
|
|
|
; // End src/core/Events.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 {
|
|
extended: false,
|
|
narrowDetections: 0,
|
|
narrowphaseTests: 0,
|
|
narrowReuse: 0,
|
|
narrowReuseCount: 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) {
|
|
if (metrics.extended) {
|
|
metrics.narrowDetections = 0;
|
|
metrics.narrowphaseTests = 0;
|
|
metrics.narrowReuse = 0;
|
|
metrics.narrowReuseCount = 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) {
|
|
if (metrics.extended) {
|
|
var world = engine.world,
|
|
broadphase = engine.broadphase[engine.broadphase.current],
|
|
bodies = Composite.allBodies(world);
|
|
|
|
metrics.collisions = metrics.narrowDetections;
|
|
metrics.pairs = engine.pairs.list.length;
|
|
metrics.bodies = 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 / (bodies.length || 1))).toFixed(2);
|
|
metrics.narrowReuse = (metrics.narrowReuseCount / (metrics.narrowphaseTests || 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;
|
|
|
|
this.element = element || document.body;
|
|
this.absolute = { x: 0, y: 0 };
|
|
this.position = { x: 0, y: 0 };
|
|
this.mousedownPosition = { x: 0, y: 0 };
|
|
this.mouseupPosition = { x: 0, y: 0 };
|
|
this.offset = { x: 0, y: 0 };
|
|
this.scale = { x: 1, y: 1 };
|
|
this.wheelDelta = 0;
|
|
this.button = -1;
|
|
|
|
this.sourceEvents = {
|
|
mousemove: null,
|
|
mousedown: null,
|
|
mouseup: null,
|
|
mousewheel: null
|
|
};
|
|
|
|
this.mousemove = function(event) {
|
|
var position = _getRelativeMousePosition(event, mouse.element),
|
|
touches = event.changedTouches;
|
|
|
|
if (touches) {
|
|
mouse.button = 0;
|
|
event.preventDefault();
|
|
}
|
|
|
|
mouse.absolute.x = position.x;
|
|
mouse.absolute.y = position.y;
|
|
mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x;
|
|
mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y;
|
|
mouse.sourceEvents.mousemove = event;
|
|
};
|
|
|
|
this.mousedown = function(event) {
|
|
var position = _getRelativeMousePosition(event, mouse.element),
|
|
touches = event.changedTouches;
|
|
|
|
if (touches) {
|
|
mouse.button = 0;
|
|
event.preventDefault();
|
|
} else {
|
|
mouse.button = event.button;
|
|
}
|
|
|
|
mouse.absolute.x = position.x;
|
|
mouse.absolute.y = position.y;
|
|
mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x;
|
|
mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y;
|
|
mouse.mousedownPosition.x = mouse.position.x;
|
|
mouse.mousedownPosition.y = mouse.position.y;
|
|
mouse.sourceEvents.mousedown = event;
|
|
};
|
|
|
|
this.mouseup = function(event) {
|
|
var position = _getRelativeMousePosition(event, mouse.element),
|
|
touches = event.changedTouches;
|
|
|
|
if (touches) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
mouse.button = -1;
|
|
mouse.absolute.x = position.x;
|
|
mouse.absolute.y = position.y;
|
|
mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x;
|
|
mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y;
|
|
mouse.mouseupPosition.x = mouse.position.x;
|
|
mouse.mouseupPosition.y = mouse.position.y;
|
|
mouse.sourceEvents.mouseup = event;
|
|
};
|
|
|
|
this.mousewheel = function(event) {
|
|
mouse.wheelDelta = Math.max(-1, Math.min(1, event.wheelDelta || -event.detail));
|
|
event.preventDefault();
|
|
};
|
|
|
|
Mouse.setElement(mouse, mouse.element);
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method create
|
|
* @param {HTMLElement} element
|
|
* @return {mouse} A new mouse
|
|
*/
|
|
Mouse.create = function(element) {
|
|
return new Mouse(element);
|
|
};
|
|
|
|
/**
|
|
* Sets the element the mouse is bound to (and relative to)
|
|
* @method setElement
|
|
* @param {mouse} mouse
|
|
* @param {HTMLElement} element
|
|
*/
|
|
Mouse.setElement = function(mouse, element) {
|
|
mouse.element = element;
|
|
|
|
element.addEventListener('mousemove', mouse.mousemove);
|
|
element.addEventListener('mousedown', mouse.mousedown);
|
|
element.addEventListener('mouseup', mouse.mouseup);
|
|
|
|
element.addEventListener("mousewheel", mouse.mousewheel);
|
|
element.addEventListener("DOMMouseScroll", mouse.mousewheel);
|
|
|
|
element.addEventListener('touchmove', mouse.mousemove);
|
|
element.addEventListener('touchstart', mouse.mousedown);
|
|
element.addEventListener('touchend', mouse.mouseup);
|
|
};
|
|
|
|
/**
|
|
* Clears all captured source events
|
|
* @method clearSourceEvents
|
|
* @param {mouse} mouse
|
|
*/
|
|
Mouse.clearSourceEvents = function(mouse) {
|
|
mouse.sourceEvents.mousemove = null;
|
|
mouse.sourceEvents.mousedown = null;
|
|
mouse.sourceEvents.mouseup = null;
|
|
mouse.sourceEvents.mousewheel = null;
|
|
mouse.wheelDelta = 0;
|
|
};
|
|
|
|
/**
|
|
* Sets the offset
|
|
* @method setOffset
|
|
* @param {mouse} mouse
|
|
*/
|
|
Mouse.setOffset = function(mouse, offset) {
|
|
mouse.offset.x = offset.x;
|
|
mouse.offset.y = offset.y;
|
|
mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x;
|
|
mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y;
|
|
};
|
|
|
|
/**
|
|
* Sets the scale
|
|
* @method setScale
|
|
* @param {mouse} mouse
|
|
*/
|
|
Mouse.setScale = function(mouse, scale) {
|
|
mouse.scale.x = scale.x;
|
|
mouse.scale.y = scale.y;
|
|
mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x;
|
|
mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y;
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method _getRelativeMousePosition
|
|
* @private
|
|
* @param {} event
|
|
* @param {} element
|
|
* @return ObjectExpression
|
|
*/
|
|
var _getRelativeMousePosition = function(event, element) {
|
|
var elementBounds = element.getBoundingClientRect(),
|
|
rootNode = (document.documentElement || document.body.parentNode || document.body),
|
|
scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset : rootNode.scrollLeft,
|
|
scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset : rootNode.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;
|
|
|
|
// wake up bodies if they have a force applied
|
|
if (body.force.x > 0 || body.force.y > 0) {
|
|
Sleeping.set(body, false);
|
|
continue;
|
|
}
|
|
|
|
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];
|
|
|
|
// don't wake inactive pairs
|
|
if (!pair.isActive)
|
|
continue;
|
|
|
|
var collision = pair.collision,
|
|
bodyA = collision.bodyA,
|
|
bodyB = collision.bodyB;
|
|
|
|
// don't wake if at least one body is static
|
|
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 = {
|
|
label: 'Rectangle Body',
|
|
position: { x: x, y: y },
|
|
vertices: Vertices.fromPath('L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height)
|
|
};
|
|
|
|
if (options.chamfer) {
|
|
var chamfer = options.chamfer;
|
|
rectangle.vertices = Vertices.chamfer(rectangle.vertices, chamfer.radius,
|
|
chamfer.quality, chamfer.qualityMin, chamfer.qualityMax);
|
|
delete options.chamfer;
|
|
}
|
|
|
|
return Body.create(Common.extend({}, rectangle, options));
|
|
};
|
|
|
|
/**
|
|
* 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 = {
|
|
label: 'Trapezoid Body',
|
|
position: { x: x, y: y },
|
|
vertices: Vertices.fromPath('L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0')
|
|
};
|
|
|
|
if (options.chamfer) {
|
|
var chamfer = options.chamfer;
|
|
trapezoid.vertices = Vertices.chamfer(trapezoid.vertices, chamfer.radius,
|
|
chamfer.quality, chamfer.qualityMin, chamfer.qualityMax);
|
|
delete options.chamfer;
|
|
}
|
|
|
|
return Body.create(Common.extend({}, trapezoid, options));
|
|
};
|
|
|
|
/**
|
|
* 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 || {};
|
|
options.label = 'Circle Body';
|
|
|
|
// 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 = {
|
|
label: 'Polygon Body',
|
|
position: { x: x, y: y },
|
|
vertices: Vertices.fromPath(path)
|
|
};
|
|
|
|
if (options.chamfer) {
|
|
var chamfer = options.chamfer;
|
|
polygon.vertices = Vertices.chamfer(polygon.vertices, chamfer.radius,
|
|
chamfer.quality, chamfer.qualityMin, chamfer.qualityMax);
|
|
delete options.chamfer;
|
|
}
|
|
|
|
return Body.create(Common.extend({}, polygon, options));
|
|
};
|
|
|
|
})();
|
|
|
|
; // 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({ label: 'Stack' }),
|
|
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));
|
|
}
|
|
|
|
composite.label += ' Chain';
|
|
|
|
return composite;
|
|
};
|
|
|
|
/**
|
|
* Connects bodies in the composite with constraints in a grid pattern, with optional cross braces
|
|
* @method mesh
|
|
* @param {composite} composite
|
|
* @param {number} columns
|
|
* @param {number} rows
|
|
* @param {boolean} crossBrace
|
|
* @param {object} options
|
|
* @return {composite} The composite containing objects meshed together with constraints
|
|
*/
|
|
Composites.mesh = function(composite, columns, rows, crossBrace, options) {
|
|
var bodies = composite.bodies,
|
|
row,
|
|
col,
|
|
bodyA,
|
|
bodyB,
|
|
bodyC;
|
|
|
|
for (row = 0; row < rows; row++) {
|
|
for (col = 0; col < columns; col++) {
|
|
if (col > 0) {
|
|
bodyA = bodies[(col - 1) + (row * columns)];
|
|
bodyB = bodies[col + (row * columns)];
|
|
Composite.addConstraint(composite, Constraint.create(Common.extend({ bodyA: bodyA, bodyB: bodyB }, options)));
|
|
}
|
|
}
|
|
|
|
for (col = 0; col < columns; col++) {
|
|
if (row > 0) {
|
|
bodyA = bodies[col + ((row - 1) * columns)];
|
|
bodyB = bodies[col + (row * columns)];
|
|
Composite.addConstraint(composite, Constraint.create(Common.extend({ bodyA: bodyA, bodyB: bodyB }, options)));
|
|
|
|
if (crossBrace && col > 0) {
|
|
bodyC = bodies[(col - 1) + ((row - 1) * columns)];
|
|
Composite.addConstraint(composite, Constraint.create(Common.extend({ bodyA: bodyC, bodyB: bodyB }, options)));
|
|
}
|
|
|
|
if (crossBrace && col < columns - 1) {
|
|
bodyC = bodies[(col + 1) + ((row - 1) * columns)];
|
|
Composite.addConstraint(composite, Constraint.create(Common.extend({ bodyA: bodyC, bodyB: bodyB }, options)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
composite.label += ' Mesh';
|
|
|
|
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({ label: 'Newtons Cradle' });
|
|
|
|
for (var i = 0; i < number; i++) {
|
|
var separation = 1.9,
|
|
circle = Bodies.circle(xx + i * (size * separation), yy + length, size,
|
|
{ inertia: 99999, 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({ label: 'Car' }),
|
|
body = Bodies.trapezoid(xx, yy, width, height, 0.3, {
|
|
groupId: groupId,
|
|
friction: 0.01,
|
|
chamfer: {
|
|
radius: 10
|
|
}
|
|
});
|
|
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* Creates a simple soft body like object
|
|
* @method softBody
|
|
* @param {number} xx
|
|
* @param {number} yy
|
|
* @param {number} columns
|
|
* @param {number} rows
|
|
* @param {number} columnGap
|
|
* @param {number} rowGap
|
|
* @param {boolean} crossBrace
|
|
* @param {number} particleRadius
|
|
* @param {} particleOptions
|
|
* @param {} constraintOptions
|
|
* @return {composite} A new composite softBody
|
|
*/
|
|
Composites.softBody = function(xx, yy, columns, rows, columnGap, rowGap, crossBrace, particleRadius, particleOptions, constraintOptions) {
|
|
particleOptions = Common.extend({ inertia: Infinity }, particleOptions);
|
|
constraintOptions = Common.extend({ stiffness: 0.4 }, constraintOptions);
|
|
|
|
var softBody = Composites.stack(xx, yy, columns, rows, columnGap, rowGap, function(x, y, column, row) {
|
|
return Bodies.circle(x, y, particleRadius, particleOptions);
|
|
});
|
|
|
|
Composites.mesh(softBody, columns, rows, crossBrace, constraintOptions);
|
|
|
|
softBody.label = 'Soft Body';
|
|
|
|
return softBody;
|
|
};
|
|
|
|
})();
|
|
|
|
; // 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 }
|
|
};
|
|
|
|
if (vertices)
|
|
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);
|
|
};
|
|
|
|
/**
|
|
* Translates the bounds by the given vector
|
|
* @method translate
|
|
* @param {bounds} bounds
|
|
* @param {vector} vector
|
|
*/
|
|
Bounds.translate = function(bounds, vector) {
|
|
bounds.min.x += vector.x;
|
|
bounds.max.x += vector.x;
|
|
bounds.min.y += vector.y;
|
|
bounds.max.y += vector.y;
|
|
};
|
|
|
|
/**
|
|
* Shifts the bounds to the given position
|
|
* @method shift
|
|
* @param {bounds} bounds
|
|
* @param {vector} position
|
|
*/
|
|
Bounds.shift = function(bounds, position) {
|
|
var deltaX = bounds.max.x - bounds.min.x,
|
|
deltaY = bounds.max.y - bounds.min.y;
|
|
|
|
bounds.min.x = position.x;
|
|
bounds.max.x = position.x + deltaX;
|
|
bounds.min.y = position.y;
|
|
bounds.max.y = position.y + deltaY;
|
|
};
|
|
|
|
})();
|
|
|
|
; // 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 };
|
|
};
|
|
|
|
/**
|
|
* Returns the angle in radians between the two vectors relative to the x-axis
|
|
* @method angle
|
|
* @param {vector} vectorA
|
|
* @param {vector} vectorB
|
|
* @return {number} The angle in radians
|
|
*/
|
|
Vector.angle = function(vectorA, vectorB) {
|
|
return Math.atan2(vectorB.y - vectorA.y, vectorB.x - vectorA.x);
|
|
};
|
|
|
|
})();
|
|
|
|
; // 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 area = Vertices.area(vertices, true),
|
|
centre = { x: 0, y: 0 },
|
|
cross,
|
|
temp,
|
|
j;
|
|
|
|
for (var i = 0; i < vertices.length; i++) {
|
|
j = (i + 1) % vertices.length;
|
|
cross = Vector.cross(vertices[i], vertices[j]);
|
|
temp = Vector.mult(Vector.add(vertices[i], vertices[j]), cross);
|
|
centre = Vector.add(centre, temp);
|
|
}
|
|
|
|
return Vector.div(centre, 6 * area);
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method area
|
|
* @param {vertices} vertices
|
|
* @param {bool} signed
|
|
* @return {number} The area
|
|
*/
|
|
Vertices.area = function(vertices, signed) {
|
|
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;
|
|
}
|
|
|
|
if (signed)
|
|
return area / 2;
|
|
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* Scales the vertices from a point (default is centre)
|
|
* @method scale
|
|
* @param {vertices} vertices
|
|
* @param {number} scaleX
|
|
* @param {number} scaleY
|
|
* @param {vector} point
|
|
*/
|
|
Vertices.scale = function(vertices, scaleX, scaleY, point) {
|
|
if (scaleX === 1 && scaleY === 1)
|
|
return vertices;
|
|
|
|
point = point || Vertices.centre(vertices);
|
|
|
|
var vertex,
|
|
delta;
|
|
|
|
for (var i = 0; i < vertices.length; i++) {
|
|
vertex = vertices[i];
|
|
delta = Vector.sub(vertex, point);
|
|
vertices[i].x = point.x + delta.x * scaleX;
|
|
vertices[i].y = point.y + delta.y * scaleY;
|
|
}
|
|
|
|
return vertices;
|
|
};
|
|
|
|
/**
|
|
* Chamfers a set of vertices by giving them rounded corners, returns a new set of vertices.
|
|
* The radius parameter is a single number or an array to specify the radius for each vertex.
|
|
* @method chamfer
|
|
* @param {vertices} vertices
|
|
* @param {number[]} radius
|
|
* @param {number} quality
|
|
* @param {number} qualityMin
|
|
* @param {number} qualityMax
|
|
*/
|
|
Vertices.chamfer = function(vertices, radius, quality, qualityMin, qualityMax) {
|
|
radius = radius || [8];
|
|
|
|
if (!radius.length)
|
|
radius = [radius];
|
|
|
|
// quality defaults to -1, which is auto
|
|
quality = (typeof quality !== 'undefined') ? quality : -1;
|
|
qualityMin = qualityMin || 2;
|
|
qualityMax = qualityMax || 14;
|
|
|
|
var centre = Vertices.centre(vertices),
|
|
newVertices = [];
|
|
|
|
for (var i = 0; i < vertices.length; i++) {
|
|
var prevVertex = vertices[i - 1 >= 0 ? i - 1 : vertices.length - 1],
|
|
vertex = vertices[i],
|
|
nextVertex = vertices[(i + 1) % vertices.length],
|
|
currentRadius = radius[i < radius.length ? i : radius.length - 1];
|
|
|
|
if (currentRadius === 0) {
|
|
newVertices.push(vertex);
|
|
continue;
|
|
}
|
|
|
|
var prevNormal = Vector.normalise({
|
|
x: vertex.y - prevVertex.y,
|
|
y: prevVertex.x - vertex.x
|
|
});
|
|
|
|
var nextNormal = Vector.normalise({
|
|
x: nextVertex.y - vertex.y,
|
|
y: vertex.x - nextVertex.x
|
|
});
|
|
|
|
var diagonalRadius = Math.sqrt(2 * Math.pow(currentRadius, 2)),
|
|
radiusVector = Vector.mult(Common.clone(prevNormal), currentRadius),
|
|
midNormal = Vector.normalise(Vector.mult(Vector.add(prevNormal, nextNormal), 0.5)),
|
|
scaledVertex = Vector.sub(vertex, Vector.mult(midNormal, diagonalRadius));
|
|
|
|
var precision = quality;
|
|
|
|
if (quality === -1) {
|
|
// automatically decide precision
|
|
precision = Math.pow(currentRadius, 0.32) * 1.75;
|
|
}
|
|
|
|
precision = Common.clamp(precision, qualityMin, qualityMax);
|
|
|
|
// use an even value for precision, more likely to reduce axes by using symmetry
|
|
if (precision % 2 === 1)
|
|
precision += 1;
|
|
|
|
var alpha = Math.acos(Vector.dot(prevNormal, nextNormal)),
|
|
theta = alpha / precision;
|
|
|
|
for (var j = 0; j < precision; j++) {
|
|
newVertices.push(Vector.add(Vector.rotate(radiusVector, theta * j), scaledVertex));
|
|
}
|
|
}
|
|
|
|
return newVertices;
|
|
};
|
|
|
|
})();
|
|
|
|
; // End src/geometry/Vertices.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,
|
|
element: null,
|
|
canvas: null,
|
|
options: {
|
|
width: 800,
|
|
height: 600,
|
|
background: '#fafafa',
|
|
wireframeBackground: '#222',
|
|
hasBounds: false,
|
|
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');
|
|
render.textures = {};
|
|
|
|
render.bounds = render.bounds || {
|
|
min: {
|
|
x: 0,
|
|
y: 0
|
|
},
|
|
max: {
|
|
x: render.options.width,
|
|
y: render.options.height
|
|
}
|
|
};
|
|
|
|
Render.setBackground(render, render.options.background);
|
|
|
|
if (Common.isElement(render.element)) {
|
|
render.element.appendChild(render.canvas);
|
|
} else {
|
|
Common.log('No "render.element" passed, "render.canvas" was not inserted into document.', 'warn');
|
|
}
|
|
|
|
return render;
|
|
};
|
|
|
|
/**
|
|
* Clears the renderer. In this implementation, this is a noop.
|
|
* @method clear
|
|
* @param {RenderPixi} render
|
|
*/
|
|
Render.clear = function(render) {
|
|
// nothing required to clear this renderer implentation
|
|
// if a scene graph is required, clear it here (see RenderPixi.js)
|
|
};
|
|
|
|
/**
|
|
* Sets the background CSS property of the canvas
|
|
* @method setBackground
|
|
* @param {render} render
|
|
* @param {string} background
|
|
*/
|
|
Render.setBackground = function(render, background) {
|
|
if (render.currentBackground !== background) {
|
|
var cssBackground = background;
|
|
|
|
if (/(jpg|gif|png)$/.test(background))
|
|
cssBackground = 'url(' + background + ')';
|
|
|
|
render.canvas.style.background = cssBackground;
|
|
render.canvas.style.backgroundSize = "contain";
|
|
render.currentBackground = background;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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,
|
|
allBodies = Composite.allBodies(world),
|
|
allConstraints = Composite.allConstraints(world),
|
|
bodies = [],
|
|
constraints = [],
|
|
i;
|
|
|
|
if (options.wireframes) {
|
|
Render.setBackground(render, options.wireframeBackground);
|
|
} else {
|
|
Render.setBackground(render, options.background);
|
|
}
|
|
|
|
// clear the canvas with a transparent fill, to allow the canvas background to show
|
|
context.globalCompositeOperation = 'source-in';
|
|
context.fillStyle = "transparent";
|
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
context.globalCompositeOperation = 'source-over';
|
|
|
|
// handle bounds
|
|
var boundsWidth = render.bounds.max.x - render.bounds.min.x,
|
|
boundsHeight = render.bounds.max.y - render.bounds.min.y,
|
|
boundsScaleX = boundsWidth / render.options.width,
|
|
boundsScaleY = boundsHeight / render.options.height;
|
|
|
|
if (options.hasBounds) {
|
|
// filter out bodies that are not in view
|
|
for (i = 0; i < allBodies.length; i++) {
|
|
var body = allBodies[i];
|
|
if (Bounds.overlaps(body.bounds, render.bounds))
|
|
bodies.push(body);
|
|
}
|
|
|
|
// filter out constraints that are not in view
|
|
for (i = 0; i < allConstraints.length; i++) {
|
|
var constraint = allConstraints[i],
|
|
bodyA = constraint.bodyA,
|
|
bodyB = constraint.bodyB,
|
|
pointAWorld = constraint.pointA,
|
|
pointBWorld = constraint.pointB;
|
|
|
|
if (bodyA) pointAWorld = Vector.add(bodyA.position, constraint.pointA);
|
|
if (bodyB) pointBWorld = Vector.add(bodyB.position, constraint.pointB);
|
|
|
|
if (!pointAWorld || !pointBWorld)
|
|
continue;
|
|
|
|
if (Bounds.contains(render.bounds, pointAWorld) || Bounds.contains(render.bounds, pointBWorld))
|
|
constraints.push(constraint);
|
|
}
|
|
|
|
// transform the view
|
|
context.scale(1 / boundsScaleX, 1 / boundsScaleY);
|
|
context.translate(-render.bounds.min.x, -render.bounds.min.y);
|
|
} else {
|
|
constraints = allConstraints;
|
|
bodies = allBodies;
|
|
}
|
|
|
|
if (!options.wireframes || (engine.enableSleeping && options.showSleeping)) {
|
|
// fully featured rendering of bodies
|
|
Render.bodies(engine, bodies, context);
|
|
} else {
|
|
// optimised method for wireframes only
|
|
Render.bodyWireframes(engine, bodies, context);
|
|
}
|
|
|
|
if (options.showBounds)
|
|
Render.bodyBounds(engine, bodies, context);
|
|
|
|
if (options.showAxes || options.showAngleIndicator)
|
|
Render.bodyAxes(engine, bodies, context);
|
|
|
|
if (options.showPositions)
|
|
Render.bodyPositions(engine, bodies, context);
|
|
|
|
if (options.showVelocity)
|
|
Render.bodyVelocity(engine, bodies, context);
|
|
|
|
if (options.showIds)
|
|
Render.bodyIds(engine, bodies, context);
|
|
|
|
if (options.showCollisions)
|
|
Render.collisions(engine, engine.pairs.list, context);
|
|
|
|
Render.constraints(constraints, 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);
|
|
|
|
if (options.hasBounds) {
|
|
// revert view transforms
|
|
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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,
|
|
bodies = Composite.allBodies(world),
|
|
space = " ";
|
|
|
|
if (engine.timing.timestamp - (render.debugTimestamp || 0) >= 500) {
|
|
var text = "";
|
|
text += "fps: " + Math.round(engine.timing.fps) + space;
|
|
|
|
if (engine.metrics.extended) {
|
|
text += "delta: " + engine.timing.delta.toFixed(3) + space;
|
|
text += "correction: " + engine.timing.correction.toFixed(3) + space;
|
|
text += "bodies: " + 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.list.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 constraints
|
|
* @param {constraint[]} constraints
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.constraints = function(constraints, context) {
|
|
var c = context;
|
|
|
|
for (var i = 0; i < constraints.length; i++) {
|
|
var constraint = constraints[i];
|
|
|
|
if (!constraint.render.visible || !constraint.pointA || !constraint.pointB)
|
|
continue;
|
|
|
|
var bodyA = constraint.bodyA,
|
|
bodyB = constraint.bodyB;
|
|
|
|
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.render.lineWidth;
|
|
c.strokeStyle = constraint.render.strokeStyle;
|
|
c.stroke();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method bodyShadows
|
|
* @param {engine} engine
|
|
* @param {body[]} bodies
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.bodyShadows = function(engine, bodies, context) {
|
|
var c = context,
|
|
render = engine.render,
|
|
options = render.options;
|
|
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i];
|
|
|
|
if (!body.render.visible)
|
|
continue;
|
|
|
|
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 bodies
|
|
* @param {engine} engine
|
|
* @param {body[]} bodies
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.bodies = function(engine, bodies, context) {
|
|
var c = context,
|
|
render = engine.render,
|
|
options = render.options,
|
|
i;
|
|
|
|
for (i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i];
|
|
|
|
if (!body.render.visible)
|
|
continue;
|
|
|
|
if (body.render.sprite && body.render.sprite.texture && !options.wireframes) {
|
|
// body sprite
|
|
var sprite = body.render.sprite,
|
|
texture = _getTexture(render, sprite.texture);
|
|
|
|
if (options.showSleeping && body.isSleeping)
|
|
c.globalAlpha = 0.5;
|
|
|
|
c.translate(body.position.x, body.position.y);
|
|
c.rotate(body.angle);
|
|
|
|
c.drawImage(texture, texture.width * -0.5 * sprite.xScale, texture.height * -0.5 * sprite.yScale,
|
|
texture.width * sprite.xScale, texture.height * sprite.yScale);
|
|
|
|
// revert translation, hopefully faster than save / restore
|
|
c.rotate(-body.angle);
|
|
c.translate(-body.position.x, -body.position.y);
|
|
|
|
if (options.showSleeping && body.isSleeping)
|
|
c.globalAlpha = 1;
|
|
} else {
|
|
// body polygon
|
|
if (body.circleRadius) {
|
|
c.beginPath();
|
|
c.arc(body.position.x, body.position.y, body.circleRadius, 0, 2 * Math.PI);
|
|
} 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) {
|
|
if (options.showSleeping && body.isSleeping) {
|
|
c.fillStyle = Common.shadeColor(body.render.fillStyle, 50);
|
|
} else {
|
|
c.fillStyle = body.render.fillStyle;
|
|
}
|
|
|
|
c.lineWidth = body.render.lineWidth;
|
|
c.strokeStyle = body.render.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();
|
|
}
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Optimised method for drawing body wireframes in one pass
|
|
* @method bodyWireframes
|
|
* @param {engine} engine
|
|
* @param {body[]} bodies
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.bodyWireframes = function(engine, bodies, context) {
|
|
var c = context,
|
|
i,
|
|
j;
|
|
|
|
c.beginPath();
|
|
|
|
for (i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i];
|
|
|
|
if (!body.render.visible)
|
|
continue;
|
|
|
|
c.moveTo(body.vertices[0].x, body.vertices[0].y);
|
|
|
|
for (j = 1; j < body.vertices.length; j++) {
|
|
c.lineTo(body.vertices[j].x, body.vertices[j].y);
|
|
}
|
|
|
|
c.lineTo(body.vertices[0].x, body.vertices[0].y);
|
|
}
|
|
|
|
c.lineWidth = 1;
|
|
c.strokeStyle = '#bbb';
|
|
c.stroke();
|
|
};
|
|
|
|
/**
|
|
* Draws body bounds
|
|
* @method bodyBounds
|
|
* @param {engine} engine
|
|
* @param {body[]} bodies
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.bodyBounds = function(engine, bodies, context) {
|
|
var c = context,
|
|
render = engine.render,
|
|
options = render.options;
|
|
|
|
c.beginPath();
|
|
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i];
|
|
|
|
if (body.render.visible)
|
|
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);
|
|
}
|
|
|
|
if (options.wireframes) {
|
|
c.strokeStyle = 'rgba(255,255,255,0.08)';
|
|
} else {
|
|
c.strokeStyle = 'rgba(0,0,0,0.1)';
|
|
}
|
|
|
|
c.lineWidth = 1;
|
|
c.stroke();
|
|
};
|
|
|
|
/**
|
|
* Draws body angle indicators and axes
|
|
* @method bodyAxes
|
|
* @param {engine} engine
|
|
* @param {body[]} bodies
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.bodyAxes = function(engine, bodies, context) {
|
|
var c = context,
|
|
render = engine.render,
|
|
options = render.options,
|
|
i,
|
|
j;
|
|
|
|
c.beginPath();
|
|
|
|
for (i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i];
|
|
|
|
if (!body.render.visible)
|
|
continue;
|
|
|
|
if (options.showAxes) {
|
|
// render all axes
|
|
for (j = 0; j < body.axes.length; j++) {
|
|
var axis = body.axes[j];
|
|
c.moveTo(body.position.x, body.position.y);
|
|
c.lineTo(body.position.x + axis.x * 20, body.position.y + axis.y * 20);
|
|
}
|
|
} else {
|
|
// render a single axis indicator
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (options.wireframes) {
|
|
c.strokeStyle = 'indianred';
|
|
} else {
|
|
c.strokeStyle = 'rgba(0,0,0,0.3)';
|
|
}
|
|
|
|
c.lineWidth = 1;
|
|
c.stroke();
|
|
};
|
|
|
|
/**
|
|
* Draws body positions
|
|
* @method bodyPositions
|
|
* @param {engine} engine
|
|
* @param {body[]} bodies
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.bodyPositions = function(engine, bodies, context) {
|
|
var c = context,
|
|
render = engine.render,
|
|
options = render.options,
|
|
body,
|
|
i;
|
|
|
|
c.beginPath();
|
|
|
|
// render current positions
|
|
for (i = 0; i < bodies.length; i++) {
|
|
body = bodies[i];
|
|
if (body.render.visible) {
|
|
c.arc(body.position.x, body.position.y, 3, 0, 2 * Math.PI, false);
|
|
c.closePath();
|
|
}
|
|
}
|
|
|
|
if (options.wireframes) {
|
|
c.fillStyle = 'indianred';
|
|
} else {
|
|
c.fillStyle = 'rgba(0,0,0,0.5)';
|
|
}
|
|
c.fill();
|
|
|
|
c.beginPath();
|
|
|
|
// render previous positions
|
|
for (i = 0; i < bodies.length; i++) {
|
|
body = bodies[i];
|
|
if (body.render.visible) {
|
|
c.arc(body.positionPrev.x, body.positionPrev.y, 2, 0, 2 * Math.PI, false);
|
|
c.closePath();
|
|
}
|
|
}
|
|
|
|
c.fillStyle = 'rgba(255,165,0,0.8)';
|
|
c.fill();
|
|
};
|
|
|
|
/**
|
|
* Draws body velocity
|
|
* @method bodyVelocity
|
|
* @param {engine} engine
|
|
* @param {body[]} bodies
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.bodyVelocity = function(engine, bodies, context) {
|
|
var c = context,
|
|
render = engine.render,
|
|
options = render.options;
|
|
|
|
c.beginPath();
|
|
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i];
|
|
|
|
if (!body.render.visible)
|
|
continue;
|
|
|
|
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();
|
|
};
|
|
|
|
/**
|
|
* Draws body ids
|
|
* @method bodyIds
|
|
* @param {engine} engine
|
|
* @param {body[]} bodies
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.bodyIds = function(engine, bodies, context) {
|
|
var c = context;
|
|
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i];
|
|
|
|
if (!body.render.visible)
|
|
continue;
|
|
|
|
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 collisions
|
|
* @param {engine} engine
|
|
* @param {pair[]} pairs
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.collisions = function(engine, pairs, context) {
|
|
var c = context,
|
|
options = engine.render.options,
|
|
pair,
|
|
collision,
|
|
i,
|
|
j;
|
|
|
|
c.beginPath();
|
|
|
|
// render collision positions
|
|
for (i = 0; i < pairs.length; i++) {
|
|
pair = pairs[i];
|
|
collision = pair.collision;
|
|
for (j = 0; j < pair.activeContacts.length; j++) {
|
|
var contact = pair.activeContacts[j],
|
|
vertex = contact.vertex;
|
|
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();
|
|
|
|
c.beginPath();
|
|
|
|
// render collision normals
|
|
for (i = 0; i < pairs.length; i++) {
|
|
pair = pairs[i];
|
|
collision = pair.collision;
|
|
|
|
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;
|
|
}
|
|
|
|
c.moveTo(normalPosX - collision.normal.x * 8, normalPosY - collision.normal.y * 8);
|
|
c.lineTo(normalPosX, normalPosY);
|
|
}
|
|
}
|
|
|
|
if (options.wireframes) {
|
|
c.strokeStyle = 'rgba(255,165,0,0.7)';
|
|
} else {
|
|
c.strokeStyle = 'orange';
|
|
}
|
|
|
|
c.lineWidth = 1;
|
|
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;
|
|
|
|
if (options.wireframes) {
|
|
c.strokeStyle = 'rgba(255,180,0,0.1)';
|
|
} else {
|
|
c.strokeStyle = 'rgba(255,180,0,0.5)';
|
|
}
|
|
|
|
c.beginPath();
|
|
|
|
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.rect(0.5 + parseInt(region[0], 10) * grid.bucketWidth,
|
|
0.5 + parseInt(region[1], 10) * grid.bucketHeight,
|
|
grid.bucketWidth,
|
|
grid.bucketHeight);
|
|
}
|
|
|
|
c.lineWidth = 1;
|
|
c.stroke();
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method inspector
|
|
* @param {inspector} inspector
|
|
* @param {RenderingContext} context
|
|
*/
|
|
Render.inspector = function(inspector, context) {
|
|
var engine = inspector.engine,
|
|
mouse = engine.input.mouse,
|
|
selected = inspector.selected,
|
|
c = context,
|
|
render = engine.render,
|
|
options = render.options,
|
|
bounds;
|
|
|
|
if (options.hasBounds) {
|
|
var boundsWidth = render.bounds.max.x - render.bounds.min.x,
|
|
boundsHeight = render.bounds.max.y - render.bounds.min.y,
|
|
boundsScaleX = boundsWidth / render.options.width,
|
|
boundsScaleY = boundsHeight / render.options.height;
|
|
|
|
context.scale(1 / boundsScaleX, 1 / boundsScaleY);
|
|
context.translate(-render.bounds.min.x, -render.bounds.min.y);
|
|
}
|
|
|
|
for (var i = 0; i < selected.length; i++) {
|
|
var item = selected[i].data;
|
|
|
|
context.translate(0.5, 0.5);
|
|
context.lineWidth = 1;
|
|
context.strokeStyle = 'rgba(255,165,0,0.9)';
|
|
context.setLineDash([1,2]);
|
|
|
|
switch (item.type) {
|
|
|
|
case 'body':
|
|
|
|
// render body selections
|
|
bounds = item.bounds;
|
|
context.beginPath();
|
|
context.rect(Math.floor(bounds.min.x - 3), Math.floor(bounds.min.y - 3),
|
|
Math.floor(bounds.max.x - bounds.min.x + 6), Math.floor(bounds.max.y - bounds.min.y + 6));
|
|
context.closePath();
|
|
context.stroke();
|
|
|
|
break;
|
|
|
|
case 'constraint':
|
|
|
|
// render constraint selections
|
|
var point = item.pointA;
|
|
if (item.bodyA)
|
|
point = item.pointB;
|
|
context.beginPath();
|
|
context.arc(point.x, point.y, 10, 0, 2 * Math.PI);
|
|
context.closePath();
|
|
context.stroke();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
context.setLineDash([0]);
|
|
context.translate(-0.5, -0.5);
|
|
}
|
|
|
|
// render selection region
|
|
if (inspector.selectStart !== null) {
|
|
context.translate(0.5, 0.5);
|
|
context.lineWidth = 1;
|
|
context.strokeStyle = 'rgba(255,165,0,0.6)';
|
|
context.fillStyle = 'rgba(255,165,0,0.1)';
|
|
bounds = inspector.selectBounds;
|
|
context.beginPath();
|
|
context.rect(Math.floor(bounds.min.x), Math.floor(bounds.min.y),
|
|
Math.floor(bounds.max.x - bounds.min.x), Math.floor(bounds.max.y - bounds.min.y));
|
|
context.closePath();
|
|
context.stroke();
|
|
context.fill();
|
|
context.translate(-0.5, -0.5);
|
|
}
|
|
|
|
if (options.hasBounds)
|
|
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
};
|
|
|
|
/**
|
|
* Gets the requested texture (an Image) via its path
|
|
* @method _getTexture
|
|
* @private
|
|
* @param {render} render
|
|
* @param {string} imagePath
|
|
* @return {Image} texture
|
|
*/
|
|
var _getTexture = function(render, imagePath) {
|
|
var image = render.textures[imagePath];
|
|
|
|
if (image)
|
|
return image;
|
|
|
|
image = render.textures[imagePath] = new Image();
|
|
image.src = imagePath;
|
|
|
|
return image;
|
|
};
|
|
|
|
})();
|
|
|
|
; // End src/render/Render.js
|
|
|
|
|
|
// Begin src/render/RenderPixi.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 RenderPixi
|
|
*/
|
|
|
|
var RenderPixi = {};
|
|
|
|
(function() {
|
|
|
|
/**
|
|
* Creates a new Pixi.js WebGL renderer
|
|
* @method create
|
|
* @param {object} options
|
|
* @return {RenderPixi} A new renderer
|
|
*/
|
|
RenderPixi.create = function(options) {
|
|
var defaults = {
|
|
controller: RenderPixi,
|
|
element: null,
|
|
canvas: null,
|
|
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);
|
|
|
|
// init pixi
|
|
render.context = new PIXI.WebGLRenderer(800, 600, render.canvas, false, true);
|
|
render.canvas = render.context.view;
|
|
render.stage = new PIXI.Stage();
|
|
|
|
// caches
|
|
render.textures = {};
|
|
render.sprites = {};
|
|
render.primitives = {};
|
|
|
|
// use a sprite batch for performance
|
|
render.spriteBatch = new PIXI.SpriteBatch();
|
|
render.stage.addChild(render.spriteBatch);
|
|
|
|
// insert canvas
|
|
if (Common.isElement(render.element)) {
|
|
render.element.appendChild(render.canvas);
|
|
} else {
|
|
Common.log('No "render.element" passed, "render.canvas" was not inserted into document.', 'warn');
|
|
}
|
|
|
|
// prevent menus on canvas
|
|
render.canvas.oncontextmenu = function() { return false; };
|
|
render.canvas.onselectstart = function() { return false; };
|
|
|
|
return render;
|
|
};
|
|
|
|
/**
|
|
* Clears the scene graph
|
|
* @method clear
|
|
* @param {RenderPixi} render
|
|
*/
|
|
RenderPixi.clear = function(render) {
|
|
var stage = render.stage,
|
|
spriteBatch = render.spriteBatch;
|
|
|
|
// clear stage
|
|
while (stage.children[0]) {
|
|
stage.removeChild(stage.children[0]);
|
|
}
|
|
|
|
// clear sprite batch
|
|
while (spriteBatch.children[0]) {
|
|
spriteBatch.removeChild(spriteBatch.children[0]);
|
|
}
|
|
|
|
var bgSprite = render.sprites['bg-0'];
|
|
|
|
// clear caches
|
|
render.textures = {};
|
|
render.sprites = {};
|
|
render.primitives = {};
|
|
|
|
// set background sprite
|
|
render.sprites['bg-0'] = bgSprite;
|
|
if (bgSprite)
|
|
spriteBatch.addChildAt(bgSprite, 0);
|
|
|
|
// add sprite batch back into stage
|
|
render.stage.addChild(render.spriteBatch);
|
|
|
|
// reset background state
|
|
render.currentBackground = null;
|
|
};
|
|
|
|
/**
|
|
* Sets the background of the canvas
|
|
* @method setBackground
|
|
* @param {RenderPixi} render
|
|
* @param {string} background
|
|
*/
|
|
RenderPixi.setBackground = function(render, background) {
|
|
if (render.currentBackground !== background) {
|
|
var isColor = background.indexOf && background.indexOf('#') !== -1,
|
|
bgSprite = render.sprites['bg-0'];
|
|
|
|
if (isColor) {
|
|
// if solid background color
|
|
var color = Common.colorToNumber(background);
|
|
render.stage.setBackgroundColor(color);
|
|
|
|
// remove background sprite if existing
|
|
if (bgSprite)
|
|
render.spriteBatch.removeChild(bgSprite);
|
|
} else {
|
|
// initialise background sprite if needed
|
|
if (!bgSprite) {
|
|
var texture = _getTexture(render, background);
|
|
|
|
bgSprite = render.sprites['bg-0'] = new PIXI.Sprite(texture);
|
|
bgSprite.position.x = 0;
|
|
bgSprite.position.y = 0;
|
|
render.spriteBatch.addChildAt(bgSprite, 0);
|
|
}
|
|
}
|
|
|
|
render.currentBackground = background;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method world
|
|
* @param {engine} engine
|
|
*/
|
|
RenderPixi.world = function(engine) {
|
|
var render = engine.render,
|
|
world = engine.world,
|
|
context = render.context,
|
|
stage = render.stage,
|
|
options = render.options,
|
|
bodies = Composite.allBodies(world),
|
|
constraints = Composite.allConstraints(world),
|
|
i;
|
|
|
|
if (options.wireframes) {
|
|
RenderPixi.setBackground(render, options.wireframeBackground);
|
|
} else {
|
|
RenderPixi.setBackground(render, options.background);
|
|
}
|
|
|
|
for (i = 0; i < bodies.length; i++)
|
|
RenderPixi.body(engine, bodies[i]);
|
|
|
|
for (i = 0; i < constraints.length; i++)
|
|
RenderPixi.constraint(engine, constraints[i]);
|
|
|
|
context.render(stage);
|
|
};
|
|
|
|
|
|
/**
|
|
* Description
|
|
* @method constraint
|
|
* @param {engine} engine
|
|
* @param {constraint} constraint
|
|
*/
|
|
RenderPixi.constraint = function(engine, constraint) {
|
|
var render = engine.render,
|
|
bodyA = constraint.bodyA,
|
|
bodyB = constraint.bodyB,
|
|
pointA = constraint.pointA,
|
|
pointB = constraint.pointB,
|
|
stage = render.stage,
|
|
constraintRender = constraint.render,
|
|
primitiveId = 'c-' + constraint.id,
|
|
primitive = render.primitives[primitiveId];
|
|
|
|
// initialise constraint primitive if not existing
|
|
if (!primitive)
|
|
primitive = render.primitives[primitiveId] = new PIXI.Graphics();
|
|
|
|
// don't render if constraint does not have two end points
|
|
if (!constraintRender.visible || !constraint.pointA || !constraint.pointB) {
|
|
primitive.clear();
|
|
return;
|
|
}
|
|
|
|
// add to scene graph if not already there
|
|
if (stage.children.indexOf(primitive) === -1)
|
|
stage.addChild(primitive);
|
|
|
|
// render the constraint on every update, since they can change dynamically
|
|
primitive.clear();
|
|
primitive.beginFill(0, 0);
|
|
primitive.lineStyle(constraintRender.lineWidth, Common.colorToNumber(constraintRender.strokeStyle), 1);
|
|
|
|
if (bodyA) {
|
|
primitive.moveTo(bodyA.position.x + pointA.x, bodyA.position.y + pointA.y);
|
|
} else {
|
|
primitive.moveTo(pointA.x, pointA.y);
|
|
}
|
|
|
|
if (bodyB) {
|
|
primitive.lineTo(bodyB.position.x + pointB.x, bodyB.position.y + pointB.y);
|
|
} else {
|
|
primitive.lineTo(pointB.x, pointB.y);
|
|
}
|
|
|
|
primitive.endFill();
|
|
};
|
|
|
|
/**
|
|
* Description
|
|
* @method body
|
|
* @param {engine} engine
|
|
* @param {body} body
|
|
*/
|
|
RenderPixi.body = function(engine, body) {
|
|
var render = engine.render,
|
|
bodyRender = body.render;
|
|
|
|
if (!bodyRender.visible)
|
|
return;
|
|
|
|
if (bodyRender.sprite && bodyRender.sprite.texture) {
|
|
var spriteId = 'b-' + body.id,
|
|
sprite = render.sprites[spriteId],
|
|
spriteBatch = render.spriteBatch;
|
|
|
|
// initialise body sprite if not existing
|
|
if (!sprite)
|
|
sprite = render.sprites[spriteId] = _createBodySprite(render, body);
|
|
|
|
// add to scene graph if not already there
|
|
if (spriteBatch.children.indexOf(sprite) === -1)
|
|
spriteBatch.addChild(sprite);
|
|
|
|
// update body sprite
|
|
sprite.position.x = body.position.x;
|
|
sprite.position.y = body.position.y;
|
|
sprite.rotation = body.angle;
|
|
} else {
|
|
var primitiveId = 'b-' + body.id,
|
|
primitive = render.primitives[primitiveId],
|
|
stage = render.stage;
|
|
|
|
// initialise body primitive if not existing
|
|
if (!primitive) {
|
|
primitive = render.primitives[primitiveId] = _createBodyPrimitive(render, body);
|
|
primitive.initialAngle = body.angle;
|
|
}
|
|
|
|
// add to scene graph if not already there
|
|
if (stage.children.indexOf(primitive) === -1)
|
|
stage.addChild(primitive);
|
|
|
|
// update body primitive
|
|
primitive.position.x = body.position.x;
|
|
primitive.position.y = body.position.y;
|
|
primitive.rotation = body.angle - primitive.initialAngle;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates a body sprite
|
|
* @method _createBodySprite
|
|
* @private
|
|
* @param {RenderPixi} render
|
|
* @param {body} body
|
|
* @return {PIXI.Sprite} sprite
|
|
*/
|
|
var _createBodySprite = function(render, body) {
|
|
var bodyRender = body.render,
|
|
texturePath = bodyRender.sprite.texture,
|
|
texture = _getTexture(render, texturePath),
|
|
sprite = new PIXI.Sprite(texture);
|
|
|
|
sprite.anchor.x = 0.5;
|
|
sprite.anchor.y = 0.5;
|
|
|
|
return sprite;
|
|
};
|
|
|
|
/**
|
|
* Creates a body primitive
|
|
* @method _createBodyPrimitive
|
|
* @private
|
|
* @param {RenderPixi} render
|
|
* @param {body} body
|
|
* @return {PIXI.Graphics} graphics
|
|
*/
|
|
var _createBodyPrimitive = function(render, body) {
|
|
var bodyRender = body.render,
|
|
options = render.options,
|
|
primitive = new PIXI.Graphics();
|
|
|
|
primitive.clear();
|
|
|
|
if (!options.wireframes) {
|
|
primitive.beginFill(Common.colorToNumber(bodyRender.fillStyle), 1);
|
|
primitive.lineStyle(body.render.lineWidth, Common.colorToNumber(bodyRender.strokeStyle), 1);
|
|
} else {
|
|
primitive.beginFill(0, 0);
|
|
primitive.lineStyle(1, Common.colorToNumber('#bbb'), 1);
|
|
}
|
|
|
|
primitive.moveTo(body.vertices[0].x - body.position.x, body.vertices[0].y - body.position.y);
|
|
|
|
for (var j = 1; j < body.vertices.length; j++) {
|
|
primitive.lineTo(body.vertices[j].x - body.position.x, body.vertices[j].y - body.position.y);
|
|
}
|
|
|
|
primitive.lineTo(body.vertices[0].x - body.position.x, body.vertices[0].y - body.position.y);
|
|
|
|
primitive.endFill();
|
|
|
|
// angle indicator
|
|
if (options.showAngleIndicator || options.showAxes) {
|
|
primitive.beginFill(0, 0);
|
|
|
|
if (options.wireframes) {
|
|
primitive.lineStyle(1, Common.colorToNumber('#CD5C5C'), 1);
|
|
} else {
|
|
primitive.lineStyle(1, Common.colorToNumber(body.render.strokeStyle));
|
|
}
|
|
|
|
primitive.moveTo(0, 0);
|
|
primitive.lineTo(((body.vertices[0].x + body.vertices[body.vertices.length-1].x) / 2) - body.position.x,
|
|
((body.vertices[0].y + body.vertices[body.vertices.length-1].y) / 2) - body.position.y);
|
|
|
|
primitive.endFill();
|
|
}
|
|
|
|
return primitive;
|
|
};
|
|
|
|
/**
|
|
* Gets the requested texture (a PIXI.Texture) via its path
|
|
* @method _getTexture
|
|
* @private
|
|
* @param {RenderPixi} render
|
|
* @param {string} imagePath
|
|
* @return {PIXI.Texture} texture
|
|
*/
|
|
var _getTexture = function(render, imagePath) {
|
|
var texture = render.textures[imagePath];
|
|
|
|
if (!texture)
|
|
texture = render.textures[imagePath] = PIXI.Texture.fromImage(imagePath);
|
|
|
|
return texture;
|
|
};
|
|
|
|
})();
|
|
|
|
; // End src/render/RenderPixi.js
|
|
|
|
|
|
// aliases
|
|
|
|
World.add = Composite.add;
|
|
World.remove = Composite.remove;
|
|
World.addComposite = Composite.addComposite;
|
|
World.addBody = Composite.addBody;
|
|
World.addConstraint = Composite.addConstraint;
|
|
World.clear = Composite.clear;
|
|
|
|
// exports
|
|
|
|
Matter.Body = Body;
|
|
Matter.Composite = Composite;
|
|
Matter.World = World;
|
|
Matter.Contact = Contact;
|
|
Matter.Detector = Detector;
|
|
Matter.Grid = Grid;
|
|
Matter.Pairs = Pairs;
|
|
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.Render = Render;
|
|
Matter.RenderPixi = RenderPixi;
|
|
Matter.Events = Events;
|
|
Matter.Query = Query;
|
|
|
|
// 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
|
|
|
|
})(); |