0
0
Fork 0
mirror of https://github.com/liabru/matter-js.git synced 2025-01-20 17:10:11 -05:00

initial commit

This commit is contained in:
liabru 2014-02-19 14:15:05 +00:00
commit c80fe7f3f9
43 changed files with 11025 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
npm-debug.log

28
.jshintrc Normal file
View file

@ -0,0 +1,28 @@
{
// settings
"passfail" : false, // Stop on first error.
"maxerr" : 100, // Maximum error before stopping.
// dev
"debug" : false, // Allow debugger statements e.g. browser breakpoints.
"devel" : true, // Allow developments statements e.g. `console.log();`.
// ECMAScript 5
"es5" : false, // Allow ECMAScript 5 syntax.
"strict" : false, // Require `use strict` pragma in every file.
"globalstrict" : false, // Allow global "use strict" (also enables 'strict').
// options
"laxbreak" : true, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons.
// style
"newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`.
"noempty" : true, // Prohibit use of empty blocks.
"nonew" : true, // Prohibit use of constructors for side-effects.
"onevar" : false, // Allow only one `var` statement per function.
"plusplus" : false, // Prohibit use of `++` & `--`.
"sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`.
"trailing" : false, // Prohibit trailing whitespaces.
"white" : false, // Check against strict whitespace and indentation rules.
"indent" : 4 // Specify indentation spacing
}

78
Gruntfile.js Normal file
View file

@ -0,0 +1,78 @@
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: {
build: {
options: {
process: function(src, filepath) {
return '// Begin ' + filepath + '\n\n' + src + '\n\n; // End ' + filepath + '\n\n';
}
},
src: ['src/**/*.js', '!src/module/*'],
dest: 'build/<%= pkg.name %>.js'
},
pack: {
options: {
banner: '/**\n* <%= pkg.name %>.js <%= pkg.version %> <%= grunt.template.today("yyyy-mm-dd") %>\n* <%= pkg.homepage %>\n* License: <%= pkg.license %>\n*/\n\n',
},
src: ['src/module/Intro.js', 'build/<%= pkg.name %>.js', 'src/module/Outro.js'],
dest: 'build/<%= pkg.name %>.js'
}
},
uglify: {
options: {
banner: '/**\n* <%= pkg.name %>.min.js <%= pkg.version %> <%= grunt.template.today("yyyy-mm-dd") %>\n* <%= pkg.homepage %>\n* License: <%= pkg.license %>\n*/\n\n',
},
build: {
src: 'build/<%= pkg.name %>.js',
dest: 'build/<%= pkg.name %>.min.js'
}
},
copy: {
demo: {
src: 'build/<%= pkg.name %>.js',
dest: 'demo/js/lib/<%= pkg.name %>.js'
}
},
jshint: {
options: {
jshintrc: '.jshintrc'
},
all: ['src/**/*.js', '!src/module/*']
},
connect: {
watch: {
options: {
port: 9000,
open: 'http://localhost:9000/demo',
livereload: 9001
}
}
},
watch: {
options: {
livereload: {
port: 9001
}
},
src: {
files: ['src/**/*.js'],
tasks: ['concat', 'uglify', 'copy']
},
demo: {
files: ['build/matter.js', 'demo/js/**/*.html', 'demo/js/**/*.js', 'demo/css/**/*.css']
}
},
});
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('default', ['test', 'concat', 'uglify', 'copy']);
grunt.registerTask('test', ['jshint']);
grunt.registerTask('dev', ['connect:watch', 'watch']);
};

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
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.

110
README.md Normal file
View file

@ -0,0 +1,110 @@
# Matter.js
*Matter.js* is a 2D rigid body physics engine for the web written in JavaScript (yes, another)
[brm.io/matter-js](http://brm.io/matter-js)
### Demos
- [Mixed Shapes](http://brm.io/matter-js-demo#mixed)
- [Solid Shapes](http://brm.io/matter-js-demo#mixedSolid)
- [Stack](http://brm.io/matter-js-demo#stack)
- [Circle Stack](http://brm.io/matter-js-demo#circleStack)
- [Pyramid](http://brm.io/matter-js-demo#pyramid)
- [Car](http://brm.io/matter-js-demo#car)
- [Newton's Cradle](http://brm.io/matter-js-demo#newtonsCradle)
- [Restitution](http://brm.io/matter-js-demo#restitution)
- [Friction](http://brm.io/matter-js-demo#friction)
- [Air Friction](http://brm.io/matter-js-demo#airFriction)
- [Reverse Gravity](http://brm.io/matter-js-demo#gravity)
- [Sleeping](http://brm.io/matter-js-demo#sleeping)
- [Grid Broadphase](http://brm.io/matter-js-demo#broadphase)
- [Chains](http://brm.io/matter-js-demo#chains)
- [Ball Pool](http://brm.io/matter-js-demo#ballPool)
- [Wrecking Ball](http://brm.io/matter-js-demo#wreckingBall)
- [Avalanche](http://brm.io/matter-js-demo#avalanche)
- [Beach Balls](http://brm.io/matter-js-demo#beachBalls)
- [Stress 1](http://brm.io/matter-js-demo#stress)
- [Stress 2](http://brm.io/matter-js-demo#stress2)
### Features
- Physical properties (mass, area, density etc.)
- Rigid bodies of any convex polygon
- Stable stacking and resting
- Restitution (elastic and inelastic collisions)
- Conservation of momentum
- Friction and resistance
- Constraints
- Gravity
- Composite bodies
- Sleeping and static bodies
- Original JavaScript physics implementation (not a port)
- HTML5 canvas renderer
- Mobile-compatible (touch, scaleable)
- Cross-browser (chrome, firefox, IE8+)
- World state serialisation (requires
[resurrect.js](https://github.com/skeeto/resurrect-js))
- Built in GUI for testing (requires
[dat.gui.js](http://workshop.chromeexperiments.com/examples/gui/))
- Time scaling (slow-mo, speed-up)
- Broad-phase, mid-phase and narrow-phase collisions
### Status
Matter.js is currently only *alpha* status, so production use should be avoided.
This engine is the result of [learning game physics](http://brm.io/game-physics-for-beginners/) and getting a bit carried away.
<br>There's no intention to compete with other engines, so I'm not sure yet if it will be developed further.
<br>That said, the engine is reasonably stable and performs well, it's just not feature complete.
Though if I get time and people are interested, I may continue working on it.
<br>Contributions are also welcome, if you'd like to help.
### Install
Download [matter.js](#) or [matter.min.js](#) and include the script in your HTML file:
<script src="matter.js" type="text/javascript"></script>
### Usage
See [Demo.js](#) and [DemoMobile.js](#) for many usage examples. No docs at the moment, sorry.
<br>When loaded, all functions are under the global <code>Matter.*</code> namespace.
### Implementation
The technical details for physics nerds and game devs.
<br>This engine is using the following techniques:
- Time-corrected position Verlet integrator
- Adaptive grid broad-phase detection
- AABB mid-phase detection
- SAT narrow-phase detection
- Iterative sequential impulse solver and position solver
- Resting collisions with resting constraints ala Erin Catto's method
(GDC08)
- Temporal coherence impulse caching and warming
- Collision pairs, contacts and impulses maintained with a pair
manager
- Approximate Coulomb friction model using friction constraints
- Constraints solved with the Gauss-Siedel method
- Semi-variable time step, synced with rendering
- A basic sleeping strategy
- HTML5 canvas renderer
### References
See my post on [Game physics for beginners](http://brm.io/game-physics-for-beginners/).
### License
Matter.js is licensed under [The MIT License (MIT)](http://opensource.org/licenses/MIT)
<br/>Copyright (c) 2014 Liam Brummitt
This license is also supplied with the release and source code.
<br/>As stated in the license, absolutely no warranty is provided.
### Similar Projects
If you need a more complete engine, check out jswiki's list of [Physics libraries](https://github.com/bebraw/jswiki/wiki/Physics-libraries)

3185
build/matter.js Normal file

File diff suppressed because it is too large Load diff

8
build/matter.min.js vendored Normal file

File diff suppressed because one or more lines are too long

85
demo/css/style.css Normal file
View file

@ -0,0 +1,85 @@
body {
background: #222;
font-family: Georgia, Times, "Times New Roman", serif;
color: #aaa;
}
body.mobile {
font-size: 0.9em;
}
* {
padding: 0;
margin: 0;
}
h1 {
color: #fff;
display: block;
margin: 0 0 1em 0;
font-weight: normal;
font-size: 30px;
}
.nav-sep {
padding: 0 5px;
}
a, a:link, a:visited, a:active, a:hover {
color: #aaa;
text-decoration: none;
border-bottom: 1px solid #555;
padding: 0 0 2px 0;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 6% 300px 6% 6%;
}
.mobile .container {
padding: 50px 40px 0 40px;
}
canvas {
margin: 40px 0;
max-width: 100%;
}
.mobile canvas {
margin: 0;
}
canvas:active {
cursor: pointer;
cursor: -webkit-grabbing;
}
.controls-container {
margin: 40px 0 0 0;
width: 100%;
}
#canvas-container {
margin: 20px 0;
}
#demo-select {
padding: 8px 10px;
width: 40%;
color: #000;
}
#demo-reset {
padding: 8px 10px;
color: #000;
}
#demo-start {
display: block;
margin: 50px 0;
padding: 20px 40px;
width: 200px;
color: #000;
}

46
demo/index.html Normal file
View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="robots" content="noindex">
<script type="text/javascript" src="./js/lib/dat.gui.min.js"></script>
<script type="text/javascript" src="./js/lib/resurrect.js"></script>
<script type="text/javascript" src="./js/lib/matter.js"></script>
<script type="text/javascript" src="./js/Demo.js"></script>
<link rel="stylesheet" href="./css/style.css" type="text/css">
<title>Matter.js Physics Engine Demo</title>
</head>
<body>
<div class="container">
<h1>Matter.js Physics Engine Demo</h1>
<p><a href="./mobile.html">Mobile Demo</a> <span class="nav-sep">&middot;</span> <a href="http://brm.io/matter-js/">Project page</a> <span class="nav-sep">&middot;</span> <a href="https://github.com/liabru/matter-js">GitHub</a></p>
<div class="controls-container">
<select id="demo-select">
<option value="mixed">Mixed Shapes</option>
<option value="mixedSolid">Solid Rendering</option>
<option value="stack">Stack</option>
<option value="circleStack">Circle Stack</option>
<option value="pyramid">Pyramid</option>
<option value="car">Car</option>
<option value="newtonsCradle">Newton's Cradle</option>
<option value="restitution">Restitution</option>
<option value="friction">Friction</option>
<option value="airFriction">Air Friction</option>
<option value="gravity">Reverse Gravity</option>
<option value="sleeping">Sleeping</option>
<option value="broadphase">Grid Broadphase</option>
<option value="chains">Chains</option>
<option value="ballPool">Ball Pool</option>
<option value="wreckingBall">Wrecking Ball</option>
<option value="avalanche">Avalanche</option>
<option value="beachBalls">Beach Balls</option>
<option value="stress">Stress 1</option>
<option value="stress2">Stress 2</option>
</select>
<input id="demo-reset" value="Reset" type="submit">
</div>
<div id="canvas-container"></div>
</div>
</body>
</html>

514
demo/js/Demo.js Normal file
View file

@ -0,0 +1,514 @@
(function() {
// Matter aliases
var Engine = Matter.Engine,
Gui = Matter.Gui,
World = Matter.World,
Bodies = Matter.Bodies,
Body = Matter.Body,
Composite = Matter.Composite,
Composites = Matter.Composites,
Common = Matter.Common,
Constraint = Matter.Constraint;
var Demo = {};
var _engine,
_gui,
_sceneName;
Demo.init = function() {
var container = document.getElementById('canvas-container');
// engine options - these are the defaults
var options = {
positionIterations: 6,
velocityIterations: 4,
enableSleeping: false,
timeScale: 1
};
// create a Matter engine, with the element to insert the canvas into
// NOTE: this is actually Matter.Engine.create(), see the aliases at top of this file
_engine = Engine.create(container, options);
// run the engine
Engine.run(_engine);
// default scene function name
_sceneName = 'mixed';
// get the scene function name from hash
if (window.location.hash.length !== 0)
_sceneName = window.location.hash.replace('#', '');
// set up a scene with bodies
Demo[_sceneName]();
// set up demo interface
Demo.initControls();
};
if (window.addEventListener) {
window.addEventListener('load', Demo.init);
} else if (window.attachEvent) {
window.attachEvent('load', Demo.init);
}
Demo.initControls = function() {
var demoSelect = document.getElementById('demo-select'),
demoReset = document.getElementById('demo-reset');
// create a dat.gui using Matter helper
_gui = Gui.create(_engine);
demoSelect.value = _sceneName;
demoSelect.addEventListener('change', function(e) {
Demo[_sceneName = e.target.value]();
Gui.update(_gui);
var scrollY = window.scrollY;
window.location.hash = _sceneName;
window.scrollY = scrollY;
});
demoReset.addEventListener('click', function(e) {
Demo[_sceneName]();
Gui.update(_gui);
});
};
Demo.reset = function() {
var _world = _engine.world;
World.clear(_world);
Engine.clear(_engine);
_engine.enableSleeping = false;
_engine.world.gravity.y = 1;
var offset = 5;
World.addBody(_world, Bodies.rectangle(400, -offset, 800.5 + 2 * offset, 50.5, { isStatic: true }));
World.addBody(_world, Bodies.rectangle(400, 600 + offset, 800.5 + 2 * offset, 50.5, { isStatic: true }));
World.addBody(_world, Bodies.rectangle(800 + offset, 300, 50.5, 600.5 + 2 * offset, { isStatic: true }));
World.addBody(_world, Bodies.rectangle(-offset, 300, 50.5, 600.5 + 2 * offset, { isStatic: true }));
var renderOptions = _engine.render.options;
renderOptions.wireframes = true;
renderOptions.showDebug = false;
renderOptions.showBroadphase = false;
renderOptions.showBounds = false;
renderOptions.showVelocity = false;
renderOptions.showCollisions = false;
renderOptions.showAxes = false;
renderOptions.showPositions = false;
renderOptions.showAngleIndicator = true;
renderOptions.showIds = false;
renderOptions.showShadows = false;
};
// all functions below are for setting up scenes
Demo.mixed = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(20, 20, 20, 5, 0, 0, function(x, y, column, row) {
switch (Math.round(Common.random(0, 1))) {
case 0:
if (Math.random() < 0.8) {
return Bodies.rectangle(x, y, Common.random(20, 50), Common.random(20, 50));
} else {
return Bodies.rectangle(x, y, Common.random(80, 120), Common.random(20, 30));
}
case 1:
return Bodies.polygon(x, y, Math.round(Common.random(1, 8)), Common.random(20, 50));
}
});
World.addComposite(_world, stack);
var renderOptions = _engine.render.options;
};
Demo.mixedSolid = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(50, 50, 12, 3, 0, 0, function(x, y, column, row) {
switch (Math.round(Common.random(0, 1))) {
case 0:
if (Math.random() < 0.8) {
return Bodies.rectangle(x, y, Common.random(20, 50), Common.random(20, 50));
} else {
return Bodies.rectangle(x, y, Common.random(80, 120), Common.random(20, 30));
}
case 1:
return Bodies.polygon(x, y, Math.round(Common.random(1, 8)), Common.random(20, 50));
}
});
World.addComposite(_world, stack);
var renderOptions = _engine.render.options;
renderOptions.wireframes = false;
renderOptions.showAngleIndicator = false;
renderOptions.showShadows = true;
};
Demo.chains = function() {
var _world = _engine.world;
Demo.reset();
var groupId = Body.nextGroupId();
var ropeA = Composites.stack(200, 100, 5, 2, 10, 10, function(x, y, column, row) {
return Bodies.rectangle(x, y, 50, 20, { groupId: groupId });
});
Composites.chain(ropeA, 0.5, 0, -0.5, 0, { stiffness: 0.8, length: 2 });
Composite.addConstraint(ropeA, Constraint.create({
bodyB: ropeA.bodies[0],
pointB: { x: -25, y: 0 },
pointA: { x: 200, y: 100 },
stiffness: 0.5
}));
World.addComposite(_world, ropeA);
groupId = Body.nextGroupId();
var ropeB = Composites.stack(500, 100, 5, 2, 10, 10, function(x, y, column, row) {
return Bodies.circle(x, y, 20, { groupId: groupId });
});
Composites.chain(ropeB, 0.5, 0, -0.5, 0, { stiffness: 0.8, length: 2 });
Composite.addConstraint(ropeB, Constraint.create({
bodyB: ropeB.bodies[0],
pointB: { x: -20, y: 0 },
pointA: { x: 500, y: 100 },
stiffness: 0.5
}));
World.addComposite(_world, ropeB);
};
Demo.car = function() {
var _world = _engine.world;
Demo.reset();
var scale;
scale = 0.8;
World.addComposite(_world, Composites.car(150, 100, 100 * scale, 40 * scale, 30 * scale));
scale = 0.7;
World.addComposite(_world, Composites.car(350, 300, 100 * scale, 40 * scale, 30 * scale));
World.addBody(_world, Bodies.rectangle(200, 150, 700, 20, { isStatic: true, angle: Math.PI * 0.06 }));
World.addBody(_world, Bodies.rectangle(500, 350, 700, 20, { isStatic: true, angle: -Math.PI * 0.06 }));
World.addBody(_world, Bodies.rectangle(340, 580, 700, 20, { isStatic: true, angle: Math.PI * 0.04 }));
var renderOptions = _engine.render.options;
renderOptions.showAngleIndicator = true;
renderOptions.showCollisions = true;
};
Demo.friction = function() {
var _world = _engine.world;
Demo.reset();
World.addBody(_world, Bodies.rectangle(300, 180, 700, 20, { isStatic: true, angle: Math.PI * 0.06 }));
World.addBody(_world, Bodies.rectangle(300, 70, 40, 40, { friction: 0.001 }));
World.addBody(_world, Bodies.rectangle(300, 350, 700, 20, { isStatic: true, angle: Math.PI * 0.06 }));
World.addBody(_world, Bodies.rectangle(300, 250, 40, 40, { friction: 0.0005 }));
World.addBody(_world, Bodies.rectangle(300, 520, 700, 20, { isStatic: true, angle: Math.PI * 0.06 }));
World.addBody(_world, Bodies.rectangle(300, 430, 40, 40, { friction: 0 }));
var renderOptions = _engine.render.options;
renderOptions.showAngleIndicator = true;
renderOptions.showCollisions = true;
};
Demo.airFriction = function() {
var _world = _engine.world;
Demo.reset();
World.addBody(_world, Bodies.rectangle(200, 100, 60, 60, { frictionAir: 0.001 }));
World.addBody(_world, Bodies.rectangle(400, 100, 60, 60, { frictionAir: 0.05 }));
World.addBody(_world, Bodies.rectangle(600, 100, 60, 60, { frictionAir: 0.1 }));
var renderOptions = _engine.render.options;
renderOptions.showAngleIndicator = true;
renderOptions.showCollisions = true;
};
Demo.sleeping = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(50, 50, 12, 3, 0, 0, function(x, y, column, row) {
switch (Math.round(Common.random(0, 1))) {
case 0:
if (Math.random() < 0.8) {
return Bodies.rectangle(x, y, Common.random(20, 50), Common.random(20, 50));
} else {
return Bodies.rectangle(x, y, Common.random(80, 120), Common.random(20, 30));
}
case 1:
return Bodies.polygon(x, y, Math.round(Common.random(1, 8)), Common.random(20, 50));
}
});
World.addComposite(_world, stack);
_engine.enableSleeping = true;
var renderOptions = _engine.render.options;
};
Demo.broadphase = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(20, 20, 20, 5, 0, 0, function(x, y, column, row) {
switch (Math.round(Common.random(0, 1))) {
case 0:
if (Math.random() < 0.8) {
return Bodies.rectangle(x, y, Common.random(20, 50), Common.random(20, 50));
} else {
return Bodies.rectangle(x, y, Common.random(80, 120), Common.random(20, 30));
}
case 1:
return Bodies.polygon(x, y, Math.round(Common.random(1, 8)), Common.random(20, 50));
}
});
World.addComposite(_world, stack);
var renderOptions = _engine.render.options;
renderOptions.showBroadphase = true;
};
Demo.gravity = function() {
var _world = _engine.world;
Demo.reset();
_engine.world.gravity.y = -1;
var stack = Composites.stack(20, 20, 20, 5, 0, 0, function(x, y, column, row) {
switch (Math.round(Common.random(0, 1))) {
case 0:
if (Math.random() < 0.8) {
return Bodies.rectangle(x, y, Common.random(20, 50), Common.random(20, 50));
} else {
return Bodies.rectangle(x, y, Common.random(80, 120), Common.random(20, 30));
}
case 1:
return Bodies.polygon(x, y, Math.round(Common.random(1, 8)), Common.random(20, 50));
}
});
World.addComposite(_world, stack);
var renderOptions = _engine.render.options;
};
Demo.avalanche = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(20, 20, 20, 5, 0, 0, function(x, y, column, row) {
return Bodies.circle(x, y, Common.random(10, 20), { friction: 0.00001, restitution: 0.5, density: 0.1 });
});
World.addComposite(_world, stack);
World.addBody(_world, Bodies.rectangle(200, 150, 700, 20, { isStatic: true, angle: Math.PI * 0.06 }));
World.addBody(_world, Bodies.rectangle(500, 350, 700, 20, { isStatic: true, angle: -Math.PI * 0.06 }));
World.addBody(_world, Bodies.rectangle(340, 580, 700, 20, { isStatic: true, angle: Math.PI * 0.04 }));
};
Demo.newtonsCradle = function() {
var _world = _engine.world;
Demo.reset();
var cradle = Composites.newtonsCradle(300, 100, 5, 30, 200);
World.addComposite(_world, cradle);
Body.translate(cradle.bodies[0], { x: -180, y: -100 });
cradle = Composites.newtonsCradle(250, 400, 7, 20, 140);
World.addComposite(_world, cradle);
Body.translate(cradle.bodies[0], { x: -140, y: -100 });
var renderOptions = _engine.render.options;
renderOptions.showVelocity = true;
renderOptions.showAngleIndicator = false;
};
Demo.stack = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(100, 100, 10, 5, 0, 0, function(x, y, column, row) {
return Bodies.rectangle(x, y, 40, 40);
});
World.addComposite(_world, stack);
var renderOptions = _engine.render.options;
};
Demo.circleStack = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(100, 100, 10, 10, 20, 0, function(x, y, column, row) {
return Bodies.circle(x, y, 20);
});
World.addComposite(_world, stack);
var renderOptions = _engine.render.options;
};
Demo.wreckingBall = function() {
var _world = _engine.world;
Demo.reset();
var rows = 10,
yy = 600 - 21 - 40 * rows;
var stack = Composites.stack(400, yy, 5, rows, 0, 0, function(x, y, column, row) {
return Bodies.rectangle(x, y, 40, 40, { friction: 0.9, restitution: 0.1 });
});
World.addComposite(_world, stack);
var ball = Bodies.circle(100, 400, 50, { density: 1, frictionAir: 0.001});
World.addBody(_world, ball);
World.addConstraint(_world, Constraint.create({
pointA: { x: 300, y: 100 },
bodyB: ball
}));
var renderOptions = _engine.render.options;
renderOptions.showCollisions = true;
renderOptions.showVelocity = true;
};
Demo.ballPool = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(100, 50, 10, 15, 10, 10, function(x, y, column, row) {
return Bodies.circle(x, y, Common.random(15, 30), { restitution: 0.6, friction: 0.1 });
});
World.addComposite(_world, stack);
World.addBody(_world, Bodies.polygon(200, 560, 3, 60));
World.addBody(_world, Bodies.polygon(400, 560, 5, 60));
World.addBody(_world, Bodies.rectangle(600, 560, 80, 80));
var renderOptions = _engine.render.options;
};
Demo.stress = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(90, 30, 18, 15, 0, 0, function(x, y, column, row) {
return Bodies.rectangle(x, y, 35, 35);
});
World.addComposite(_world, stack);
var renderOptions = _engine.render.options;
renderOptions.showAngleIndicator = false;
};
Demo.stress2 = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(100, 120, 25, 18, 0, 0, function(x, y, column, row) {
return Bodies.rectangle(x, y, 25, 25);
});
World.addComposite(_world, stack);
var renderOptions = _engine.render.options;
renderOptions.showAngleIndicator = false;
};
Demo.pyramid = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.pyramid(100, 100, 15, 10, 0, 0, function(x, y, column, row) {
return Bodies.rectangle(x, y, 40, 40);
});
World.addComposite(_world, stack);
var renderOptions = _engine.render.options;
};
Demo.restitution = function() {
var _world = _engine.world;
Demo.reset();
var rest = 0.9,
space = 600 / 5;
World.addBody(_world, Bodies.rectangle(100 + space * 0, 150, 50, 50, { restitution: rest }));
World.addBody(_world, Bodies.rectangle(100 + space * 1, 150, 50, 50, { restitution: rest, angle: -Math.PI * 0.15 }));
World.addBody(_world, Bodies.rectangle(100 + space * 2, 150, 50, 50, { restitution: rest, angle: -Math.PI * 0.25 }));
World.addBody(_world, Bodies.circle(100 + space * 3, 150, 25, { restitution: rest }));
World.addBody(_world, Bodies.rectangle(100 + space * 5, 150, 180, 20, { restitution: rest, angle: -Math.PI * 0.5 }));
var renderOptions = _engine.render.options;
renderOptions.showCollisions = true;
renderOptions.showVelocity = true;
renderOptions.showAngleIndicator = true;
};
Demo.beachBalls = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(0, 100, 5, 1, 20, 0, function(x, y, column, row) {
return Bodies.circle(x, y, 75, { restitution: 0.9 });
});
World.addComposite(_world, stack);
};
})();

153
demo/js/DemoMobile.js Normal file
View file

@ -0,0 +1,153 @@
(function() {
// Matter aliases
var Engine = Matter.Engine,
Gui = Matter.Gui,
World = Matter.World,
Bodies = Matter.Bodies,
Body = Matter.Body,
Composite = Matter.Composite,
Composites = Matter.Composites,
Common = Matter.Common,
Constraint = Matter.Constraint;
var Demo = {};
var _engine,
_sceneName = 'mixed',
_sceneWidth,
_sceneHeight;
Demo.init = function() {
var canvasContainer = document.getElementById('canvas-container'),
demoStart = document.getElementById('demo-start');
demoStart.addEventListener('click', function() {
demoStart.style.display = 'none';
_engine = Engine.create(canvasContainer, {
positionIterations: 3,
velocityIterations: 2,
render: {
options: {
wireframes: true,
showAngleIndicator: true
}
}
});
_engine.broadphase.current = 'bruteForce';
Demo.fullscreen();
setTimeout(function() {
Engine.run(_engine);
Demo.updateScene();
}, 800);
});
window.addEventListener('deviceorientation', Demo.updateGravity, true);
window.addEventListener('touchstart', Demo.fullscreen);
window.addEventListener('orientationchange', function() {
Demo.updateScene();
Demo.fullscreen();
}, false);
};
if (window.addEventListener) {
window.addEventListener('load', Demo.init);
} else if (window.attachEvent) {
window.attachEvent('load', Demo.init);
}
Demo.updateScene = function() {
if (!_engine)
return;
_sceneWidth = document.documentElement.clientWidth;
_sceneHeight = document.documentElement.clientHeight;
var boundsMax = _engine.world.bounds.max,
renderOptions = _engine.render.options,
canvas = _engine.render.canvas;
boundsMax.x = _sceneWidth;
boundsMax.y = _sceneHeight;
canvas.width = renderOptions.width = _sceneWidth;
canvas.height = renderOptions.height = _sceneHeight;
Demo[_sceneName]();
};
Demo.updateGravity = function () {
if (!_engine)
return;
var orientation = window.orientation,
gravity = _engine.world.gravity;
if (orientation === 0) {
gravity.x = Common.clamp(event.gamma, -90, 90) / 90;
gravity.y = Common.clamp(event.beta, -90, 90) / 90;
} else if (orientation === 180) {
gravity.x = Common.clamp(event.gamma, -90, 90) / 90;
gravity.y = Common.clamp(-event.beta, -90, 90) / 90;
} else if (orientation === 90) {
gravity.x = Common.clamp(event.beta, -90, 90) / 90;
gravity.y = Common.clamp(-event.gamma, -90, 90) / 90;
} else if (orientation === -90) {
gravity.x = Common.clamp(-event.beta, -90, 90) / 90;
gravity.y = Common.clamp(event.gamma, -90, 90) / 90;
}
};
Demo.fullscreen = function(){
var _fullscreenElement = _engine.render.canvas;
if (!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement) {
if (_fullscreenElement.requestFullscreen) {
_fullscreenElement.requestFullscreen();
} else if (_fullscreenElement.mozRequestFullScreen) {
_fullscreenElement.mozRequestFullScreen();
} else if (_fullscreenElement.webkitRequestFullscreen) {
_fullscreenElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
}
};
Demo.reset = function() {
var _world = _engine.world;
World.clear(_world);
Engine.clear(_engine);
var offset = 5;
World.addBody(_world, Bodies.rectangle(_sceneWidth * 0.5, -offset, _sceneWidth + 0.5, 50.5, { isStatic: true }));
World.addBody(_world, Bodies.rectangle(_sceneWidth * 0.5, _sceneHeight + offset, _sceneWidth + 0.5, 50.5, { isStatic: true }));
World.addBody(_world, Bodies.rectangle(_sceneWidth + offset, _sceneHeight * 0.5, 50.5, _sceneHeight + 0.5, { isStatic: true }));
World.addBody(_world, Bodies.rectangle(-offset, _sceneHeight * 0.5, 50.5, _sceneHeight + 0.5, { isStatic: true }));
};
Demo.mixed = function() {
var _world = _engine.world;
Demo.reset();
var stack = Composites.stack(20, 20, 10, 5, 0, 0, function(x, y, column, row) {
switch (Math.round(Common.random(0, 1))) {
case 0:
if (Math.random() < 0.8) {
return Bodies.rectangle(x, y, Common.random(20, 40), Common.random(20, 40), { friction: 0.01, restitution: 0.4 });
} else {
return Bodies.rectangle(x, y, Common.random(80, 120), Common.random(20, 30), { friction: 0.01, restitution: 0.4 });
}
case 1:
return Bodies.polygon(x, y, Math.round(Common.random(4, 6)), Common.random(20, 40), { friction: 0.01, restitution: 0.4 });
}
});
World.addComposite(_world, stack);
};
})();

94
demo/js/lib/dat.gui.min.js vendored Normal file

File diff suppressed because one or more lines are too long

3185
demo/js/lib/matter.js Normal file

File diff suppressed because it is too large Load diff

439
demo/js/lib/resurrect.js Normal file
View file

@ -0,0 +1,439 @@
/**
* # ResurrectJS
* @version 1.0.0
*
* ResurrectJS preserves object behavior (prototypes) and reference
* circularity with a special JSON encoding. Unlike regular JSON,
* Date, RegExp, DOM objects, and `undefined` are also properly
* preserved.
*
* ## Examples
*
* function Foo() {}
* Foo.prototype.greet = function() { return "hello"; };
*
* // Behavior is preserved:
* var necromancer = new Resurrect();
* var json = necromancer.stringify(new Foo());
* var foo = necromancer.resurrect(json);
* foo.greet(); // => "hello"
*
* // References to the same object are preserved:
* json = necromancer.stringify([foo, foo]);
* var array = necromancer.resurrect(json);
* array[0] === array[1]; // => true
* array[1].greet(); // => "hello"
*
* // Dates are restored properly
* json = necromancer.stringify(new Date());
* var date = necromancer.resurrect(json);
* Object.prototype.toString.call(date); // => "[object Date]"
*
* ## Options
*
* Options are provided to the constructor as an object with these
* properties:
*
* prefix ('#'): A prefix string used for temporary properties added
* to objects during serialization and deserialization. It is
* important that you don't use any properties beginning with this
* string. This option must be consistent between both
* serialization and deserialization.
*
* cleanup (false): Perform full property cleanup after both
* serialization and deserialization using the `delete`
* operator. This may cause performance penalties (breaking hidden
* classes in V8) on objects that ResurrectJS touches, so enable
* with care.
*
* revive (true): Restore behavior (__proto__) to objects that have
* been resurrected. If this is set to false during serialization,
* resurrection information will not be encoded. You still get
* circularity and Date support.
*
* resolver (Resurrect.NamespaceResolver(window)): Converts between
* a name and a prototype. Create a custom resolver if your
* constructors are not stored in global variables. The resolver
* has two methods: getName(object) and getPrototype(string).
*
* For example,
*
* var necromancer = new Resurrect({
* prefix: '__#',
* cleanup: true
* });
*
* ## Caveats
*
* * With the default resolver, all constructors must be named and
* stored in the global variable under that name. This is required
* so that the prototypes can be looked up and reconnected at
* resurrection time.
*
* * The wrapper objects Boolean, String, and Number will be
* unwrapped. This means extra properties added to these objects
* will not be preserved.
*
* * Functions cannot ever be serialized. Resurrect will throw an
* error if a function is found when traversing a data structure.
*/
/**
* @namespace
* @constructor
*/
function Resurrect(options) {
this.table = null;
this.prefix = '#';
this.cleanup = false;
this.revive = true;
for (var option in options) {
if (options.hasOwnProperty(option)) {
this[option] = options[option];
}
}
this.refcode = this.prefix + 'id';
this.origcode = this.prefix + 'original';
this.buildcode = this.prefix + '.';
this.valuecode = this.prefix + 'v';
}
/* Helper Objects */
/**
* @constructor
*/
Resurrect.prototype.Error = function ResurrectError(message) {
this.message = message || '';
this.stack = new Error().stack;
};
Resurrect.prototype.Error.prototype = Object.create(Error.prototype);
Resurrect.prototype.Error.prototype.name = 'ResurrectError';
/**
* Resolves prototypes through the properties on an object and
* constructor names.
* @constructor
*/
Resurrect.NamespaceResolver = function(scope) {
this.scope = scope;
};
/**
* Gets the prototype of the given property name from an object. If
* not found, it throws an error.
* @param {string} name
* @method
*/
Resurrect.NamespaceResolver.prototype.getPrototype = function(name) {
var constructor = this.scope[name];
if (constructor) {
return constructor.prototype;
} else {
throw new Resurrect.prototype.Error('Unknown constructor: ' + name);
}
};
/**
* Get the prototype name for an object, to be fetched later with
* getPrototype.
* @returns {string} Null if the constructor is Object.
* @method
*/
Resurrect.NamespaceResolver.prototype.getName = function(object) {
var constructor = object.constructor.name;
if (constructor == null) { // IE
var funcPattern = /^\s*function\s*([A-Za-z0-9_$]*)/;
constructor = funcPattern.exec(object.constructor)[1];
}
if (constructor === '') {
var msg = "Can't serialize objects with anonymous constructors.";
throw new Resurrect.prototype.Error(msg);
} else if (constructor === 'Object' || constructor === 'Array') {
return null;
} else {
return constructor;
}
};
/* Set the default resolver searches the global object. */
Resurrect.prototype.resolver = new Resurrect.NamespaceResolver(window);
/**
* Create a DOM node from HTML source; behaves like a constructor.
* @constructor
*/
Resurrect.Node = function(html) {
var div = document.createElement('div');
div.innerHTML = html;
return div.firstChild;
};
/* Type Tests */
/**
* @param {string} type
* @returns {Function} A function that tests for type.
*/
Resurrect.is = function(type) {
var string = '[object ' + type + ']';
return function(object) {
return Object.prototype.toString.call(object) === string;
};
};
Resurrect.isArray = Resurrect.is('Array');
Resurrect.isString = Resurrect.is('String');
Resurrect.isBoolean = Resurrect.is('Boolean');
Resurrect.isNumber = Resurrect.is('Number');
Resurrect.isFunction = Resurrect.is('Function');
Resurrect.isDate = Resurrect.is('Date');
Resurrect.isRegExp = Resurrect.is('RegExp');
Resurrect.isObject = Resurrect.is('Object');
Resurrect.isAtom = function(object) {
return !Resurrect.isObject(object) && !Resurrect.isArray(object);
};
/* Methods */
/**
* Create a reference (encoding) to an object.
* @method
*/
Resurrect.prototype.ref = function(object) {
var ref = {};
if (object === undefined) {
ref[this.prefix] = -1;
} else {
ref[this.prefix] = object[this.refcode];
}
return ref;
};
/**
* Lookup an object in the table by reference object.
* @method
*/
Resurrect.prototype.deref = function(ref) {
return this.table[ref[this.prefix]];
};
/**
* Put a temporary identifier on an object and store it in the table.
* @returns {number} The unique identifier number.
* @method
*/
Resurrect.prototype.tag = function(object) {
if (this.revive) {
var constructor = this.resolver.getName(object);
if (constructor) {
var proto = Object.getPrototypeOf(object);
if (this.resolver.getPrototype(constructor) !== proto) {
throw new this.Error('Constructor mismatch!');
} else {
object[this.prefix] = constructor;
}
}
}
object[this.refcode] = this.table.length;
this.table.push(object);
return object[this.refcode];
};
/**
* Create a builder object (encoding) for serialization.
* @param {string} name The name of the constructor.
* @param value The value to pass to the constructor.
* @method
*/
Resurrect.prototype.builder = function(name, value) {
var builder = {};
builder[this.buildcode] = name;
builder[this.valuecode] = value;
return builder;
};
/**
* Build a value from a deserialized builder.
* @method
* @see http://stackoverflow.com/a/14378462
*/
Resurrect.prototype.build = function(ref) {
var type = ref[this.buildcode].split(/\./).reduce(function(object, name) {
return object[name];
}, window);
/* Brilliant hack by kybernetikos: */
var args = [null].concat(ref[this.valuecode]);
var factory = type.bind.apply(type, args);
return new factory();
};
/**
* Dereference or build an object or value from an encoding.
* @method
*/
Resurrect.prototype.decode = function(ref) {
if (this.prefix in ref) {
return this.deref(ref);
} else if (this.buildcode in ref) {
return this.build(ref);
} else {
throw new this.Error('Unknown encoding.');
}
};
/**
* @returns True if the provided object is already tagged for serialization.
* @method
*/
Resurrect.prototype.isTagged = function(object) {
return (this.refcode in object) && (object[this.refcode] != null);
};
/**
* Visit root and all its ancestors, visiting atoms with f.
* @param {Function} f
* @returns A fresh copy of root to be serialized.
* @method
*/
Resurrect.prototype.visit = function(root, f) {
if (Resurrect.isAtom(root)) {
return f(root);
} else if (!this.isTagged(root)) {
var copy = null;
if (Resurrect.isArray(root)) {
copy = [];
root[this.refcode] = this.tag(copy);
for (var i = 0; i < root.length; i++) {
copy.push(this.visit(root[i], f));
}
} else { /* Object */
copy = Object.create(Object.getPrototypeOf(root));
root[this.refcode] = this.tag(copy);
for (var key in root) {
if (root.hasOwnProperty(key)) {
copy[key] = this.visit(root[key], f);
}
}
}
copy[this.origcode] = root;
return this.ref(copy);
} else {
return this.ref(root);
}
};
/**
* Manage special atom values, possibly returning an encoding.
*/
Resurrect.prototype.handleAtom = function(atom) {
if (Resurrect.isFunction(atom)) {
throw new this.Error("Can't serialize functions.");
} else if (atom instanceof Node) {
var xmls = new XMLSerializer();
return this.builder('Resurrect.Node', [xmls.serializeToString(atom)]);
} else if (Resurrect.isDate(atom)) {
return this.builder('Date', [atom.toISOString()]);
} else if (Resurrect.isRegExp(atom)) {
var args = atom.toString().match(/\/(.+)\/([gimy]*)/).slice(1);
return this.builder('RegExp', args);
} else if (atom === undefined) {
return this.ref(undefined);
} else {
return atom;
}
};
/**
* Serialize an arbitrary JavaScript object, carefully preserving it.
* @method
*/
Resurrect.prototype.stringify = function(object) {
if (Resurrect.isAtom(object)) {
return JSON.stringify(this.handleAtom(object));
} else {
this.table = [];
this.visit(object, this.handleAtom.bind(this));
for (var i = 0; i < this.table.length; i++) {
if (this.cleanup) {
delete this.table[i][this.origcode][this.refcode];
} else {
this.table[i][this.origcode][this.refcode] = null;
}
delete this.table[i][this.refcode];
delete this.table[i][this.origcode];
}
var table = this.table;
this.table = null;
return JSON.stringify(table);
}
};
/**
* Restore the __proto__ of the given object to the proper value.
* @returns The object.
*/
Resurrect.prototype.fixPrototype = function(object) {
if (this.prefix in object) {
var name = object[this.prefix];
var prototype = this.resolver.getPrototype(name);
if ('__proto__' in object) {
object.__proto__ = prototype;
if (this.cleanup) {
delete object[this.prefix];
}
return object;
} else { // IE
var copy = Object.create(prototype);
for (var key in object) {
if (object.hasOwnProperty(key) && key !== this.prefix) {
copy[key] = object[key];
}
}
return copy;
}
} else {
return object;
}
};
/**
* Deserialize an encoded object, restoring circularity and behavior.
* @param {string} string
* @returns The decoded object or value.
* @method
*/
Resurrect.prototype.resurrect = function(string) {
var result = null;
var data = JSON.parse(string);
if (Resurrect.isArray(data)) {
this.table = data;
/* Restore __proto__. */
if (this.revive) {
for (var i = 0; i < this.table.length; i++) {
this.table[i] = this.fixPrototype(this.table[i]);
}
}
/* Re-establish object references and construct atoms. */
for (i = 0; i < this.table.length; i++) {
var object = this.table[i];
for (var key in object) {
if (object.hasOwnProperty(key)) {
if (!(Resurrect.isAtom(object[key]))) {
object[key] = this.decode(object[key]);
}
}
}
}
result = this.table[0];
} else if (Resurrect.isObject(data)) {
this.table = [];
result = this.decode(data);
} else {
result = data;
}
this.table = null;
return result;
};

30
demo/mobile.html Normal file
View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="robots" content="noindex">
<script type="text/javascript" src="./js/lib/matter.js"></script>
<script type="text/javascript" src="./js/DemoMobile.js"></script>
<link rel="stylesheet" href="./css/style.css" type="text/css">
<title>Matter.js Physics Engine Mobile Demo</title>
</head>
<body class="mobile">
<div class="container">
<h1>Matter.js Mobile Demo</h1>
<p>
Open this page on your mobile device in Chrome or Safari
</p>
<p>
Touch and drag to interact with the blocks
<br>Tilt and turn to master gravity like you're the boss of it
</p>
<input id="demo-start" value="Start Demo" type="submit">
<p>Or back to the <a href="index.html">full demo</a> page for more info.</p>
<div id="canvas-container"></div>
</div>
</body>
</html>

19
package.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "matter",
"version": "0.5.0",
"license": "MIT",
"homepage": "http://brm.io/matter-js/",
"author": "Liam Brummitt <liam@brm.io> (http://brm.io/)",
"devDependencies": {
"grunt": "~0.4.2",
"grunt-contrib-jshint": "~0.6.3",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-connect": "~0.6.0",
"grunt-contrib-copy": "~0.5.0",
"grunt-contrib-watch": "~0.5.3"
},
"scripts": {
"dev": "npm install && grunt dev"
}
}

176
src/body/Body.js Normal file
View file

@ -0,0 +1,176 @@
var Body = {};
(function() {
var _nextId = 0,
_nextGroupId = 1;
Body.create = function(options) {
var defaults = {
id: Body.nextId(),
angle: 0,
position: { x: 0, y: 0 },
force: { x: 0, y: 0 },
torque: 0,
positionImpulse: { x: 0, y: 0 },
speed: 0,
angularSpeed: 0,
velocity: { x: 0, y: 0 },
angularVelocity: 0,
isStatic: false,
isSleeping: false,
motion: 0,
sleepThreshold: 60,
density: 0.001,
restitution: 0,
friction: 0.1,
frictionAir: 0.01,
path: 'L 0 0 L 40 0 L 40 40 L 0 40',
fillStyle: options.isStatic ? '#eeeeee' : Common.choose(['#556270', '#4ECDC4', '#C7F464', '#FF6B6B', '#C44D58']),
lineWidth: 1.5,
groupId: 0,
slop: 0.05
};
var body = Common.extend(defaults, options);
Body.updateProperties(body);
return body;
};
Body.nextId = function() {
return _nextId++;
};
Body.nextGroupId = function() {
return _nextGroupId++;
};
Body.updateProperties = function(body) {
// calculated properties
body.vertices = body.vertices || Vertices.fromPath(body.path);
body.axes = body.axes || Axes.fromVertices(body.vertices);
body.area = Vertices.area(body.vertices);
body.bounds = Bounds.create(body.vertices);
body.mass = body.mass || (body.density * body.area);
body.inverseMass = 1 / body.mass;
body.inertia = body.inertia || Vertices.inertia(body.vertices, body.mass);
body.inverseInertia = 1 / body.inertia;
body.positionPrev = body.positionPrev || { x: body.position.x, y: body.position.y };
body.anglePrev = body.anglePrev || body.angle;
body.strokeStyle = body.strokeStyle || Common.shadeColor(body.fillStyle, -20);
// update geometry
Vertices.create(body.vertices, body);
var centre = Vertices.centre(body.vertices);
Vertices.translate(body.vertices, body.position);
Vertices.translate(body.vertices, centre, -1);
Vertices.rotate(body.vertices, body.angle, body.position);
Axes.rotate(body.axes, body.angle);
Bounds.update(body.bounds, body.vertices, body.velocity);
if (body.isStatic) {
body.restitution = 0;
body.friction = 1;
body.mass = body.inertia = body.density = Infinity;
body.inverseMass = body.inverseInertia = 0;
body.lineWidth = 1;
}
Sleeping.set(body, body.isSleeping);
};
Body.resetForcesAll = function(bodies, gravity) {
for (var i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (body.isStatic || body.isSleeping)
continue;
// reset force buffers
body.force.x = 0;
body.force.y = 0;
body.torque = 0;
// apply gravity
body.force.y += body.mass * gravity.y * 0.001;
body.force.x += body.mass * gravity.x * 0.001;
}
};
Body.updateAll = function(bodies, deltaTime, correction, worldBounds) {
for (var i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (body.isStatic || body.isSleeping)
continue;
// don't update out of world bodies
// TODO: viewports
if (body.bounds.max.x < worldBounds.min.x || body.bounds.min.x > worldBounds.max.x
|| body.bounds.max.y < worldBounds.min.y || body.bounds.min.y > worldBounds.max.y)
continue;
Body.update(body, deltaTime, correction);
}
};
Body.update = function(body, deltaTime, correction) {
var deltaTimeSquared = deltaTime * deltaTime;
// from the previous step
var frictionAir = 1 - body.frictionAir,
velocityPrevX = body.position.x - body.positionPrev.x,
velocityPrevY = body.position.y - body.positionPrev.y;
// update velocity with verlet integration
body.velocity.x = (velocityPrevX * frictionAir * correction) + (body.force.x / body.mass) * deltaTimeSquared;
body.velocity.y = (velocityPrevY * frictionAir * correction) + (body.force.y / body.mass) * deltaTimeSquared;
body.positionPrev.x = body.position.x;
body.positionPrev.y = body.position.y;
body.position.x += body.velocity.x;
body.position.y += body.velocity.y;
// update angular velocity with verlet integration
body.angularVelocity = ((body.angle - body.anglePrev) * frictionAir * correction) + (body.torque / body.inertia) * deltaTimeSquared;
body.anglePrev = body.angle;
body.angle += body.angularVelocity;
// track speed and acceleration
body.speed = Vector.magnitude(body.velocity);
body.angularSpeed = Math.abs(body.angularVelocity);
// transform the body geometry
Vertices.translate(body.vertices, body.velocity);
Vertices.rotate(body.vertices, body.angularVelocity, body.position);
Axes.rotate(body.axes, body.angularVelocity);
Bounds.update(body.bounds, body.vertices, body.velocity);
};
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;
};
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);
};
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);
};
})();

36
src/body/Composite.js Normal file
View file

@ -0,0 +1,36 @@
// TODO: composite translate, rotate
var Composite = {};
(function() {
Composite.create = function(options) {
return Common.extend({ bodies: [], constraints: [], composites: [] }, options);
};
Composite.add = function(compositeA, compositeB) {
if (compositeA.bodies && compositeB.bodies)
compositeA.bodies = compositeA.bodies.concat(compositeB.bodies);
if (compositeA.constraints && compositeB.constraints)
compositeA.constraints = compositeA.constraints.concat(compositeB.constraints);
if (compositeA.composites && compositeB.composites)
compositeA.composites = compositeA.composites.concat(compositeB.composites);
return compositeA;
};
Composite.addBody = function(composite, body) {
composite.bodies = composite.bodies || [];
composite.bodies.push(body);
return composite;
};
Composite.addConstraint = function(composite, constraint) {
composite.constraints = composite.constraints || [];
composite.constraints.push(constraint);
return composite;
};
})();

31
src/body/World.js Normal file
View file

@ -0,0 +1,31 @@
var World = {};
(function() {
World.create = function(options) {
var defaults = {
gravity: { x: 0, y: 1 },
bodies: [],
constraints: [],
bounds: {
min: { x: 0, y: 0 },
max: { x: 800, y: 600 }
}
};
return Common.extend(defaults, options);
};
World.clear = function(world, keepStatic) {
world.bodies = keepStatic ? world.bodies.filter(function(body) { return body.isStatic; }) : [];
world.constraints = [];
};
// world is a composite body
// see src/module/Outro.js for these aliases:
// World.addComposite = Composite.add;
// World.addBody = Composite.addBody;
// World.addConstraint = Composite.addConstraint;
})();

18
src/collision/Contact.js Normal file
View file

@ -0,0 +1,18 @@
var Contact = {};
(function() {
Contact.create = function(vertex) {
return {
id: Contact.id(vertex),
vertex: vertex,
normalImpulse: 0,
tangentImpulse: 0
};
};
Contact.id = function(vertex) {
return vertex.body.id + '_' + vertex.index;
};
})();

79
src/collision/Detector.js Normal file
View file

@ -0,0 +1,79 @@
// TODO: speculative contacts
var Detector = {};
(function() {
Detector.collisions = function(pairs, metrics) {
var collisions = [];
for (var i = 0; i < pairs.length; i++) {
var bodyA = pairs[i][0],
bodyB = pairs[i][1];
// NOTE: could share a function for the below, but may drop performance?
if (bodyA.groupId && bodyB.groupId && bodyA.groupId === bodyB.groupId)
continue;
if ((bodyA.isStatic || bodyA.isSleeping) && (bodyB.isStatic || bodyB.isSleeping))
continue;
metrics.midphaseTests += 1;
// mid phase
if (Bounds.overlaps(bodyA.bounds, bodyB.bounds)) {
// narrow phase
var collision = SAT.collides(bodyA, bodyB);
metrics.narrowphaseTests += 1;
if (collision.collided) {
collisions.push(collision);
metrics.narrowDetections += 1;
}
}
}
return collisions;
};
Detector.bruteForce = function(bodies, metrics) {
var collisions = [];
for (var i = 0; i < bodies.length; i++) {
for (var j = i + 1; j < bodies.length; j++) {
var bodyA = bodies[i],
bodyB = bodies[j];
// NOTE: could share a function for the below, but may drop performance?
if (bodyA.groupId && bodyB.groupId && bodyA.groupId === bodyB.groupId)
continue;
if ((bodyA.isStatic || bodyA.isSleeping) && (bodyB.isStatic || bodyB.isSleeping))
continue;
metrics.midphaseTests += 1;
// mid phase
if (Bounds.overlaps(bodyA.bounds, bodyB.bounds)) {
// narrow phase
var collision = SAT.collides(bodyA, bodyB);
metrics.narrowphaseTests += 1;
if (collision.collided) {
collisions.push(collision);
metrics.narrowDetections += 1;
}
}
}
}
return collisions;
};
})();

208
src/collision/Grid.js Normal file
View file

@ -0,0 +1,208 @@
var Grid = {};
(function() {
Grid.create = function(bucketWidth, bucketHeight) {
return {
buckets: {},
pairs: {},
pairsList: [],
bucketWidth: bucketWidth || 48,
bucketHeight: bucketHeight || 48
};
};
Grid.update = function(grid, bodies, engine, forceUpdate) {
var i, col, row,
world = engine.world,
buckets = grid.buckets,
bucket,
bucketId,
metrics = engine.metrics,
gridChanged = false;
metrics.broadphaseTests = 0;
for (i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (body.isSleeping)
continue;
// don't update out of world bodies
if (body.bounds.max.x < 0 || body.bounds.min.x > world.bounds.width
|| body.bounds.max.y < 0 || body.bounds.min.y > world.bounds.height)
continue;
var newRegion = _getRegion(grid, body);
// if the body has changed grid region
if (!body.region || newRegion.id !== body.region.id || forceUpdate) {
metrics.broadphaseTests += 1;
if (!body.region || forceUpdate)
body.region = newRegion;
var union = _regionUnion(newRegion, body.region);
// update grid buckets affected by region change
// iterate over the union of both regions
for (col = union.startCol; col <= union.endCol; col++) {
for (row = union.startRow; row <= union.endRow; row++) {
bucketId = _getBucketId(col, row);
bucket = buckets[bucketId];
var isInsideNewRegion = (col >= newRegion.startCol && col <= newRegion.endCol
&& row >= newRegion.startRow && row <= newRegion.endRow);
var isInsideOldRegion = (col >= body.region.startCol && col <= body.region.endCol
&& row >= body.region.startRow && row <= body.region.endRow);
// remove from old region buckets
if (!isInsideNewRegion && isInsideOldRegion) {
if (isInsideOldRegion) {
if (bucket)
_bucketRemoveBody(grid, bucket, body);
}
}
// add to new region buckets
if (body.region === newRegion || (isInsideNewRegion && !isInsideOldRegion) || forceUpdate) {
if (!bucket)
bucket = _createBucket(buckets, bucketId);
_bucketAddBody(grid, bucket, body);
}
}
}
// set the new region
body.region = newRegion;
// flag changes so we can update pairs
gridChanged = true;
}
}
// update pairs list only if pairs changed (i.e. a body changed region)
if (gridChanged)
grid.pairsList = _createActivePairsList(grid);
};
Grid.clear = function(grid) {
grid.buckets = {};
grid.pairs = {};
grid.pairsList = [];
};
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);
};
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);
};
var _createRegion = function(startCol, endCol, startRow, endRow) {
return {
id: startCol + ',' + endCol + ',' + startRow + ',' + endRow,
startCol: startCol,
endCol: endCol,
startRow: startRow,
endRow: endRow
};
};
var _getBucketId = function(column, row) {
return column + ',' + row;
};
var _createBucket = function(buckets, bucketId) {
var bucket = buckets[bucketId] = [];
return bucket;
};
var _bucketAddBody = function(grid, bucket, body) {
// add new pairs
for (var i = 0; i < bucket.length; i++) {
var bodyB = bucket[i];
if (body.id === bodyB.id || (body.isStatic && bodyB.isStatic))
continue;
// keep track of the number of buckets the pair exists in
// important for Grid.update to work
var pairId = _getPairId(body, bodyB);
if (grid.pairs[pairId]) {
grid.pairs[pairId][2] += 1;
} else {
grid.pairs[pairId] = [body, bodyB, 1];
}
}
// add to bodies (after pairs, otherwise pairs with self)
bucket.push(body);
};
var _bucketRemoveBody = function(grid, bucket, body) {
// remove from bodies
for (var i = 0; i < bucket.length; i++) {
var bodyB = bucket[i];
// when position of body in bucket array is found, remove it
if (bodyB.id === body.id) {
bucket.splice(i, 1);
continue;
} else {
// keep track of the number of buckets the pair exists in
// important for Grid.update to work
var pairId = _getPairId(body, bodyB);
if (grid.pairs[pairId]) {
var pairCount = grid.pairs[pairId][2];
grid.pairs[pairId][2] = pairCount > 0 ? pairCount - 1 : 0;
}
}
}
};
var _getPairId = function(bodyA, bodyB) {
if (bodyA.id < bodyB.id) {
return bodyA.id + ',' + bodyB.id;
} else {
return bodyB.id + ',' + bodyA.id;
}
};
var _createActivePairsList = function(grid) {
var pairKeys,
pair,
pairs = [];
// grid.pairs is used as a hashmap
pairKeys = Common.keys(grid.pairs);
// iterate over grid.pairs
for (var k = 0; k < pairKeys.length; k++) {
pair = grid.pairs[pairKeys[k]];
// if pair exists in at least one bucket
// it is a pair that needs further collision testing so push it
if (pair[2] > 0)
pairs.push(pair);
}
return pairs;
};
})();

61
src/collision/Manager.js Normal file
View file

@ -0,0 +1,61 @@
var Manager = {};
(function() {
var _pairMaxIdleLife = 500;
Manager.updatePairs = function(pairs, pairsList, candidatePairs, metrics, detector) {
var i;
// first set all pairs inactive
for (i = 0; i < pairsList.length; i++) {
var pair = pairsList[i];
Pair.setActive(pair, false);
}
// detect collisions in current step
var pairsUpdated = false,
collisions = detector(candidatePairs, metrics);
// set collision pairs to active, or create if pair is new
for (i = 0; i < collisions.length; i++) {
var collision = collisions[i],
pairId = Pair.id(collision.bodyA, collision.bodyB);
if (pairId in pairs) {
Pair.update(pairs[pairId], collision);
} else {
pairs[pairId] = Pair.create(collision);
pairsUpdated = true;
}
}
return pairsUpdated;
};
Manager.removeOldPairs = function(pairs, pairsList) {
var timeNow = Common.now(),
pairsRemoved = false,
i;
for (i = 0; i < pairsList.length; i++) {
var pair = pairsList[i],
collision = pair.collision;
// never remove sleeping pairs
if (collision.bodyA.isSleeping || collision.bodyB.isSleeping) {
pair.timestamp = timeNow;
continue;
}
// if pair is inactive for too long, remove it
if (timeNow - pair.timestamp > _pairMaxIdleLife) {
delete pairs[pair.id];
pairsRemoved = true;
}
}
return pairsRemoved;
};
})();

72
src/collision/Pair.js Normal file
View file

@ -0,0 +1,72 @@
var Pair = {};
(function() {
Pair.create = function(collision) {
var bodyA = collision.bodyA,
bodyB = collision.bodyB;
var pair = {
id: Pair.id(bodyA, bodyB),
contacts: {},
activeContacts: [],
separation: 0,
isActive: true,
timestamp: Common.now(),
inverseMass: bodyA.inverseMass + bodyB.inverseMass,
friction: Math.min(bodyA.friction, bodyB.friction),
restitution: Math.max(bodyA.restitution, bodyB.restitution),
slop: Math.max(bodyA.slop, bodyB.slop)
};
Pair.update(pair, collision);
return pair;
};
Pair.update = function(pair, collision) {
var contacts = pair.contacts,
supports = collision.supports,
activeContacts = [];
pair.collision = collision;
if (collision.collided) {
for (var i = 0; i < supports.length; i++) {
var support = supports[i],
contactId = Contact.id(support);
if (contactId in contacts) {
activeContacts.push(contacts[contactId]);
} else {
activeContacts.push(contacts[contactId] = Contact.create(support));
}
}
pair.activeContacts = activeContacts;
pair.separation = collision.depth;
Pair.setActive(pair, true);
} else {
Pair.setActive(pair, false);
}
};
Pair.setActive = function(pair, isActive) {
if (isActive) {
pair.isActive = true;
pair.timestamp = Common.now();
} else {
pair.isActive = false;
pair.activeContacts = [];
}
};
Pair.id = function(bodyA, bodyB) {
if (bodyA.id < bodyB.id) {
return bodyA.id + '_' + bodyB.id;
} else {
return bodyB.id + '_' + bodyA.id;
}
};
})();

244
src/collision/Resolver.js Normal file
View file

@ -0,0 +1,244 @@
var Resolver = {};
(function() {
var _restingThresh = 4,
_positionDampen = 0.2,
_positionWarming = 0.6;
Resolver.solvePosition = function(pairs) {
var i,
pair,
collision,
bodyA,
bodyB,
vertex,
vertexCorrected,
normal,
bodyBtoA;
// find impulses required to resolve penetration
for (i = 0; i < pairs.length; i++) {
pair = pairs[i];
if (!pair.isActive)
continue;
collision = pair.collision;
bodyA = collision.bodyA;
bodyB = collision.bodyB;
vertex = collision.supports[0];
vertexCorrected = collision.supportCorrected;
normal = collision.normal;
// get current separation between body edges involved in collision
bodyBtoA = Vector.sub(Vector.add(bodyB.positionImpulse, vertex),
Vector.add(bodyA.positionImpulse, vertexCorrected));
pair.separation = Vector.dot(normal, bodyBtoA);
}
for (i = 0; i < pairs.length; i++) {
pair = pairs[i];
if (!pair.isActive)
continue;
collision = pair.collision;
bodyA = collision.bodyA;
bodyB = collision.bodyB;
normal = collision.normal;
positionImpulse = (pair.separation * _positionDampen) - pair.slop;
if (bodyA.isStatic || bodyB.isStatic)
positionImpulse *= 2;
if (!(bodyA.isStatic || bodyA.isSleeping)) {
bodyA.positionImpulse.x += normal.x * positionImpulse;
bodyA.positionImpulse.y += normal.y * positionImpulse;
}
if (!(bodyB.isStatic || bodyB.isSleeping)) {
bodyB.positionImpulse.x -= normal.x * positionImpulse;
bodyB.positionImpulse.y -= normal.y * positionImpulse;
}
}
};
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;
}
}
};
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;
}
}
}
};
Resolver.solveVelocity = function(pairs) {
var impulse = {};
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i];
if (!pair.isActive)
continue;
var collision = pair.collision,
bodyA = collision.bodyA,
bodyB = collision.bodyB,
normal = collision.normal,
tangent = collision.tangent,
contacts = pair.activeContacts,
contactShare = 1 / contacts.length;
// update body velocities
bodyA.velocity.x = bodyA.position.x - bodyA.positionPrev.x;
bodyA.velocity.y = bodyA.position.y - bodyA.positionPrev.y;
bodyB.velocity.x = bodyB.position.x - bodyB.positionPrev.x;
bodyB.velocity.y = bodyB.position.y - bodyB.positionPrev.y;
bodyA.angularVelocity = bodyA.angle - bodyA.anglePrev;
bodyB.angularVelocity = bodyB.angle - bodyB.anglePrev;
// resolve each contact
for (var j = 0; j < contacts.length; j++) {
var contact = contacts[j],
contactVertex = contact.vertex,
offsetA = Vector.sub(contactVertex, bodyA.position),
offsetB = Vector.sub(contactVertex, bodyB.position),
velocityPointA = Vector.add(bodyA.velocity, Vector.mult(Vector.perp(offsetA), bodyA.angularVelocity)),
velocityPointB = Vector.add(bodyB.velocity, Vector.mult(Vector.perp(offsetB), bodyB.angularVelocity)),
relativeVelocity = Vector.sub(velocityPointA, velocityPointB),
normalVelocity = Vector.dot(normal, relativeVelocity);
var tangentVelocity = Vector.dot(tangent, relativeVelocity),
tangentSpeed = Math.abs(tangentVelocity),
tangentVelocityDirection = Common.sign(tangentVelocity);
// raw impulses
var normalImpulse = (1 + pair.restitution) * normalVelocity,
normalForce = Common.clamp(pair.separation + normalVelocity, 0, 1);
// coulomb friction
var tangentImpulse = tangentVelocity;
if (tangentSpeed > normalForce * pair.friction)
tangentImpulse = normalForce * pair.friction * tangentVelocityDirection;
// modify impulses accounting for mass, inertia and offset
var oAcN = Vector.cross(offsetA, normal),
oBcN = Vector.cross(offsetB, normal),
share = contactShare / (pair.inverseMass + bodyA.inverseInertia * oAcN * oAcN + bodyB.inverseInertia * oBcN * oBcN);
normalImpulse *= share;
tangentImpulse *= share;
// handle high velocity and resting collisions separately
if (normalVelocity < 0 && normalVelocity * normalVelocity > _restingThresh) {
// high velocity so clear cached contact impulse
contact.normalImpulse = 0;
contact.tangentImpulse = 0;
} else {
// solve resting collision constraints using Erin Catto's method (GDC08)
// impulse constraint, tends to 0
var contactNormalImpulse = contact.normalImpulse;
contact.normalImpulse = Math.min(contact.normalImpulse + normalImpulse, 0);
normalImpulse = contact.normalImpulse - contactNormalImpulse;
// tangent impulse, tends to -maxFriction or maxFriction
var contactTangentImpulse = contact.tangentImpulse;
contact.tangentImpulse = Common.clamp(contact.tangentImpulse + tangentImpulse, -tangentSpeed, tangentSpeed);
tangentImpulse = contact.tangentImpulse - contactTangentImpulse;
}
// total impulse from contact
impulse.x = (normal.x * normalImpulse) + (tangent.x * tangentImpulse);
impulse.y = (normal.y * normalImpulse) + (tangent.y * tangentImpulse);
// apply impulse from contact
if (!(bodyA.isStatic || bodyA.isSleeping)) {
bodyA.positionPrev.x += impulse.x * bodyA.inverseMass;
bodyA.positionPrev.y += impulse.y * bodyA.inverseMass;
bodyA.anglePrev += Vector.cross(offsetA, impulse) * bodyA.inverseInertia;
}
if (!(bodyB.isStatic || bodyB.isSleeping)) {
bodyB.positionPrev.x -= impulse.x * bodyB.inverseMass;
bodyB.positionPrev.y -= impulse.y * bodyB.inverseMass;
bodyB.anglePrev -= Vector.cross(offsetB, impulse) * bodyB.inverseInertia;
}
}
}
};
})();

165
src/collision/SAT.js Normal file
View file

@ -0,0 +1,165 @@
// TODO: true circles and curves
// TODO: cache the previously found axis and body, and start there first for faster early out
var SAT = {};
(function() {
SAT.collides = function(bodyA, bodyB) {
var overlapAB,
overlapBA,
minOverlap,
collision = { collided: false, bodyA: bodyA, bodyB: bodyB};
overlapAB = _overlapAxes(bodyA.vertices, bodyB.vertices, bodyA.axes);
if (overlapAB.overlap === 0)
return collision;
overlapBA = _overlapAxes(bodyB.vertices, bodyA.vertices, bodyB.axes);
if (overlapBA.overlap === 0)
return collision;
if (overlapAB.overlap < overlapBA.overlap) {
minOverlap = overlapAB;
} else {
minOverlap = overlapBA;
collision.bodyA = bodyB;
collision.bodyB = bodyA;
}
collision.collided = true;
collision.normal = minOverlap.axis;
collision.depth = minOverlap.overlap;
bodyA = collision.bodyA;
bodyB = collision.bodyB;
// ensure normal is facing away from bodyA
if (Vector.dot(collision.normal, Vector.sub(bodyB.position, bodyA.position)) > 0)
collision.normal = Vector.neg(collision.normal);
collision.tangent = Vector.perp(collision.normal);
collision.penetration = {
x: collision.normal.x * collision.depth,
y: collision.normal.y * collision.depth
};
// find support points, there is always either exactly one or two
var verticesB = _findSupports(bodyA, bodyB, collision.normal),
supports = [verticesB[0]];
if (Vertices.contains(bodyA.vertices, verticesB[1])) {
supports.push(verticesB[1]);
} else {
var verticesA = _findSupports(bodyB, bodyA, Vector.neg(collision.normal));
if (Vertices.contains(bodyB.vertices, verticesA[0])) {
supports.push(verticesA[0]);
}
if (supports.length < 2 && Vertices.contains(bodyB.vertices, verticesA[1])) {
supports.push(verticesA[1]);
}
}
collision.supports = supports;
collision.supportCorrected = Vector.sub(verticesB[0], collision.penetration);
return collision;
};
var _overlapAxes = function(verticesA, verticesB, axes) {
var projectionA = {},
projectionB = {},
result = { overlap: Number.MAX_VALUE },
overlap,
axis;
for (var i = 0; i < axes.length; i++) {
axis = axes[i];
_projectToAxis(projectionA, verticesA, axis);
_projectToAxis(projectionB, verticesB, axis);
overlap = projectionA.min < projectionB.min
? projectionA.max - projectionB.min
: projectionB.max - projectionA.min;
if (overlap <= 0)
return { overlap: 0 };
if (overlap < result.overlap) {
result.overlap = overlap;
result.axis = axis;
}
}
return result;
};
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;
};
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];
};
})();

View file

@ -0,0 +1,194 @@
// 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;
Constraint.create = function(options) {
var constraint = options;
// if bodies defined but no points, use body centre
if (constraint.bodyA && !constraint.pointA)
constraint.pointA = { x: 0, y: 0 };
if (constraint.bodyB && !constraint.pointB)
constraint.pointB = { x: 0, y: 0 };
// calculate static length using initial world space points
var initialPointA = constraint.bodyA ? Vector.add(constraint.bodyA.position, constraint.pointA) : constraint.pointA,
initialPointB = constraint.bodyB ? Vector.add(constraint.bodyB.position, constraint.pointB) : constraint.pointB,
length = Vector.magnitude(Vector.sub(initialPointA, initialPointB));
constraint.length = constraint.length || length || _minLength;
// option defaults
constraint.lineWidth = constraint.lineWidth || 2;
constraint.strokeStyle = constraint.strokeStyle || '#666';
constraint.stiffness = constraint.stiffness || 1;
constraint.angularStiffness = constraint.angularStiffness || 0;
constraint.angleA = constraint.bodyA ? constraint.bodyA.angle : constraint.angleA;
constraint.angleB = constraint.bodyB ? constraint.bodyB.angle : constraint.angleB;
return constraint;
};
Constraint.updateAll = function(constraints) {
for (var i = 0; i < constraints.length; i++) {
Constraint.update(constraints[i]);
}
};
Constraint.update = function(constraint) {
var bodyA = constraint.bodyA,
bodyB = constraint.bodyB,
pointA = constraint.pointA,
pointB = constraint.pointB;
// update reference angle
if (bodyA && !bodyA.isStatic) {
constraint.pointA = Vector.rotate(pointA, bodyA.angle - constraint.angleA);
constraint.angleA = bodyA.angle;
}
// update reference angle
if (bodyB && !bodyB.isStatic) {
constraint.pointB = Vector.rotate(pointB, bodyB.angle - constraint.angleB);
constraint.angleB = bodyB.angle;
}
var pointAWorld = pointA,
pointBWorld = pointB;
if (bodyA) pointAWorld = Vector.add(bodyA.position, pointA);
if (bodyB) pointBWorld = Vector.add(bodyB.position, pointB);
if (!pointAWorld || !pointBWorld)
return;
var delta = Vector.sub(pointAWorld, pointBWorld),
currentLength = Vector.magnitude(delta);
// prevent singularity
if (currentLength === 0)
currentLength = _minLength;
// solve distance constraint with Gauss-Siedel method
var difference = (currentLength - constraint.length) / currentLength,
normal = Vector.div(delta, currentLength),
force = Vector.mult(delta, difference * 0.5 * constraint.stiffness);
var velocityPointA,
velocityPointB,
offsetA,
offsetB,
oAn,
oBn,
bodyADenom,
bodyBDenom;
if (bodyA && !bodyA.isStatic) {
// point body offset
offsetA = {
x: pointAWorld.x - bodyA.position.x + force.x,
y: pointAWorld.y - bodyA.position.y + force.y
};
// update velocity
bodyA.velocity.x = bodyA.position.x - bodyA.positionPrev.x;
bodyA.velocity.y = bodyA.position.y - bodyA.positionPrev.y;
bodyA.angularVelocity = bodyA.angle - bodyA.anglePrev;
// find point velocity and body mass
velocityPointA = Vector.add(bodyA.velocity, Vector.mult(Vector.perp(offsetA), bodyA.angularVelocity));
oAn = Vector.dot(offsetA, normal);
bodyADenom = bodyA.inverseMass + bodyA.inverseInertia * oAn * oAn;
} else {
velocityPointA = { x: 0, y: 0 };
bodyADenom = bodyA ? bodyA.inverseMass : 0;
}
if (bodyB && !bodyB.isStatic) {
// point body offset
offsetB = {
x: pointBWorld.x - bodyB.position.x - force.x,
y: pointBWorld.y - bodyB.position.y - force.y
};
// update velocity
bodyB.velocity.x = bodyB.position.x - bodyB.positionPrev.x;
bodyB.velocity.y = bodyB.position.y - bodyB.positionPrev.y;
bodyB.angularVelocity = bodyB.angle - bodyB.anglePrev;
// find point velocity and body mass
velocityPointB = Vector.add(bodyB.velocity, Vector.mult(Vector.perp(offsetB), bodyB.angularVelocity));
oBn = Vector.dot(offsetB, normal);
bodyBDenom = bodyB.inverseMass + bodyB.inverseInertia * oBn * oBn;
} else {
velocityPointB = { x: 0, y: 0 };
bodyBDenom = bodyB ? bodyB.inverseMass : 0;
}
var relativeVelocity = Vector.sub(velocityPointB, velocityPointA),
normalImpulse = Vector.dot(normal, relativeVelocity) / (bodyADenom + bodyBDenom);
if (normalImpulse > 0) normalImpulse = 0;
var normalVelocity = {
x: normal.x * normalImpulse,
y: normal.y * normalImpulse
};
var torque;
if (bodyA && !bodyA.isStatic) {
torque = Vector.cross(offsetA, normalVelocity) * bodyA.inverseInertia * (1 - constraint.angularStiffness);
Sleeping.set(bodyA, false);
// clamp to prevent instabillity
// TODO: solve this properlly
torque = Common.clamp(torque, -0.01, 0.01);
// apply forces
bodyA.position.x -= force.x;
bodyA.position.y -= force.y;
bodyA.angle += torque;
// update geometry
Vertices.translate(bodyA.vertices, force, -1);
Vertices.rotate(bodyA.vertices, torque, bodyA.position);
Axes.rotate(bodyA.axes, torque);
Bounds.update(bodyA.bounds, bodyA.vertices, bodyA.velocity);
}
if (bodyB && !bodyB.isStatic) {
torque = Vector.cross(offsetB, normalVelocity) * bodyB.inverseInertia * (1 - constraint.angularStiffness);
Sleeping.set(bodyB, false);
// clamp to prevent instabillity
// TODO: solve this properlly
torque = Common.clamp(torque, -0.01, 0.01);
// apply forces
bodyB.position.x += force.x;
bodyB.position.y += force.y;
bodyB.angle -= torque;
// update geometry
Vertices.translate(bodyB.vertices, force);
Vertices.rotate(bodyB.vertices, -torque, bodyB.position);
Axes.rotate(bodyB.axes, -torque);
Bounds.update(bodyB.bounds, bodyB.vertices, bodyB.velocity);
}
};
})();

View file

@ -0,0 +1,53 @@
var MouseConstraint = {};
(function() {
MouseConstraint.create = function(mouse) {
var constraint = Constraint.create({
pointA: mouse.position,
pointB: { x: 0, y: 0 },
length: 0.01,
stiffness: 0.1,
angularStiffness: 1,
strokeStyle: 'lightgreen',
lineWidth: 3
});
return {
mouse: mouse,
dragBody: null,
dragPoint: null,
constraints: [constraint]
};
};
MouseConstraint.update = function(mouseConstraint, bodies) {
var mouse = mouseConstraint.mouse,
constraint = mouseConstraint.constraints[0];
if (mouse.button === 0 || mouse.button === 2) {
if (!constraint.bodyB) {
for (var i = 0; i < bodies.length; i++) {
var body = bodies[i];
if (Bounds.contains(body.bounds, mouse.position)
&& Vertices.contains(body.vertices, mouse.position)) {
constraint.pointA = mouse.position;
constraint.bodyB = body;
constraint.pointB = { x: mouse.position.x - body.position.x, y: mouse.position.y - body.position.y };
constraint.angleB = body.angle;
Sleeping.set(body, false);
}
}
}
} else {
constraint.bodyB = null;
constraint.pointB = null;
}
if (constraint.bodyB) {
Sleeping.set(constraint.bodyB, false);
constraint.pointA = mouse.position;
}
};
})();

117
src/core/Common.js Normal file
View file

@ -0,0 +1,117 @@
var Common = {};
(function() {
Common.extend = function(obj) {
var args = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < args.length; i++) {
var source = args[i];
if (source) {
for (var prop in source) {
if (source[prop].constructor === Object) {
if (!obj[prop] || obj[prop].constructor === Object) {
obj[prop] = obj[prop] || {};
Common.extend(obj[prop], source[prop]);
} else {
obj[prop] = source[prop];
}
} else {
obj[prop] = source[prop];
}
}
}
}
return obj;
};
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;
};
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;
};
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);
};
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;
};
Common.choose = function(choices) {
return choices[Math.floor(Math.random() * choices.length)];
};
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");
}
};
Common.clamp = function(value, min, max) {
if (value < min)
return min;
if (value > max)
return max;
return value;
};
Common.sign = function(value) {
return value < 0 ? -1 : 1;
};
Common.now = Date.now || function() {
// http://stackoverflow.com/questions/221294/how-do-you-get-a-timestamp-in-javascript
return +(new Date());
};
Common.random = function(min, max) {
return min + Math.random() * (max - min);
};
})();

218
src/core/Engine.js Normal file
View file

@ -0,0 +1,218 @@
// TODO: multiple event handlers, before & after handlers
// TODO: viewports
// TODO: frameskipping
var Engine = {};
(function() {
var _fps = 60,
_deltaSampleSize = 8,
_delta = 1000 / _fps;
var _requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame || window.msRequestAnimationFrame
|| function(callback){ window.setTimeout(function() { callback(Common.now()); }, _delta); };
Engine.create = function(element, options) {
var defaults = {
enabled: true,
positionIterations: 6,
velocityIterations: 4,
constraintIterations: 1,
pairs: {},
pairsList: [],
enableSleeping: false,
timeScale: 1,
input: {},
timing: {
fps: _fps,
timestamp: 0,
delta: _delta,
correction: 1,
deltaMin: 1000 / _fps,
deltaMax: 1000 / (_fps * 0.5)
}
};
var engine = Common.extend(defaults, options);
engine = Common.isElement(element) ? (engine || {}) : element;
// create default renderer only if no custom renderer set
// but still allow engine.render.engine to pass through if set
if (!engine.render || (engine.render && !engine.render.controller)) {
engine.render = Render.create(engine.render);
if (Common.isElement(element))
element.appendChild(engine.render.canvas);
}
engine.world = World.create(engine.world);
engine.metrics = engine.metrics || Metrics.create();
engine.input.mouse = engine.input.mouse || Mouse.create(engine.render.canvas);
engine.mouseConstraint = engine.mouseConstraint || MouseConstraint.create(engine.input.mouse);
World.addComposite(engine.world, engine.mouseConstraint);
engine.broadphase = engine.broadphase || {
current: 'grid',
grid: {
controller: Grid,
instance: Grid.create(),
detector: Detector.collisions
},
bruteForce: {
detector: Detector.bruteForce
}
};
engine.events = {
tick: function(engine) {
Engine.update(engine, engine.timing.delta, engine.timing.correction);
},
render: function(engine) {
if (engine.render.options.enabled)
engine.render.controller.world(engine);
}
};
return engine;
};
Engine.run = function(engine) {
var timing = engine.timing,
delta,
counterTimestamp = 0,
frameCounter = 0,
deltaHistory = [];
(function render(timestamp){
_requestAnimationFrame(render);
if (!engine.enabled)
return;
delta = (timestamp - timing.timestamp) || _delta;
// optimistically filter delta over a few frames, to improve stability
deltaHistory.push(delta);
deltaHistory = deltaHistory.slice(-_deltaSampleSize);
delta = Math.min.apply(null, deltaHistory);
// limit delta
delta = delta < engine.timing.deltaMin ? engine.timing.deltaMin : delta;
delta = delta > engine.timing.deltaMax ? engine.timing.deltaMax : delta;
timing.timestamp = timestamp;
timing.correction = delta / timing.delta;
timing.delta = delta;
frameCounter += 1;
if (timestamp - counterTimestamp >= 1000) {
timing.fps = frameCounter * ((timestamp - counterTimestamp) / 1000);
counterTimestamp = timestamp;
frameCounter = 0;
}
engine.events.tick(engine);
engine.events.render(engine);
})();
};
Engine.update = function(engine, delta, correction) {
var world = engine.world,
broadphase = engine.broadphase[engine.broadphase.current],
broadphasePairs = [],
i;
Body.resetForcesAll(world.bodies, world.gravity);
Metrics.reset(engine.metrics);
MouseConstraint.update(engine.mouseConstraint, world.bodies, engine.input);
Body.updateAll(world.bodies, delta * engine.timeScale, correction, world.bounds);
for (i = 0; i < engine.constraintIterations; i++) {
Constraint.updateAll(world.constraints);
}
if (broadphase.controller) {
broadphase.controller.update(broadphase.instance, world.bodies, engine);
broadphasePairs = broadphase.instance.pairsList;
} else {
broadphasePairs = world.bodies;
}
var pairsUpdated = Manager.updatePairs(engine.pairs, engine.pairsList, broadphasePairs, engine.metrics, broadphase.detector),
pairsRemoved = Manager.removeOldPairs(engine.pairs, engine.pairsList);
if (pairsUpdated || pairsRemoved)
engine.pairsList = Common.values(engine.pairs);
// wake up bodies involved in collisions
if (engine.enableSleeping)
Sleeping.afterCollisions(engine.pairsList);
// iteratively resolve velocity between collisions
Resolver.preSolveVelocity(engine.pairsList);
for (i = 0; i < engine.velocityIterations; i++) {
Resolver.solveVelocity(engine.pairsList);
}
// iteratively resolve position between collisions
for (i = 0; i < engine.positionIterations; i++) {
Resolver.solvePosition(engine.pairsList);
}
Resolver.postSolvePosition(world.bodies);
if (engine.enableSleeping)
Sleeping.update(world.bodies);
Metrics.update(engine.metrics, engine);
return engine;
};
Engine.merge = function(engineA, engineB) {
Common.extend(engineA, engineB);
if (engineB.world) {
engineA.world = engineB.world;
Engine.clear(engineA);
var bodies = engineA.world.bodies;
for (var i = 0; i < bodies.length; i++) {
var body = bodies[i];
Sleeping.set(body, false);
body.id = Body.nextId();
}
var broadphase = engineA.broadphase[engineA.broadphase.current];
if (broadphase.controller === Grid) {
var grid = broadphase.instance;
Grid.clear(grid);
Grid.update(grid, engineA.world.bodies, engineA, true);
}
}
};
Engine.clear = function(engine) {
var world = engine.world;
engine.pairs = {};
engine.pairsList = [];
World.addComposite(engine.world, engine.mouseConstraint);
var broadphase = engine.broadphase[engine.broadphase.current];
if (broadphase.controller === Grid)
Grid.clear(broadphase.instance);
if (broadphase.controller) {
broadphase.controller.update(broadphase.instance, world.bodies, engine, true);
}
};
})();

49
src/core/Metrics.js Normal file
View file

@ -0,0 +1,49 @@
var Metrics = {};
(function() {
Metrics.create = function() {
return {
narrowDetections: 0,
narrowphaseTests: 0,
midphaseTests: 0,
broadphaseTests: 0,
narrowEff: 0.0001,
midEff: 0.0001,
broadEff: 0.0001,
collisions: 0,
buckets: 0,
bodies: 0,
pairs: 0
};
};
Metrics.reset = function(metrics) {
metrics.narrowDetections = 0;
metrics.narrowphaseTests = 0;
metrics.midphaseTests = 0;
metrics.broadphaseTests = 0;
metrics.narrowEff = 0;
metrics.midEff = 0;
metrics.broadEff = 0;
metrics.collisions = 0;
metrics.buckets = 0;
metrics.pairs = 0;
metrics.bodies = 0;
};
Metrics.update = function(metrics, engine) {
var world = engine.world,
broadphase = engine.broadphase[engine.broadphase.current];
metrics.collisions = metrics.narrowDetections;
metrics.pairs = engine.pairsList.length;
metrics.bodies = world.bodies.length;
metrics.midEff = (metrics.narrowDetections / (metrics.midphaseTests || 1)).toFixed(2);
metrics.narrowEff = (metrics.narrowDetections / (metrics.narrowphaseTests || 1)).toFixed(2);
metrics.broadEff = (1 - (metrics.broadphaseTests / (world.bodies.length || 1))).toFixed(2);
if (broadphase.instance)
metrics.buckets = Common.keys(broadphase.instance.buckets).length;
};
})();

88
src/core/Mouse.js Normal file
View file

@ -0,0 +1,88 @@
var Mouse;
(function() {
Mouse = function(element) {
var mouse = this;
element = element || document.body;
this.position = { x: 0, y: 0 };
this.mousedownPosition = { x: 0, y: 0 };
this.mouseupPosition = { x: 0, y: 0 };
this.button = -1;
var mousemove = function(event) {
var position = _getRelativeMousePosition(event, element),
touches = event.changedTouches;
if (touches) {
mouse.button = 0;
event.preventDefault();
}
mouse.position = position;
};
var mousedown = function(event) {
var position = _getRelativeMousePosition(event, element),
touches = event.changedTouches;
if (touches) {
mouse.button = 0;
event.preventDefault();
} else {
mouse.button = event.button;
}
mouse.position = mouse.mousedownPosition = position;
};
var mouseup = function(event) {
var position = _getRelativeMousePosition(event, element),
touches = event.changedTouches;
if (touches) {
event.preventDefault();
}
mouse.button = -1;
mouse.position = mouse.mouseupPosition = position;
};
element.addEventListener('mousemove', mousemove);
element.addEventListener('mousedown', mousedown);
element.addEventListener('mouseup', mouseup);
element.addEventListener('touchmove', mousemove);
element.addEventListener('touchstart', mousedown);
element.addEventListener('touchend', mouseup);
};
Mouse.create = function(element) {
return new Mouse(element);
};
var _getRelativeMousePosition = function(event, element) {
var elementBounds = element.getBoundingClientRect(),
scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft,
scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop,
touches = event.changedTouches,
x, y;
if (touches) {
x = touches[0].pageX - elementBounds.left - scrollX;
y = touches[0].pageY - elementBounds.top - scrollY;
} else {
x = event.pageX - elementBounds.left - scrollX;
y = event.pageY - elementBounds.top - scrollY;
}
return {
x: x / (element.clientWidth / element.width),
y: y / (element.clientHeight / element.height)
};
};
})();

75
src/core/Sleeping.js Normal file
View file

@ -0,0 +1,75 @@
var Sleeping = {};
(function() {
var _motionWakeThreshold = 0.18,
_motionSleepThreshold = 0.08,
_minBias = 0.9;
Sleeping.update = function(bodies) {
// update bodies sleeping status
for (var i = 0; i < bodies.length; i++) {
var body = bodies[i],
motion = body.speed * body.speed + body.angularSpeed * body.angularSpeed;
var minMotion = Math.min(body.motion, motion),
maxMotion = Math.max(body.motion, motion);
// biased average motion estimation between frames
body.motion = _minBias * minMotion + (1 - _minBias) * maxMotion;
if (body.sleepThreshold > 0 && body.motion < _motionSleepThreshold) {
body.sleepCounter += 1;
if (body.sleepCounter >= body.sleepThreshold)
Sleeping.set(body, true);
} else if (body.sleepCounter > 0) {
body.sleepCounter -= 1;
}
}
};
Sleeping.afterCollisions = function(pairs) {
// wake up bodies involved in collisions
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i],
collision = pair.collision,
bodyA = collision.bodyA,
bodyB = collision.bodyB;
if ((bodyA.isSleeping && bodyB.isSleeping) || bodyA.isStatic || bodyB.isStatic)
continue;
if (bodyA.isSleeping || bodyB.isSleeping) {
var sleepingBody = (bodyA.isSleeping && !bodyA.isStatic) ? bodyA : bodyB,
movingBody = sleepingBody === bodyA ? bodyB : bodyA;
if (!sleepingBody.isStatic && movingBody.motion > _motionWakeThreshold) {
Sleeping.set(sleepingBody, false);
}
}
}
};
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;
}
};
})();

80
src/factory/Bodies.js Normal file
View file

@ -0,0 +1,80 @@
// TODO: true circle bodies
var Bodies = {};
(function() {
Bodies.rectangle = function(x, y, width, height, options) {
options = options || {};
var rectangle = {
position: { x: x, y: y },
path: 'L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height
};
return Body.create(Common.extend({}, rectangle, options));
};
Bodies.trapezoid = function(x, y, width, height, slope, options) {
options = options || {};
slope *= 0.5;
var roof = (1 - (slope * 2)) * width;
var x1 = width * slope,
x2 = x1 + roof,
x3 = x2 + x1;
var trapezoid = {
position: { x: x, y: y },
path: 'L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0'
};
return Body.create(Common.extend({}, trapezoid, options));
};
Bodies.circle = function(x, y, radius, options, maxSides) {
options = options || {};
// approximate circles with polygons until true circles implemented in SAT
maxSides = maxSides || 25;
var sides = Math.ceil(Math.max(10, Math.min(maxSides, radius)));
// optimisation: always use even number of sides (half the number of unique axes)
if (sides % 2 === 1)
sides += 1;
// flag for better rendering
options.circleRadius = radius;
return Bodies.polygon(x, y, sides, radius, options);
};
Bodies.polygon = function(x, y, sides, radius, options) {
options = options || {};
if (sides < 3)
return Bodies.circle(x, y, radius, options);
var theta = 2 * Math.PI / sides,
path = '',
offset = theta * 0.5;
for (var i = 0; i < sides; i += 1) {
var angle = offset + (i * theta),
xx = Math.cos(angle) * radius,
yy = Math.sin(angle) * radius;
path += 'L ' + xx.toFixed(3) + ' ' + yy.toFixed(3) + ' ';
}
var polygon = {
position: { x: x, y: y },
path: path
};
return Body.create(Common.extend({}, polygon, options));
};
})();

160
src/factory/Composites.js Normal file
View file

@ -0,0 +1,160 @@
var Composites = {};
(function() {
Composites.stack = function(xx, yy, columns, rows, columnGap, rowGap, callback) {
var stack = Composite.create(),
x = xx,
y = yy,
lastBody,
i = 0;
for (var row = 0; row < rows; row++) {
var maxHeight = 0;
for (var column = 0; column < columns; column++) {
var body = callback(x, y, column, row, lastBody, i);
if (body) {
var bodyHeight = body.bounds.max.y - body.bounds.min.y,
bodyWidth = body.bounds.max.x - body.bounds.min.x;
if (bodyHeight > maxHeight)
maxHeight = bodyHeight;
Body.translate(body, { x: bodyWidth * 0.5, y: bodyHeight * 0.5 });
x = body.bounds.max.x + columnGap;
Composite.addBody(stack, body);
lastBody = body;
i += 1;
}
}
y += maxHeight + rowGap;
x = xx;
}
return stack;
};
Composites.chain = function(composite, xOffsetA, yOffsetA, xOffsetB, yOffsetB, options) {
var bodies = composite.bodies;
for (var i = 1; i < bodies.length; i++) {
var bodyA = bodies[i - 1],
bodyB = bodies[i],
bodyAHeight = bodyA.bounds.max.y - bodyA.bounds.min.y,
bodyAWidth = bodyA.bounds.max.x - bodyA.bounds.min.x,
bodyBHeight = bodyB.bounds.max.y - bodyB.bounds.min.y,
bodyBWidth = bodyB.bounds.max.x - bodyB.bounds.min.x;
var defaults = {
bodyA: bodyA,
pointA: { x: bodyAWidth * xOffsetA, y: bodyAHeight * yOffsetA },
bodyB: bodyB,
pointB: { x: bodyBWidth * xOffsetB, y: bodyBHeight * yOffsetB }
};
var constraint = Common.extend(defaults, options);
Composite.addConstraint(composite, Constraint.create(constraint));
}
return composite;
};
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);
});
};
Composites.newtonsCradle = function(xx, yy, number, size, length) {
var newtonsCradle = Composite.create();
for (var i = 0; i < number; i++) {
var separation = 1.9,
circle = Bodies.circle(xx + i * (size * separation), yy + length, size,
{ restitution: 1, friction: 0, frictionAir: 0.0001, slop: 0.01 }),
constraint = Constraint.create({ pointA: { x: xx + i * (size * separation), y: yy }, bodyB: circle });
Composite.addBody(newtonsCradle, circle);
Composite.addConstraint(newtonsCradle, constraint);
}
return newtonsCradle;
};
Composites.car = function(xx, yy, width, height, wheelSize) {
var groupId = Body.nextGroupId(),
wheelBase = -20,
wheelAOffset = -width * 0.5 + wheelBase,
wheelBOffset = width * 0.5 - wheelBase,
wheelYOffset = 0;
var car = Composite.create(),
body = Bodies.trapezoid(xx, yy, width, height, 0.3, { groupId: groupId, friction: 0.01 });
var wheelA = Bodies.circle(xx + wheelAOffset, yy + wheelYOffset, wheelSize, {
groupId: groupId,
restitution: 0.5,
friction: 0.9,
density: 0.01
});
var wheelB = Bodies.circle(xx + wheelBOffset, yy + wheelYOffset, wheelSize, {
groupId: groupId,
restitution: 0.5,
friction: 0.9,
density: 0.01
});
var axelA = Constraint.create({
bodyA: body,
pointA: { x: wheelAOffset, y: wheelYOffset },
bodyB: wheelA,
stiffness: 0.5
});
var axelB = Constraint.create({
bodyA: body,
pointA: { x: wheelBOffset, y: wheelYOffset },
bodyB: wheelB,
stiffness: 0.5
});
Composite.addBody(car, body);
Composite.addBody(car, wheelA);
Composite.addBody(car, wheelB);
Composite.addConstraint(car, axelA);
Composite.addConstraint(car, axelB);
return car;
};
})();

42
src/geometry/Axes.js Normal file
View file

@ -0,0 +1,42 @@
var Axes = {};
(function() {
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);
};
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;
}
};
})();

53
src/geometry/Bounds.js Normal file
View file

@ -0,0 +1,53 @@
var Bounds = {};
(function() {
Bounds.create = function(vertices) {
var bounds = {
min: { x: 0, y: 0 },
max: { x: 0, y: 0 }
};
Bounds.update(bounds, vertices);
return bounds;
};
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;
}
}
};
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;
};
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);
};
})();

71
src/geometry/Vector.js Normal file
View file

@ -0,0 +1,71 @@
// TODO: consider params for reusing vector objects
var Vector = {};
(function() {
Vector.magnitude = function(vector) {
return Math.sqrt((vector.x * vector.x) + (vector.y * vector.y));
};
Vector.magnitudeSquared = function(vector) {
return (vector.x * vector.x) + (vector.y * vector.y);
};
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
};
};
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)
};
};
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 };
};
Vector.dot = function(vectorA, vectorB) {
return (vectorA.x * vectorB.x) + (vectorA.y * vectorB.y);
};
Vector.cross = function(vectorA, vectorB) {
return (vectorA.x * vectorB.y) - (vectorA.y * vectorB.x);
};
Vector.add = function(vectorA, vectorB) {
return { x: vectorA.x + vectorB.x, y: vectorA.y + vectorB.y };
};
Vector.sub = function(vectorA, vectorB) {
return { x: vectorA.x - vectorB.x, y: vectorA.y - vectorB.y };
};
Vector.mult = function(vector, scalar) {
return { x: vector.x * scalar, y: vector.y * scalar };
};
Vector.div = function(vector, scalar) {
return { x: vector.x / scalar, y: vector.y / scalar };
};
Vector.perp = function(vector, negate) {
negate = negate === true ? -1 : 1;
return { x: negate * -vector.y, y: negate * vector.x };
};
Vector.neg = function(vector) {
return { x: -vector.x, y: -vector.y };
};
})();

111
src/geometry/Vertices.js Normal file
View file

@ -0,0 +1,111 @@
// TODO: convex decomposition - http://mnbayazit.com/406/bayazit
var Vertices = {};
(function() {
Vertices.create = function(vertices, body) {
for (var i = 0; i < vertices.length; i++) {
vertices[i].index = i;
vertices[i].body = body;
}
};
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;
};
Vertices.centre = function(vertices) {
var cx = 0, cy = 0;
for (var i = 0; i < vertices.length; i++) {
cx += vertices[i].x;
cy += vertices[i].y;
}
return { x: cx / vertices.length, y: cy / vertices.length };
};
Vertices.area = function(vertices) {
var area = 0,
j = vertices.length - 1;
for (var i = 0; i < vertices.length; i++) {
area += (vertices[j].x - vertices[i].x) * (vertices[j].y + vertices[i].y);
j = i;
}
return Math.abs(area) / 2;
};
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);
};
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;
}
}
};
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);
}
};
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;
};
})();

33
src/module/Intro.js Normal file
View file

@ -0,0 +1,33 @@
/**
* 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

57
src/module/Outro.js Normal file
View file

@ -0,0 +1,57 @@
// aliases
World.addComposite = Composite.add;
World.addBody = Composite.addBody;
World.addConstraint = Composite.addConstraint;
// exports
Matter.Body = Body;
Matter.Composite = Composite;
Matter.World = World;
Matter.Contact = Contact;
Matter.Detector = Detector;
Matter.Grid = Grid;
Matter.Manager = Manager;
Matter.Pair = Pair;
Matter.Resolver = Resolver;
Matter.SAT = SAT;
Matter.Constraint = Constraint;
Matter.MouseConstraint = MouseConstraint;
Matter.Common = Common;
Matter.Engine = Engine;
Matter.Metrics = Metrics;
Matter.Mouse = Mouse;
Matter.Sleeping = Sleeping;
Matter.Bodies = Bodies;
Matter.Composites = Composites;
Matter.Axes = Axes;
Matter.Bounds = Bounds;
Matter.Vector = Vector;
Matter.Vertices = Vertices;
Matter.Gui = Gui;
Matter.Render = Render;
// CommonJS module
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = Matter;
}
exports.Matter = Matter;
}
// AMD module
if (typeof define === 'function' && define.amd) {
define('Matter', [], function () {
return Matter;
});
}
// browser
if (typeof window === 'object' && typeof window.document === 'object') {
window.Matter = Matter;
}
// End Matter namespace closure
})();

159
src/render/Gui.js Normal file
View file

@ -0,0 +1,159 @@
var Gui = {};
(function() {
Gui.create = function(engine, options) {
var _datGuiSupported = window.dat && window.localStorage,
_serializer;
if (!_datGuiSupported) {
console.log("Could not create GUI. Check dat.gui library is loaded first.");
return;
}
var datGui = new dat.GUI(options);
if (Resurrect) {
_serializer = new Resurrect({ prefix: '$' });
_serializer.parse = _serializer.resurrect;
} else {
_serializer = JSON;
}
var gui = {
datGui: datGui,
amount: 1,
size: 40,
sides: 4,
density: 0.001,
restitution: 0,
friction: 0.1,
frictionAir: 0.01
};
var funcs = {
addBody: function() {
var options = {
density: gui.density,
friction: gui.friction,
frictionAir: gui.frictionAir,
restitution: gui.restitution
};
for (var i = 0; i < gui.amount; i++) {
World.addBody(engine.world, Bodies.polygon(120 + i * gui.size + i * 50, 200, gui.sides, gui.size, options));
}
},
clear: function() {
World.clear(engine.world, true);
Engine.clear(engine);
},
save: function() {
if (localStorage && _serializer) {
localStorage.setItem('world', _serializer.stringify(engine.world));
}
},
load: function() {
var loadedWorld;
if (localStorage && _serializer) {
loadedWorld = _serializer.parse(localStorage.getItem('world'));
}
if (loadedWorld) {
Engine.merge(engine, { world: loadedWorld });
}
}
};
var metrics = datGui.addFolder('Metrics');
metrics.add(engine.timing, 'fps').listen();
metrics.add(engine.timing, 'delta').listen();
metrics.add(engine.timing, 'correction').listen();
metrics.add(engine.metrics, 'bodies').listen();
metrics.add(engine.metrics, 'collisions').listen();
metrics.add(engine.metrics, 'pairs').listen();
metrics.add(engine.metrics, 'broadEff').listen();
metrics.add(engine.metrics, 'midEff').listen();
metrics.add(engine.metrics, 'narrowEff').listen();
metrics.close();
var controls = datGui.addFolder('Add Body');
controls.add(gui, 'amount', 1, 5).step(1);
controls.add(gui, 'size', 5, 150).step(1);
controls.add(gui, 'sides', 1, 8).step(1);
controls.add(gui, 'density', 0.0001, 0.01).step(0.001);
controls.add(gui, 'friction', 0, 1).step(0.05);
controls.add(gui, 'frictionAir', 0, gui.frictionAir * 10).step(gui.frictionAir / 10);
controls.add(gui, 'restitution', 0, 1).step(0.1);
controls.add(funcs, 'addBody');
controls.open();
var worldGui = datGui.addFolder('World');
worldGui.add(funcs, 'load');
worldGui.add(funcs, 'save');
worldGui.add(funcs, 'clear');
worldGui.open();
var gravity = worldGui.addFolder('Gravity');
gravity.add(engine.world.gravity, 'x', -1, 1).step(0.01);
gravity.add(engine.world.gravity, 'y', -1, 1).step(0.01);
gravity.open();
var physics = datGui.addFolder('Engine');
physics.add(engine, 'enableSleeping');
physics.add(engine.broadphase, 'current', ['grid', 'bruteForce']);
physics.add(engine, 'timeScale', 0.1, 2).step(0.1);
physics.add(engine, 'velocityIterations', 1, 10).step(1);
physics.add(engine, 'positionIterations', 1, 10).step(1);
physics.add(engine, 'enabled');
physics.open();
var render = datGui.addFolder('Render');
render.add(engine.render.options, 'wireframes');
render.add(engine.render.options, 'showDebug');
render.add(engine.render.options, 'showPositions');
render.add(engine.render.options, 'showBroadphase');
render.add(engine.render.options, 'showBounds');
render.add(engine.render.options, 'showVelocity');
render.add(engine.render.options, 'showCollisions');
render.add(engine.render.options, 'showAxes');
render.add(engine.render.options, 'showAngleIndicator');
render.add(engine.render.options, 'showSleeping');
render.add(engine.render.options, 'showIds');
render.add(engine.render.options, 'showShadows');
render.add(engine.render.options, 'enabled');
render.open();
//datGui.remember(world)
return gui;
};
Gui.update = function(gui, datGui) {
var i;
datGui = datGui || gui.datGui;
for (i in datGui.__folders) {
Gui.update(gui, datGui.__folders[i]);
}
for (i in datGui.__controllers) {
var controller = datGui.__controllers[i];
if (controller.updateDisplay)
controller.updateDisplay();
}
};
Gui.closeAll = function(gui) {
var datGui = gui.datGui;
for (var i in datGui.__folders) {
datGui.__folders[i].close();
}
};
})();

378
src/render/Render.js Normal file
View file

@ -0,0 +1,378 @@
// TODO: viewports
// TODO: two.js, pixi.js
var Render = {};
(function() {
Render.create = function(options) {
var defaults = {
controller: Render,
options: {
width: 800,
height: 600,
background: '#fafafa',
wireframeBackground: '#222',
enabled: true,
wireframes: true,
showSleeping: true,
showDebug: false,
showBroadphase: false,
showBounds: false,
showVelocity: false,
showCollisions: false,
showAxes: false,
showPositions: false,
showAngleIndicator: false,
showIds: false,
showShadows: false
}
};
var render = Common.extend(defaults, options);
render.canvas = render.canvas || _createCanvas(render.options.width, render.options.height);
render.context = render.canvas.getContext('2d');
return render;
};
Render.world = function(engine) {
var render = engine.render,
world = engine.world,
canvas = render.canvas,
context = render.context,
options = render.options,
i;
if (options.wireframes) {
context.fillStyle = options.wireframeBackground;
} else {
context.fillStyle = options.background;
}
context.fillRect(0, 0, canvas.width, canvas.height);
if (options.showShadows && !options.wireframes)
for (i = 0; i < world.bodies.length; i++)
Render.bodyShadow(engine, world.bodies[i], context);
for (i = 0; i < world.bodies.length; i++)
Render.body(engine, world.bodies[i], context);
for (i = 0; i < world.constraints.length; i++)
Render.constraint(world.constraints[i], context);
if (options.showCollisions)
for (i = 0; i < engine.pairsList.length; i++)
Render.collision(engine, engine.pairsList[i], context);
if (options.showBroadphase && engine.broadphase.current === 'grid')
Render.grid(engine, engine.broadphase[engine.broadphase.current].instance, context);
if (options.showDebug)
Render.debug(engine, context);
};
Render.debug = function(engine, context) {
var c = context,
world = engine.world,
render = engine.render,
options = render.options,
space = " ";
if (engine.timing.timestamp - (render.debugTimestamp || 0) >= 500) {
var text = "";
text += "delta: " + engine.timing.delta.toFixed(3) + space;
text += "fps: " + Math.round(engine.timing.fps) + space;
text += "correction: " + engine.timing.correction.toFixed(3) + space;
text += "bodies: " + world.bodies.length + space;
if (engine.broadphase.controller === Grid)
text += "buckets: " + engine.metrics.buckets + space;
text += "\n";
text += "collisions: " + engine.metrics.collisions + space;
text += "pairs: " + engine.pairs.length + space;
text += "broad: " + engine.metrics.broadEff + space;
text += "mid: " + engine.metrics.midEff + space;
text += "narrow: " + engine.metrics.narrowEff + space;
render.debugString = text;
render.debugTimestamp = engine.timing.timestamp;
}
if (render.debugString) {
c.font = "12px Arial";
if (options.wireframes) {
c.fillStyle = 'rgba(255,255,255,0.5)';
} else {
c.fillStyle = 'rgba(0,0,0,0.5)';
}
var split = render.debugString.split('\n');
for (var i = 0; i < split.length; i++) {
c.fillText(split[i], 50, 50 + i * 18);
}
}
};
Render.constraint = function(constraint, context) {
var bodyA = constraint.bodyA,
bodyB = constraint.bodyB,
c = context;
if (!constraint.pointA || !constraint.pointB)
return;
if (bodyA) {
c.beginPath();
c.moveTo(bodyA.position.x + constraint.pointA.x, bodyA.position.y + constraint.pointA.y);
} else {
c.beginPath();
c.moveTo(constraint.pointA.x, constraint.pointA.y);
}
if (bodyB) {
c.lineTo(bodyB.position.x + constraint.pointB.x, bodyB.position.y + constraint.pointB.y);
} else {
c.lineTo(constraint.pointB.x, constraint.pointB.y);
}
c.lineWidth = constraint.lineWidth;
c.strokeStyle = constraint.strokeStyle;
c.stroke();
};
Render.bodyShadow = function(engine, body, context) {
var c = context,
render = engine.render;
if (body.circleRadius) {
c.beginPath();
c.arc(body.position.x, body.position.y, body.circleRadius, 0, 2 * Math.PI);
c.closePath();
} else {
c.beginPath();
c.moveTo(body.vertices[0].x, body.vertices[0].y);
for (var j = 1; j < body.vertices.length; j++) {
c.lineTo(body.vertices[j].x, body.vertices[j].y);
}
c.closePath();
}
var distanceX = body.position.x - render.options.width * 0.5,
distanceY = body.position.y - render.options.height * 0.2,
distance = Math.abs(distanceX) + Math.abs(distanceY);
c.shadowColor = 'rgba(0,0,0,0.15)';
c.shadowOffsetX = 0.05 * distanceX;
c.shadowOffsetY = 0.05 * distanceY;
c.shadowBlur = 1 + 12 * Math.min(1, distance / 1000);
c.fill();
c.shadowColor = null;
c.shadowOffsetX = null;
c.shadowOffsetY = null;
c.shadowBlur = null;
};
Render.body = function(engine, body, context) {
var c = context,
render = engine.render,
options = render.options;
// body bounds
if (options.showBounds) {
c.beginPath();
c.rect(body.bounds.min.x, body.bounds.min.y, body.bounds.max.x - body.bounds.min.x, body.bounds.max.y - body.bounds.min.y);
c.lineWidth = 1;
if (options.wireframes) {
c.strokeStyle = 'rgba(255,255,255,0.08)';
} else {
c.strokeStyle = 'rgba(0,0,0,0.1)';
}
c.stroke();
}
// body polygon
if (body.circleRadius) {
c.beginPath();
c.arc(body.position.x, body.position.y, body.circleRadius, 0, 2 * Math.PI);
c.closePath();
} else {
c.beginPath();
c.moveTo(body.vertices[0].x, body.vertices[0].y);
for (var j = 1; j < body.vertices.length; j++) {
c.lineTo(body.vertices[j].x, body.vertices[j].y);
}
c.closePath();
}
if (!options.wireframes) {
c.fillStyle = body.fillStyle;
if (options.showSleeping && body.isSleeping)
c.fillStyle = Common.shadeColor(body.fillStyle, 50);
c.lineWidth = body.lineWidth;
c.strokeStyle = body.strokeStyle;
c.fill();
c.stroke();
} else {
c.lineWidth = 1;
c.strokeStyle = '#bbb';
if (options.showSleeping && body.isSleeping)
c.strokeStyle = 'rgba(255,255,255,0.2)';
c.stroke();
}
// angle indicator
if (options.showAngleIndicator && !options.showAxes) {
c.beginPath();
c.moveTo(body.position.x, body.position.y);
c.lineTo((body.vertices[0].x + body.vertices[body.vertices.length-1].x) / 2,
(body.vertices[0].y + body.vertices[body.vertices.length-1].y) / 2);
c.lineWidth = 1;
if (options.wireframes) {
c.strokeStyle = 'indianred';
} else {
c.strokeStyle = body.strokeStyle;
}
c.stroke();
}
// axes
if (options.showAxes) {
for (var i = 0; i < body.axes.length; i++) {
var axis = body.axes[i];
c.beginPath();
c.moveTo(body.position.x, body.position.y);
c.lineTo(body.position.x + axis.x * 20, body.position.y + axis.y * 20);
c.lineWidth = 1;
if (options.wireframes) {
c.strokeStyle = 'indianred';
} else {
c.strokeStyle = body.strokeStyle;
}
c.stroke();
}
}
// positions
if (options.showPositions) {
c.beginPath();
c.arc(body.position.x, body.position.y, 3, 0, 2 * Math.PI, false);
if (options.wireframes) {
c.fillStyle = 'indianred';
} else {
c.fillStyle = 'rgba(0,0,0,0.5)';
}
c.fill();
c.beginPath();
c.arc(body.positionPrev.x, body.positionPrev.y, 2, 0, 2 * Math.PI, false);
c.fillStyle = 'rgba(255,165,0,0.8)';
c.fill();
}
// body velocity vector
if (options.showVelocity) {
c.beginPath();
c.moveTo(body.position.x, body.position.y);
c.lineTo(body.position.x + (body.position.x - body.positionPrev.x) * 2, body.position.y + (body.position.y - body.positionPrev.y) * 2);
c.lineWidth = 3;
c.strokeStyle = 'cornflowerblue';
c.stroke();
}
// body id
if (options.showIds) {
c.font = "12px Arial";
c.fillStyle = 'rgba(255,255,255,0.5)';
c.fillText(body.id, body.position.x + 10, body.position.y - 10);
}
};
Render.collision = function(engine, pair, context) {
var c = context,
collision = pair.collision,
options = engine.render.options;
for (var i = 0; i < pair.activeContacts.length; i++) {
var contact = pair.activeContacts[i],
vertex = contact.vertex;
c.beginPath();
//c.arc(vertex.x, vertex.y, 2.5, 0, 2 * Math.PI, false);
c.rect(vertex.x - 1.5, vertex.y - 1.5, 3.5, 3.5);
if (options.wireframes) {
c.fillStyle = 'rgba(255,255,255,0.7)';
} else {
c.fillStyle = 'orange';
}
c.fill();
}
if (pair.activeContacts.length > 0) {
var normalPosX = pair.activeContacts[0].vertex.x,
normalPosY = pair.activeContacts[0].vertex.y;
if (pair.activeContacts.length === 2) {
normalPosX = (pair.activeContacts[0].vertex.x + pair.activeContacts[1].vertex.x) / 2;
normalPosY = (pair.activeContacts[0].vertex.y + pair.activeContacts[1].vertex.y) / 2;
}
// collision normal
c.beginPath();
c.moveTo(normalPosX - collision.normal.x * 8, normalPosY - collision.normal.y * 8);
c.lineWidth = 1;
c.lineTo(normalPosX, normalPosY);
if (options.wireframes) {
c.strokeStyle = 'rgba(255,165,0,0.7)';
} else {
c.strokeStyle = 'orange';
}
c.stroke();
}
};
Render.grid = function(engine, grid, context) {
var c = context,
options = engine.render.options;
c.lineWidth = 1;
if (options.wireframes) {
c.strokeStyle = 'rgba(255,180,0,0.1)';
} else {
c.strokeStyle = 'rgba(255,180,0,0.5)';
}
var bucketKeys = Common.keys(grid.buckets);
for (var i = 0; i < bucketKeys.length; i++) {
var bucketId = bucketKeys[i];
if (grid.buckets[bucketId].length < 2)
continue;
var region = bucketId.split(',');
c.beginPath();
c.rect(0.5 + parseInt(region[0], 10) * grid.bucketWidth,
0.5 + parseInt(region[1], 10) * grid.bucketHeight,
grid.bucketWidth,
grid.bucketHeight);
c.stroke();
}
};
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;
};
})();