From b6ed9f04bc3877cd59c9ef6e03d42a51f8b8f6df Mon Sep 17 00:00:00 2001 From: liabru Date: Mon, 5 May 2014 19:32:51 +0100 Subject: [PATCH] release 0.8.0 --- CHANGELOG.md | 82 + README.md | 17 +- bower.json | 4 +- build/matter-0.7.0.min.js | 8 - build/{matter-0.7.0.js => matter-0.8.0.js} | 1431 ++++++++----- build/matter-0.8.0.min.js | 8 + build/matter.js | 1782 +++-------------- build/matter.min.js | 7 +- demo/index.html | 8 +- .../lib/{matter-0.7.0.js => matter-0.8.0.js} | 1431 ++++++++----- demo/js/lib/matter-tools/matter-tools-dev.js | 54 +- demo/mobile.html | 2 +- package.json | 4 +- 13 files changed, 2261 insertions(+), 2577 deletions(-) delete mode 100644 build/matter-0.7.0.min.js rename build/{matter-0.7.0.js => matter-0.8.0.js} (83%) create mode 100644 build/matter-0.8.0.min.js rename demo/js/lib/{matter-0.7.0.js => matter-0.8.0.js} (83%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17ca230..2cec009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,88 @@ ---------- +## 0.8.0-alpha - 2014-05-05 + +#### Release Highlights + +- added [Views](http://brm.io/matter-js-demo/#views) demo +- added [Time Scaling](http://brm.io/matter-js-demo/#timescale) demo +- added [Rounded Corners (Chamfering)](http://brm.io/matter-js-demo#rounded) demo +- added [Raycasting](http://brm.io/matter-js-demo/#raycasting) demo +- added Query module, including raycasting +- added 3 useful new Composite methods +- added support for mouse wheel, mouse scaling and offsets +- added Body.scale for scaling bodies on the fly +- added Body.timeScale for slowing down bodies per-body +- added Body.setStatic for correctly setting static +- added Vertices.chamfer for rounding corners of any set of vertices +- added integration of chamfer to body factories +- added engine.timing.isFixed option to disable dynamic timing +- added render.bounds for translating and scaling views +- easier use of Engine.update and Engine.render for those using custom game loops +- fixed issues with time scaling +- fixed error in calculation of centre of mass +- removed Matter.Gui in preparation for the [MatterTools](https://github.com/liabru/matter-tools) project launch + +#### Added + +- added Demo.views +- added Demo.timescale +- added Demo.rounded +- added Demo.raycasting +- added demo support for hash + '-inspect' to start the [MatterTools](https://github.com/liabru/matter-tools) inspector +- added Composite.rebase +- added Composite.move +- added Composite.get +- added Matter.Query +- added Query.ray +- added Query.region +- added Bounds.translate +- added Bounds.shift +- added Mouse.setScale +- added Mouse.setPosition +- added mouse.absolute +- added mouse.wheelDelta +- added mouse.offset +- added Body.setStatic +- added Body.scale +- added body.timeScale +- added Vector.angle +- added Vertices.chamfer for rounding corners +- added Engine.render +- added engine.timing.isFixed option +- added render.bounds for translating and scaling views +- added Common.nextId +- added body.label +- added constraint.label +- added composite.label + +#### Changed + +- changed engine.timing.timestamp to be simulation based rather than wall-clock based +- changed correction to now be optional in Engine.update (defaults to 1) +- changed Events.on to return callback +- changed Events.off to accept callbacks for removal +- changed vertices in Bounds.create to optional +- changed engine.timeScale to engine.timing.timeScale +- moved Matter.Inspector and Matter.Gui to the [MatterTools](https://github.com/liabru/matter-tools) project +- moved event documentation to end of file for clarity +- moved respective engine event triggers into Engine.update and Engine.render + +#### Removed + +- removed body.render.path (redundant) +- removed Body.nextId +- removed Constraint.nextId +- removed Composite.nextId +- removed Body.updateProperties (now a private method) + +#### Fixed + +- fixed issues with timing.timeScale +- fixed constraints now account for timeScale +- fixed Vertices.centre calculation to get correct centre of mass + ## 0.7.0-alpha - 2014-04-01 #### Release Highlights diff --git a/README.md b/README.md index dc5adb7..f32bac6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ - [Solid Shapes](http://brm.io/matter-js-demo#mixedSolid) - [Newton's Cradle](http://brm.io/matter-js-demo#newtonsCradle) - [Wrecking Ball](http://brm.io/matter-js-demo#wreckingBall) +- [Rounded Corners (Chamfering)](http://brm.io/matter-js-demo#rounded) +- [Views](http://brm.io/matter-js-demo/#views) +- [Time Scaling](http://brm.io/matter-js-demo/#timescale) +- [Raycasting](http://brm.io/matter-js-demo/#raycasting) - [Sprites](http://brm.io/matter-js-demo/#sprites) - [Pyramid](http://brm.io/matter-js-demo#pyramid) - [Car](http://brm.io/matter-js-demo#car) @@ -51,14 +55,17 @@ - Composite bodies - Sleeping and static bodies - Events +- Rounded corners (chamfering) +- Views (translate, zoom) +- Collision queries (raycasting, region tests) - Time scaling (slow-mo, speed-up) - Canvas renderer (supports vectors and textures) - WebGL renderer (requires [pixi.js](https://github.com/GoodBoyDigital/pixi.js/)) +- [MatterTools](https://github.com/liabru/matter-tools) for creating, testing and debugging worlds - 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/)) - Cross-browser (Chrome, Firefox, Safari, IE8+) - Mobile-compatible (touch, responsive) -- An original JavaScript physics implementation, not a port! +- An original JavaScript physics implementation (not a port) ### Status @@ -73,9 +80,9 @@ Though if I get time and people are interested, I may continue working on it. ### Install -Download [matter-0.7.0.js](https://github.com/liabru/matter-js/releases/download/0.7.0-alpha/matter-0.7.0.js) or [matter-0.7.0.min.js](https://github.com/liabru/matter-js/releases/download/0.7.0-alpha/matter-0.7.0.min.js) and include the script in your HTML file: +Download [matter-0.8.0.js](https://github.com/liabru/matter-js/releases/download/0.8.0-alpha/matter-0.8.0.js) or [matter-0.8.0.min.js](https://github.com/liabru/matter-js/releases/download/0.8.0-alpha/matter-0.8.0.min.js) and include the script in your HTML file: - + For the latest features try the [edge version (master)](https://raw.github.com/liabru/matter-js/master/build/matter.js), but it may not be fully stable. @@ -92,7 +99,7 @@ Some of the demos are avaliable at [codepen.io/liabru](http://codepen.io/liabru/ ### Documentation -See the [Matter.js API Docs (v0.7.0)](http://brm.io/matter-js-docs/). +See the [Matter.js API Docs (v0.8.0)](http://brm.io/matter-js-docs/).
If you're using the [edge version (master)](https://raw2.github.com/liabru/matter-js/master/build/matter.js) then see the [API Docs (master)](http://brm.io/matter-js-docs-master/). ### Changelog diff --git a/bower.json b/bower.json index 7db42dd..528f1cb 100644 --- a/bower.json +++ b/bower.json @@ -1,12 +1,12 @@ { "name": "Matter", - "version": "0.7.0", + "version": "0.8.0", "homepage": "https://github.com/liabru/matter-js", "authors": [ "Liam Brummitt (http://brm.io/)" ], "description": "a 2D rigid body physics engine for the web", - "main": "build/matter-0.7.0.min.js", + "main": "build/matter-0.8.0.min.js", "keywords": [ "javascript", "canvas", diff --git a/build/matter-0.7.0.min.js b/build/matter-0.7.0.min.js deleted file mode 100644 index 99c14e1..0000000 --- a/build/matter-0.7.0.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/** -* matter-0.7.0.min.js 0.7.0-alpha 2014-04-01 -* http://brm.io/matter-js/ -* License: MIT -*/ - -!function(){var a={},b={};!function(){var a=0,c=1;b.create=function(a){var c={id:b.nextId(),type:"body",angle:0,position:{x:0,y:0},force:{x:0,y:0},torque:0,positionImpulse:{x:0,y:0},constraintImpulse:{x:0,y:0,angle:0},speed:0,angularSpeed:0,velocity:{x:0,y:0},angularVelocity:0,isStatic:!1,isSleeping:!1,motion:0,sleepThreshold:60,density:.001,restitution:0,friction:.1,frictionAir:.01,groupId:0,slop:.05,render:{visible:!0,sprite:{xScale:1,yScale:1},path:"L 0 0 L 40 0 L 40 40 L 0 40",lineWidth:1.5}},d=n.extend(c,a);return b.updateProperties(d),d},b.nextId=function(){return a++},b.nextGroupId=function(){return c++},b.updateProperties=function(a){a.vertices=a.vertices||y.fromPath(a.render.path),a.axes=a.axes||v.fromVertices(a.vertices),a.area=y.area(a.vertices),a.bounds=w.create(a.vertices),a.mass=a.mass||a.density*a.area,a.inverseMass=1/a.mass,a.inertia=a.inertia||y.inertia(a.vertices,a.mass),a.inverseInertia=1/a.inertia,a.positionPrev=a.positionPrev||{x:a.position.x,y:a.position.y},a.anglePrev=a.anglePrev||a.angle,a.render.fillStyle=a.render.fillStyle||(a.isStatic?"#eeeeee":n.choose(["#556270","#4ECDC4","#C7F464","#FF6B6B","#C44D58"])),a.render.strokeStyle=a.render.strokeStyle||n.shadeColor(a.render.fillStyle,-20),y.create(a.vertices,a);var b=y.centre(a.vertices);y.translate(a.vertices,a.position),y.translate(a.vertices,b,-1),y.rotate(a.vertices,a.angle,a.position),v.rotate(a.axes,a.angle),w.update(a.bounds,a.vertices,a.velocity),a.isStatic&&(a.restitution=0,a.friction=1,a.mass=a.inertia=a.density=1/0,a.inverseMass=a.inverseInertia=0,a.render.lineWidth=1),s.set(a,a.isSleeping)},b.resetForcesAll=function(a){for(var b=0;be.max.x||g.bounds.max.ye.max.y||b.update(g,c,d)}},b.update=function(a,b,c){var d=b*b,e=1-a.frictionAir,f=a.position.x-a.positionPrev.x,g=a.position.y-a.positionPrev.y;a.velocity.x=f*e*c+a.force.x/a.mass*d,a.velocity.y=g*e*c+a.force.y/a.mass*d,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.position.x+=a.velocity.x,a.position.y+=a.velocity.y,a.angularVelocity=(a.angle-a.anglePrev)*e*c+a.torque/a.inertia*d,a.anglePrev=a.angle,a.angle+=a.angularVelocity,a.speed=x.magnitude(a.velocity),a.angularSpeed=Math.abs(a.angularVelocity),y.translate(a.vertices,a.velocity),0!==a.angularVelocity&&(y.rotate(a.vertices,a.angularVelocity,a.position),v.rotate(a.axes,a.angularVelocity)),w.update(a.bounds,a.vertices,a.velocity)},b.applyForce=function(a,b,c){a.force.x+=c.x,a.force.y+=c.y;var d={x:b.x-a.position.x,y:b.y-a.position.y};a.torque+=(d.x*c.y-d.y*c.x)*a.inverseInertia},b.translate=function(a,b){a.positionPrev.x+=b.x,a.positionPrev.y+=b.y,a.position.x+=b.x,a.position.y+=b.y,y.translate(a.vertices,b),w.update(a.bounds,a.vertices,a.velocity)},b.rotate=function(a,b){a.anglePrev+=b,a.angle+=b,y.rotate(a.vertices,b,a.position),v.rotate(a.axes,b),w.update(a.bounds,a.vertices,a.velocity)}}();var c={};!function(){var a=0;c.create=function(a){return n.extend({id:c.nextId(),type:"composite",parent:null,isModified:!1,bodies:[],constraints:[],composites:[]},a)},c.nextId=function(){return a++},c.setModified=function(a,b,d,e){if(a.isModified=b,d&&a.parent&&c.setModified(a.parent,b,d,e),e)for(var f=0;fq.bounds.width||u.bounds.max.y<0||u.bounds.min.y>q.bounds.height)){var v=b(c,u);if(!u.region||v.id!==u.region.id||k){s.broadphaseTests+=1,(!u.region||k)&&(u.region=v);var w=a(v,u.region);for(m=w.startCol;m<=w.endCol;m++)for(n=w.startRow;n<=w.endRow;n++){p=d(m,n),o=r[p];var x=m>=v.startCol&&m<=v.endCol&&n>=v.startRow&&n<=v.endRow,y=m>=u.region.startCol&&m<=u.region.endCol&&n>=u.region.startRow&&n<=u.region.endRow;!x&&y&&y&&o&&i(c,o,u),(u.region===v||x&&!y||k)&&(o||(o=e(r,p)),f(c,o,u))}u.region=v,t=!0}}}t&&(c.pairsList=j(c))},g.clear=function(a){a.buckets={},a.pairs={},a.pairsList=[]};var a=function(a,b){var d=Math.min(a.startCol,b.startCol),e=Math.max(a.endCol,b.endCol),f=Math.min(a.startRow,b.startRow),g=Math.max(a.endRow,b.endRow);return c(d,e,f,g)},b=function(a,b){var d=b.bounds,e=Math.floor(d.min.x/a.bucketWidth),f=Math.floor(d.max.x/a.bucketWidth),g=Math.floor(d.min.y/a.bucketHeight),h=Math.floor(d.max.y/a.bucketHeight);return c(e,f,g,h)},c=function(a,b,c,d){return{id:a+","+b+","+c+","+d,startCol:a,endCol:b,startRow:c,endRow:d}},d=function(a,b){return a+","+b},e=function(a,b){var c=a[b]=[];return c},f=function(a,b,c){for(var d=0;d0?d.push(c):delete a.pairs[b[e]];return d}}();var h={};!function(){h.create=function(a,b){var c=a.bodyA,d=a.bodyB,e={id:h.id(c,d),bodyA:c,bodyB:d,contacts:{},activeContacts:[],separation:0,isActive:!0,timeCreated:b,timeUpdated:b,inverseMass:c.inverseMass+d.inverseMass,friction:Math.min(c.friction,d.friction),restitution:Math.max(c.restitution,d.restitution),slop:Math.max(c.slop,d.slop)};return h.update(e,a,b),e},h.update=function(a,b,c){var d=a.contacts,f=b.supports,g=a.activeContacts;if(a.collision=b,g.length=0,b.collided){for(var i=0;ia&&j.push(g);for(g=0;gB*e.friction&&(C=B*e.friction*z);var D=x.cross(q,i),E=x.cross(r,i),F=l/(e.inverseMass+g.inverseInertia*D*D+h.inverseInertia*E*E);if(A*=F,C*=F,0>v&&v*v>a)o.normalImpulse=0,o.tangentImpulse=0;else{var G=o.normalImpulse;o.normalImpulse=Math.min(o.normalImpulse+A,0),A=o.normalImpulse-G;var H=o.tangentImpulse;o.tangentImpulse=n.clamp(o.tangentImpulse+C,-y,y),C=o.tangentImpulse-H}c.x=i.x*A+j.x*C,c.y=i.y*A+j.y*C,g.isStatic||g.isSleeping||(g.positionPrev.x+=c.x*g.inverseMass,g.positionPrev.y+=c.y*g.inverseMass,g.anglePrev+=x.cross(q,c)*g.inverseInertia),h.isStatic||h.isSleeping||(h.positionPrev.x-=c.x*h.inverseMass,h.positionPrev.y-=c.y*h.inverseMass,h.anglePrev-=x.cross(r,c)*h.inverseInertia)}}}}}();var k={};!function(){k.collides=function(b,d,e){var f,g,h,i,j=e,k=!1;if(j){var l=b.speed*b.speed+b.angularSpeed*b.angularSpeed+d.speed*d.speed+d.angularSpeed*d.angularSpeed;k=j&&j.collided&&.2>l,i=j}else i={collided:!1,bodyA:b,bodyB:d};if(j&&k){var m=[j.bodyA.axes[j.axisNumber]];if(h=a(j.bodyA.vertices,j.bodyB.vertices,m),i.reused=!0,h.overlap<=0)return i.collided=!1,i}else{if(f=a(b.vertices,d.vertices,b.axes),f.overlap<=0)return i.collided=!1,i;if(g=a(d.vertices,b.vertices,d.axes),g.overlap<=0)return i.collided=!1,i;f.overlap0&&(i.normal=x.neg(i.normal)),i.tangent=x.perp(i.normal),i.penetration={x:i.normal.x*i.depth,y:i.normal.y*i.depth};var n=c(b,d,i.normal),o=[n[0]];if(y.contains(b.vertices,n[1]))o.push(n[1]);else{var p=c(d,b,x.neg(i.normal));y.contains(d.vertices,p[0])&&o.push(p[0]),o.length<2&&y.contains(d.vertices,p[1])&&o.push(p[1])}return i.supports=o,i.supportCorrected=x.sub(n[0],i.penetration),i};var a=function(a,c,d){for(var e,f,g={},h={},i={overlap:Number.MAX_VALUE},j=0;j=e)return i.overlap=e,i;ee?e=g:d>g&&(d=g)}a.min=d,a.max=e},c=function(a,b,c){for(var d,e,f=Number.MAX_VALUE,g={x:0,y:0},h=b.vertices,i=a.position,j=h[0],k=h[1],l=0;ld&&(f=d,j=e);var m=j.index-1>=0?j.index-1:h.length-1;e=h[m],g.x=e.x-i.x,g.y=e.y-i.y,f=-x.dot(c,g),k=e;var n=(j.index+1)%h.length;return e=h[n],g.x=e.x-i.x,g.y=e.y-i.y,d=-x.dot(c,g),f>d&&(f=d,k=e),[j,k]}}();var l={};!function(){var a=1e-6,b=.001,c=0;l.create=function(b){var c=b;c.bodyA&&!c.pointA&&(c.pointA={x:0,y:0}),c.bodyB&&!c.pointB&&(c.pointB={x:0,y:0});var d=c.bodyA?x.add(c.bodyA.position,c.pointA):c.pointA,e=c.bodyB?x.add(c.bodyB.position,c.pointB):c.pointB,f=x.magnitude(x.sub(d,e));c.length=c.length||f||a;var g={visible:!0,lineWidth:2,strokeStyle:"#666"};return c.render=n.extend(g,c.render),c.id=c.id||l.nextId(),c.type="constraint",c.stiffness=c.stiffness||1,c.angularStiffness=c.angularStiffness||0,c.angleA=c.bodyA?c.bodyA.angle:c.angleA,c.angleB=c.bodyB?c.bodyB.angle:c.angleB,c},l.solveAll=function(a){for(var b=0;b0&&(A=0);var B,C={x:m.x*A,y:m.y*A};d&&!d.isStatic&&(B=x.cross(r,C)*d.inverseInertia*(1-c.angularStiffness),s.set(d,!1),B=n.clamp(B,-.01,.01),d.constraintImpulse.x-=o.x,d.constraintImpulse.y-=o.y,d.constraintImpulse.angle+=B,d.position.x-=o.x,d.position.y-=o.y,d.angle+=B),e&&!e.isStatic&&(B=x.cross(t,C)*e.inverseInertia*(1-c.angularStiffness),s.set(e,!1),B=n.clamp(B,-.01,.01),e.constraintImpulse.x+=o.x,e.constraintImpulse.y+=o.y,e.constraintImpulse.angle-=B,e.position.x+=o.x,e.position.y+=o.y,e.angle-=B)}}},l.postSolveAll=function(a){for(var b=0;b>16)+d,f=(c>>8&255)+d,g=(255&c)+d;return"#"+(16777216+65536*(255>e?1>e?0:e:255)+256*(255>f?1>f?0:f:255)+(255>g?1>g?0:g:255)).toString(16).slice(1)},n.shuffle=function(a){for(var b=a.length-1;b>0;b--){var c=Math.floor(Math.random()*(b+1)),d=a[b];a[b]=a[c],a[c]=d}return a},n.choose=function(a){return a[Math.floor(Math.random()*a.length)]},n.isElement=function(a){try{return a instanceof HTMLElement}catch(b){return"object"==typeof a&&1===a.nodeType&&"object"==typeof a.style&&"object"==typeof a.ownerDocument}},n.clamp=function(a,b,c){return b>a?b:a>c?c:a},n.sign=function(a){return 0>a?-1:1},n.now=function(){var a=window.performance;return a?(a.now=a.now||a.webkitNow||a.msNow||a.oNow||a.mozNow,+a.now()):+new Date},n.random=function(a,b){return a+Math.random()*(b-a)},n.colorToNumber=function(a){return a=a.replace("#",""),3==a.length&&(a=a.charAt(0)+a.charAt(0)+a.charAt(1)+a.charAt(1)+a.charAt(2)+a.charAt(2)),parseInt(a,16)},n.log=function(a,b){if(console&&console.log){var c;switch(b){case"warn":c="color: coral";break;case"error":c="color: red"}console.log("%c [Matter] "+b+": "+a,c)}}}();var o={};!function(){var a=60,e=a,h=1e3/a,k=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(function(){a(n.now())},h)};o.create=function(b,c){c=n.isElement(b)?c:b,b=n.isElement(b)?b:null;var e={enabled:!0,positionIterations:6,velocityIterations:4,constraintIterations:2,enableSleeping:!1,timeScale:1,input:{},events:[],timing:{fps:a,timestamp:0,delta:h,correction:1,deltaMin:1e3/a,deltaMax:1e3/(.5*a)},render:{element:b,controller:A}},j=n.extend(e,c);return j.render=j.render.controller.create(j.render),j.world=d.create(j.world),j.pairs=i.create(),j.metrics=j.metrics||q.create(),j.input.mouse=j.input.mouse||r.create(j.render.canvas),j.broadphase=j.broadphase||{current:"grid",grid:{controller:g,instance:g.create(),detector:f.collisions},bruteForce:{detector:f.bruteForce}},j},o.run=function(a){var b,c,d=a.timing,f=0,g=0,i=[];!function j(l){if(k(j),a.enabled){l=l||0;var n={timestamp:l};p.trigger(a,"beforeTick",n),b=l-d.timestamp||h,i.push(b),i=i.slice(-e),b=Math.min.apply(null,i),b=ba.timing.deltaMax?a.timing.deltaMax:b,c=b/d.delta,d.timestamp=l,d.correction=c,d.delta=b,g+=1,l-f>=1e3&&(d.fps=g*((l-f)/1e3),f=l,g=0),p.trigger(a,"tick beforeUpdate",n),a.world.isModified&&a.render.controller.clear(a.render),o.update(a,b,c),t(a),m(a),p.trigger(a,"afterUpdate beforeRender",n),a.render.options.enabled&&a.render.controller.world(a),p.trigger(a,"afterTick afterRender",n)}}()},o.update=function(a,d,e){var f,g=a.world,h=a.broadphase[a.broadphase.current],k=[],m=c.allBodies(g),n=c.allConstraints(g);for(q.reset(a.metrics),a.enableSleeping&&s.update(m),b.applyGravityAll(m,g.gravity),b.updateAll(m,d*a.timeScale,e,g.bounds),f=0;f0&&p.trigger(a,"collisionStart",{pairs:b.collisionStart}),b.collisionActive.length>0&&p.trigger(a,"collisionActive",{pairs:b.collisionActive}),b.collisionEnd.length>0&&p.trigger(a,"collisionEnd",{pairs:b.collisionEnd})}}();var p={};!function(){p.on=function(a,b,c){for(var d,e=b.split(" "),f=0;f0||e.force.y>0)s.set(e,!1);else{var g=Math.min(e.motion,f),h=Math.max(e.motion,f);e.motion=c*g+(1-c)*h,e.sleepThreshold>0&&e.motion=e.sleepThreshold&&s.set(e,!0)):e.sleepCounter>0&&(e.sleepCounter-=1)}}},s.afterCollisions=function(b){for(var c=0;ca&&s.set(h,!1)}}}},s.set=function(a,b){b?(a.isSleeping=!0,a.sleepCounter=a.sleepThreshold,a.positionImpulse.x=0,a.positionImpulse.y=0,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.anglePrev=a.angle,a.speed=0,a.angularSpeed=0,a.motion=0):(a.isSleeping=!1,a.sleepCounter=0)}}();var t={};!function(){t.rectangle=function(a,c,d,e,f){f=f||{};var g={position:{x:a,y:c},render:{path:"L 0 0 L "+d+" 0 L "+d+" "+e+" L 0 "+e}};return b.create(n.extend({},g,f))},t.trapezoid=function(a,c,d,e,f,g){g=g||{},f*=.5;var h=(1-2*f)*d,i=d*f,j=i+h,k=j+i,l={position:{x:a,y:c},render:{path:"L 0 0 L "+i+" "+-e+" L "+j+" "+-e+" L "+k+" 0"}};return b.create(n.extend({},l,g))},t.circle=function(a,b,c,d,e){d=d||{},e=e||25;var f=Math.ceil(Math.max(10,Math.min(e,c)));return f%2===1&&(f+=1),d.circleRadius=c,t.polygon(a,b,f,c,d)},t.polygon=function(a,c,d,e,f){if(f=f||{},3>d)return t.circle(a,c,e,f);for(var g=2*Math.PI/d,h="",i=.5*g,j=0;d>j;j+=1){var k=i+j*g,l=Math.cos(k)*e,m=Math.sin(k)*e;h+="L "+l.toFixed(3)+" "+m.toFixed(3)+" "}var o={position:{x:a,y:c},render:{path:h}};return b.create(n.extend({},o,f))}}();var u={};!function(){u.stack=function(a,d,e,f,g,h,i){for(var j,k=c.create(),l=a,m=d,n=0,o=0;f>o;o++){for(var p=0,q=0;e>q;q++){var r=i(l,m,q,o,j,n);if(r){var s=r.bounds.max.y-r.bounds.min.y,t=r.bounds.max.x-r.bounds.min.x;s>p&&(p=s),b.translate(r,{x:.5*t,y:.5*s}),l=r.bounds.max.x+g,c.addBody(k,r),j=r,n+=1}}m+=p+h,l=a}return k},u.chain=function(a,b,d,e,f,g){for(var h=a.bodies,i=1;ig;g++){for(h=0;b>h;h++)h>0&&(i=m[h-1+g*b],j=m[h+g*b],c.addConstraint(a,l.create(n.extend({bodyA:i,bodyB:j},f))));for(h=0;b>h;h++)g>0&&(i=m[h+(g-1)*b],j=m[h+g*b],c.addConstraint(a,l.create(n.extend({bodyA:i,bodyB:j},f))),e&&h>0&&(k=m[h-1+(g-1)*b],c.addConstraint(a,l.create(n.extend({bodyA:k,bodyB:j},f)))),e&&b-1>h&&(k=m[h+1+(g-1)*b],c.addConstraint(a,l.create(n.extend({bodyA:k,bodyB:j},f)))))}return a},u.pyramid=function(a,c,d,e,f,g,h){return u.stack(a,c,d,e,f,g,function(c,g,i,j,k,l){var m=Math.min(e,Math.ceil(d/2)),n=k?k.bounds.max.x-k.bounds.min.x:0;if(!(j>m)){j=m-j;var o=j,p=d-1-j;if(!(o>i||i>p)){1===l&&b.translate(k,{x:(i+(d%2===1?1:-1))*n,y:0});var q=k?i*n:0;return h(a+q+i*f,g,i,j,k,l)}}})},u.newtonsCradle=function(a,b,d,e,f){for(var g=c.create(),h=0;d>h;h++){var i=1.9,j=t.circle(a+h*e*i,b+f,e,{inertia:99999,restitution:1,friction:0,frictionAir:1e-4,slop:.01}),k=l.create({pointA:{x:a+h*e*i,y:b},bodyB:j});c.addBody(g,j),c.addConstraint(g,k)}return g},u.car=function(a,d,e,f,g){var h=b.nextGroupId(),i=-20,j=.5*-e+i,k=.5*e-i,m=0,n=c.create(),o=t.trapezoid(a,d,e,f,.3,{groupId:h,friction:.01}),p=t.circle(a+j,d+m,g,{groupId:h,restitution:.5,friction:.9,density:.01}),q=t.circle(a+k,d+m,g,{groupId:h,restitution:.5,friction:.9,density:.01}),r=l.create({bodyA:o,pointA:{x:j,y:m},bodyB:p,stiffness:.5}),s=l.create({bodyA:o,pointA:{x:k,y:m},bodyB:q,stiffness:.5});return c.addBody(n,o),c.addBody(n,p),c.addBody(n,q),c.addConstraint(n,r),c.addConstraint(n,s),n},u.softBody=function(a,b,c,d,e,f,g,h,i,j){i=n.extend({inertia:1/0},i),j=n.extend({stiffness:.4},j);var k=u.stack(a,b,c,d,e,f,function(a,b){return t.circle(a,b,h,i)});return u.mesh(k,c,d,g,j),k}}();var v={};!function(){v.fromVertices=function(a){for(var b={},c=0;ca.max.x&&(a.max.x=e.x),e.xa.max.y&&(a.max.y=e.y),e.y0?a.max.x+=c.x:a.min.x+=c.x,c.y>0?a.max.y+=c.y:a.min.y+=c.y)},w.contains=function(a,b){return b.x>=a.min.x&&b.x<=a.max.x&&b.y>=a.min.y&&b.y<=a.max.y},w.overlaps=function(a,b){return a.min.x<=b.max.x&&a.max.x>=b.min.x&&a.max.y>=b.min.y&&a.min.y<=b.max.y}}();var x={};!function(){x.magnitude=function(a){return Math.sqrt(a.x*a.x+a.y*a.y)},x.magnitudeSquared=function(a){return a.x*a.x+a.y*a.y},x.rotate=function(a,b){var c=Math.cos(b),d=Math.sin(b);return{x:a.x*c-a.y*d,y:a.x*d+a.y*c}},x.rotateAbout=function(a,b,c){var d=Math.cos(b),e=Math.sin(b);return{x:c.x+((a.x-c.x)*d-(a.y-c.y)*e),y:c.y+((a.x-c.x)*e+(a.y-c.y)*d)}},x.normalise=function(a){var b=x.magnitude(a);return 0===b?{x:0,y:0}:{x:a.x/b,y:a.y/b}},x.dot=function(a,b){return a.x*b.x+a.y*b.y},x.cross=function(a,b){return a.x*b.y-a.y*b.x},x.add=function(a,b){return{x:a.x+b.x,y:a.y+b.y}},x.sub=function(a,b){return{x:a.x-b.x,y:a.y-b.y}},x.mult=function(a,b){return{x:a.x*b,y:a.y*b}},x.div=function(a,b){return{x:a.x/b,y:a.y/b}},x.perp=function(a,b){return b=b===!0?-1:1,{x:b*-a.y,y:b*a.x}},x.neg=function(a){return{x:-a.x,y:-a.y}}}();var y={};!function(){y.create=function(a,b){for(var c=0;c0)return!1}return!0}}();var z={};!function(){z.create=function(a,b){var e,f=window.dat&&window.localStorage;if(!f)return void console.log("Could not create GUI. Check dat.gui library is loaded first.");var g=new dat.GUI(b);Resurrect?(e=new Resurrect({prefix:"$"}),e.parse=e.resurrect):e=JSON;var h={datGui:g,amount:1,size:40,sides:4,density:.001,restitution:0,friction:.1,frictionAir:.01,renderer:"canvas"},i={addBody:function(){for(var b={density:h.density,friction:h.friction,frictionAir:h.frictionAir,restitution:h.restitution},c=0;c=500){var k="";k+="fps: "+Math.round(a.timing.fps)+j,a.metrics.extended&&(k+="delta: "+a.timing.delta.toFixed(3)+j,k+="correction: "+a.timing.correction.toFixed(3)+j,k+="bodies: "+i.length+j,a.broadphase.controller===g&&(k+="buckets: "+a.metrics.buckets+j),k+="\n",k+="collisions: "+a.metrics.collisions+j,k+="pairs: "+a.pairs.list.length+j,k+="broad: "+a.metrics.broadEff+j,k+="mid: "+a.metrics.midEff+j,k+="narrow: "+a.metrics.narrowEff+j),f.debugString=k,f.debugTimestamp=a.timing.timestamp}if(f.debugString){d.font="12px Arial",d.fillStyle=h.wireframes?"rgba(255,255,255,0.5)":"rgba(0,0,0,0.5)";for(var l=f.debugString.split("\n"),m=0;m0){var l=d.activeContacts[0].vertex.x,m=d.activeContacts[0].vertex.y;2===d.activeContacts.length&&(l=(d.activeContacts[0].vertex.x+d.activeContacts[1].vertex.x)/2,m=(d.activeContacts[0].vertex.y+d.activeContacts[1].vertex.y)/2),h.moveTo(l-8*e.normal.x,m-8*e.normal.y),h.lineTo(l,m)}h.strokeStyle=i.wireframes?"rgba(255,165,0,0.7)":"orange",h.lineWidth=1,h.stroke()},A.grid=function(a,b,c){var d=c,e=a.render.options;d.strokeStyle=e.wireframes?"rgba(255,180,0,0.1)":"rgba(255,180,0,0.5)",d.beginPath();for(var f=n.keys(b.buckets),g=0;g worldBounds.max.y) continue; - Body.update(body, deltaTime, correction); + Body.update(body, deltaTime, timeScale, correction); } }; @@ -225,13 +236,14 @@ var Body = {}; * @method update * @param {body} body * @param {number} deltaTime + * @param {number} timeScale * @param {number} correction */ - Body.update = function(body, deltaTime, correction) { - var deltaTimeSquared = deltaTime * deltaTime; + Body.update = function(body, deltaTime, timeScale, correction) { + var deltaTimeSquared = Math.pow(deltaTime * timeScale * body.timeScale, 2); // from the previous step - var frictionAir = 1 - body.frictionAir, + var frictionAir = 1 - body.frictionAir * timeScale * body.timeScale, velocityPrevX = body.position.x - body.positionPrev.x, velocityPrevY = body.position.y - body.positionPrev.y; @@ -305,6 +317,34 @@ var Body = {}; Bounds.update(body.bounds, body.vertices, body.velocity); }; + /** + * Scales the body, including updating physical properties (mass, area, axes, inertia), from a point (default is centre) + * @method translate + * @param {body} body + * @param {number} scaleX + * @param {number} scaleY + * @param {vector} point + */ + Body.scale = function(body, scaleX, scaleY, point) { + // scale vertices + Vertices.scale(body.vertices, scaleX, scaleY, point); + + // update properties + body.axes = Axes.fromVertices(body.vertices); + body.area = Vertices.area(body.vertices); + body.mass = body.density * body.area; + body.inverseMass = 1 / body.mass; + + // update inertia (requires vertices to be at origin) + Vertices.translate(body.vertices, { x: -body.position.x, y: -body.position.y }); + body.inertia = Vertices.inertia(body.vertices, body.mass); + body.inverseInertia = 1 / body.inertia; + Vertices.translate(body.vertices, { x: body.position.x, y: body.position.y }); + + // update bounds + Bounds.update(body.bounds, body.vertices, body.velocity); + }; + })(); ; // End src/body/Body.js @@ -325,8 +365,6 @@ var Composite = {}; (function() { - var _nextId = 0; - /** * Description * @method create @@ -335,25 +373,17 @@ var Composite = {}; */ Composite.create = function(options) { return Common.extend({ - id: Composite.nextId(), + id: Common.nextId(), type: 'composite', parent: null, isModified: false, bodies: [], constraints: [], - composites: [] + composites: [], + label: 'Composite' }, options); }; - /** - * Returns the next unique compositeID - * @method nextId - * @return {Number} Unique compositeID - */ - Composite.nextId = function() { - return _nextId++; - }; - /** * Sets the composite's `isModified` flag. * If `updateParents` is true, all parents will be set (default: false). @@ -672,6 +702,74 @@ var Composite = {}; return composites; }; + /** + * Searches the composite recursively for an object matching the type and id supplied, null if not found + * @method get + * @param {composite} composite + * @param {number} id + * @param {string} type + * @return {object} The requested object, if found + */ + Composite.get = function(composite, id, type) { + var objects, + object; + + switch (type) { + case 'body': + objects = Composite.allBodies(composite); + break; + case 'constraint': + objects = Composite.allConstraints(composite); + break; + case 'composite': + objects = Composite.allComposites(composite).concat(composite); + break; + } + + if (!objects) + return null; + + object = objects.filter(function(object) { + return object.id.toString() === id.toString(); + }); + + return object.length === 0 ? null : object[0]; + }; + + /** + * Moves the given object(s) from compositeA to compositeB (equal to a remove followed by an add) + * @method move + * @param {compositeA} compositeA + * @param {object[]} objects + * @param {compositeB} compositeB + * @return {composite} Returns compositeA + */ + Composite.move = function(compositeA, objects, compositeB) { + Composite.remove(compositeA, objects); + Composite.add(compositeB, objects); + return compositeA; + }; + + /** + * Assigns new ids for all objects in the composite, recursively + * @method rebase + * @param {composite} composite + * @return {composite} Returns composite + */ + Composite.rebase = function(composite) { + var objects = Composite.allBodies(composite) + .concat(Composite.allConstraints(composite)) + .concat(Composite.allComposites(composite)); + + for (var i = 0; i < objects.length; i++) { + objects[i].id = Common.nextId(); + } + + Composite.setModified(composite, true, true, false); + + return composite; + }; + })(); ; // End src/body/Composite.js @@ -701,6 +799,7 @@ var World = {}; var composite = Composite.create(); var defaults = { + label: 'World', gravity: { x: 0, y: 1 }, bounds: { min: { x: 0, y: 0 }, @@ -1497,6 +1596,77 @@ var Pairs = {}; ; // End src/collision/Pairs.js +// Begin src/collision/Query.js + +/** +* Functions for performing collision queries +* +* @class Query +*/ + +var Query = {}; + +(function() { + + /** + * Casts a ray segment against a set of bodies and returns all collisions, ray width is optional. Intersection points are not provided. + * @method ray + * @param {body[]} bodies + * @param {vector} startPoint + * @param {vector} endPoint + * @return {object[]} Collisions + */ + Query.ray = function(bodies, startPoint, endPoint, rayWidth) { + rayWidth = rayWidth || Number.MIN_VALUE; + + var rayAngle = Vector.angle(startPoint, endPoint), + rayLength = Vector.magnitude(Vector.sub(startPoint, endPoint)), + rayX = (endPoint.x + startPoint.x) * 0.5, + rayY = (endPoint.y + startPoint.y) * 0.5, + ray = Bodies.rectangle(rayX, rayY, rayLength, rayWidth, { angle: rayAngle }), + collisions = []; + + for (var i = 0; i < bodies.length; i++) { + var bodyA = bodies[i]; + + if (Bounds.overlaps(bodyA.bounds, ray.bounds)) { + var collision = SAT.collides(bodyA, ray); + if (collision.collided) { + collision.body = collision.bodyA = collision.bodyB = bodyA; + collisions.push(collision); + } + } + } + + return collisions; + }; + + /** + * Returns all bodies whose bounds are inside (or outside if set) the given set of bounds, from the given set of bodies + * @method region + * @param {body[]} bodies + * @param {bounds} bounds + * @param {bool} outside + * @return {body[]} The bodies matching the query + */ + Query.region = function(bodies, bounds, outside) { + var result = []; + + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i], + overlaps = Bounds.overlaps(body.bounds, bounds); + if ((overlaps && !outside) || (!overlaps && outside)) + result.push(body); + } + + return result; + }; + +})(); + +; // End src/collision/Query.js + + // Begin src/collision/Resolver.js /** @@ -1517,8 +1687,9 @@ var Resolver = {}; * Description * @method solvePosition * @param {pair[]} pairs + * @param {number} timeScale */ - Resolver.solvePosition = function(pairs) { + Resolver.solvePosition = function(pairs, timeScale) { var i, pair, collision, @@ -1560,7 +1731,7 @@ var Resolver = {}; bodyA = collision.bodyA; bodyB = collision.bodyB; normal = collision.normal; - positionImpulse = (pair.separation * _positionDampen) - pair.slop; + positionImpulse = ((pair.separation * _positionDampen) - pair.slop) * timeScale; if (bodyA.isStatic || bodyB.isStatic) positionImpulse *= 2; @@ -1673,8 +1844,9 @@ var Resolver = {}; * @method solveVelocity * @param {pair[]} pairs */ - Resolver.solveVelocity = function(pairs) { - var impulse = {}; + Resolver.solveVelocity = function(pairs, timeScale) { + var impulse = {}, + timeScaleSquared = timeScale * timeScale; for (var i = 0; i < pairs.length; i++) { var pair = pairs[i]; @@ -1719,8 +1891,8 @@ var Resolver = {}; // coulomb friction var tangentImpulse = tangentVelocity; - if (tangentSpeed > normalForce * pair.friction) - tangentImpulse = normalForce * pair.friction * tangentVelocityDirection; + if (tangentSpeed > normalForce * pair.friction * timeScaleSquared) + tangentImpulse = normalForce * pair.friction * timeScaleSquared * tangentVelocityDirection; // modify impulses accounting for mass, inertia and offset var oAcN = Vector.cross(offsetA, normal), @@ -1730,7 +1902,7 @@ var Resolver = {}; tangentImpulse *= share; // handle high velocity and resting collisions separately - if (normalVelocity < 0 && normalVelocity * normalVelocity > _restingThresh) { + if (normalVelocity < 0 && normalVelocity * normalVelocity > _restingThresh * timeScaleSquared) { // high velocity so clear cached contact impulse contact.normalImpulse = 0; contact.tangentImpulse = 0; @@ -2050,8 +2222,7 @@ var Constraint = {}; (function() { var _minLength = 0.000001, - _minDifference = 0.001, - _nextId = 0; + _minDifference = 0.001; /** * Description @@ -2085,7 +2256,8 @@ var Constraint = {}; constraint.render = Common.extend(render, constraint.render); // option defaults - constraint.id = constraint.id || Constraint.nextId(); + constraint.id = constraint.id || Common.nextId(); + constraint.label = constraint.label || 'Constraint'; constraint.type = 'constraint'; constraint.stiffness = constraint.stiffness || 1; constraint.angularStiffness = constraint.angularStiffness || 0; @@ -2099,10 +2271,11 @@ var Constraint = {}; * Description * @method solveAll * @param {constraint[]} constraints + * @param {number} timeScale */ - Constraint.solveAll = function(constraints) { + Constraint.solveAll = function(constraints, timeScale) { for (var i = 0; i < constraints.length; i++) { - Constraint.solve(constraints[i]); + Constraint.solve(constraints[i], timeScale); } }; @@ -2110,8 +2283,9 @@ var Constraint = {}; * Description * @method solve * @param {constraint} constraint + * @param {number} timeScale */ - Constraint.solve = function(constraint) { + Constraint.solve = function(constraint, timeScale) { var bodyA = constraint.bodyA, bodyB = constraint.bodyB, pointA = constraint.pointA, @@ -2148,10 +2322,10 @@ var Constraint = {}; // 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); + force = Vector.mult(delta, difference * 0.5 * constraint.stiffness * timeScale * timeScale); // if difference is very small, we can skip - if (Math.abs(1 - (currentLength / constraint.length)) < _minDifference) + if (Math.abs(1 - (currentLength / constraint.length)) < _minDifference * timeScale) return; var velocityPointA, @@ -2285,15 +2459,6 @@ var Constraint = {}; } }; - /** - * Returns the next unique constraintId - * @method nextId - * @return {Number} Unique constraintId - */ - Constraint.nextId = function() { - return _nextId++; - }; - })(); ; // End src/constraint/Constraint.js @@ -2323,6 +2488,7 @@ var MouseConstraint = {}; var mouse = engine.input.mouse; var constraint = Constraint.create({ + label: 'Mouse Constraint', pointA: mouse.position, pointB: { x: 0, y: 0 }, length: 0.01, @@ -2404,6 +2570,8 @@ var Common = {}; (function() { + Common._nextId = 0; + /** * Description * @method extend @@ -2663,6 +2831,15 @@ var Common = {}; console.log('%c [Matter] ' + type + ': ' + message, style); }; + /** + * Returns the next unique sequential ID + * @method nextId + * @return {Number} Unique sequential ID + */ + Common.nextId = function() { + return Common._nextId++; + }; + })(); ; // End src/core/Common.js @@ -2677,9 +2854,7 @@ var Common = {}; * @class Engine */ -// TODO: multiple event handlers, before & after handlers // TODO: viewports -// TODO: frameskipping var Engine = {}; @@ -2721,7 +2896,9 @@ var Engine = {}; delta: _delta, correction: 1, deltaMin: 1000 / _fps, - deltaMax: 1000 / (_fps * 0.5) + deltaMax: 1000 / (_fps * 0.5), + timeScale: 1, + isFixed: false }, render: { element: element, @@ -2753,89 +2930,79 @@ var Engine = {}; }; /** - * Description + * An optional utility function that provides a game loop, that handles updating the engine for you. + * Calls `Engine.update` and `Engine.render` on the `requestAnimationFrame` event automatically. + * Handles time correction and non-fixed dynamic timing (if enabled). + * Triggers `beforeTick`, `tick` and `afterTick` events. * @method run * @param {engine} engine */ Engine.run = function(engine) { - var timing = engine.timing, - delta, - correction, - counterTimestamp = 0, + var counterTimestamp = 0, frameCounter = 0, - deltaHistory = []; + deltaHistory = [], + timePrev, + timeScalePrev = 1; - (function render(timestamp){ + (function render(time){ _requestAnimationFrame(render); if (!engine.enabled) return; - // timestamp is undefined on the first update - timestamp = timestamp || 0; + var timing = engine.timing, + delta, + correction; // create an event object var event = { - timestamp: timestamp + timestamp: time }; - /** - * Fired at the start of a tick, before any updates to the engine or timing - * - * @event beforeTick - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ Events.trigger(engine, 'beforeTick', event); - delta = (timestamp - timing.timestamp) || _delta; + if (timing.isFixed) { + // fixed timestep + delta = timing.delta; + } else { + // dynamic timestep based on wall clock between calls + delta = (time - timePrev) || timing.delta; + timePrev = time; - // optimistically filter delta over a few frames, to improve stability - deltaHistory.push(delta); - deltaHistory = deltaHistory.slice(-_deltaSampleSize); - delta = Math.min.apply(null, deltaHistory); - - // limit delta - delta = delta < engine.timing.deltaMin ? engine.timing.deltaMin : delta; - delta = delta > engine.timing.deltaMax ? engine.timing.deltaMax : 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 < timing.deltaMin ? timing.deltaMin : delta; + delta = delta > timing.deltaMax ? timing.deltaMax : delta; - // verlet time correction - correction = delta / timing.delta; + // time correction for delta + correction = delta / timing.delta; - // update engine timing object - timing.timestamp = timestamp; - timing.correction = correction; - timing.delta = delta; + // update engine timing object + timing.delta = delta; + } + + // time correction for time scaling + if (timeScalePrev !== 0) + correction *= timing.timeScale / timeScalePrev; + + if (timing.timeScale === 0) + correction = 0; + + timeScalePrev = timing.timeScale; // fps counter frameCounter += 1; - if (timestamp - counterTimestamp >= 1000) { - timing.fps = frameCounter * ((timestamp - counterTimestamp) / 1000); - counterTimestamp = timestamp; + if (time - counterTimestamp >= 1000) { + timing.fps = frameCounter * ((time - counterTimestamp) / 1000); + counterTimestamp = time; frameCounter = 0; } - /** - * Fired after engine timing updated, but just before engine state updated - * - * @event tick - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - /** - * Fired just before an update - * - * @event beforeUpdate - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(engine, 'tick beforeUpdate', event); + Events.trigger(engine, 'tick', event); // if world has been modified, clear the render scene graph if (engine.world.isModified) @@ -2848,54 +3015,15 @@ var Engine = {}; _triggerCollisionEvents(engine); _triggerMouseEvents(engine); - /** - * Fired after engine update and all collision events - * - * @event afterUpdate - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - /** - * Fired just before rendering - * - * @event beforeRender - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(engine, 'afterUpdate beforeRender', event); - // render - if (engine.render.options.enabled) - engine.render.controller.world(engine); + Engine.render(engine); - /** - * Fired after rendering - * - * @event afterRender - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - /** - * Fired after engine update and after rendering - * - * @event afterTick - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(engine, 'afterTick afterRender', event); + Events.trigger(engine, 'afterTick', event); })(); }; /** - * Description + * Moves the simulation forward in time by `delta` ms. Triggers `beforeUpdate` and `afterUpdate` events. * @method update * @param {engine} engine * @param {number} delta @@ -2903,11 +3031,25 @@ var Engine = {}; * @return engine */ Engine.update = function(engine, delta, correction) { + correction = (typeof correction !== 'undefined') ? correction : 1; + var world = engine.world, + timing = engine.timing, broadphase = engine.broadphase[engine.broadphase.current], broadphasePairs = [], i; + // increment timestamp + timing.timestamp += delta * timing.timeScale; + timing.correction = correction; + + // create an event object + var event = { + timestamp: engine.timing.timestamp + }; + + Events.trigger(engine, 'beforeUpdate', event); + // get lists of all bodies and constraints, no matter what composites they are in var allBodies = Composite.allBodies(world), allConstraints = Composite.allConstraints(world); @@ -2923,11 +3065,11 @@ var Engine = {}; Body.applyGravityAll(allBodies, world.gravity); // update all body position and rotation by integration - Body.updateAll(allBodies, delta * engine.timeScale, correction, world.bounds); + Body.updateAll(allBodies, delta, timing.timeScale, correction, world.bounds); // update all constraints for (i = 0; i < engine.constraintIterations; i++) { - Constraint.solveAll(allConstraints); + Constraint.solveAll(allConstraints, timing.timeScale); } Constraint.postSolveAll(allBodies); @@ -2952,7 +3094,7 @@ var Engine = {}; // update collision pairs var pairs = engine.pairs, - timestamp = engine.timing.timestamp; + timestamp = timing.timestamp; Pairs.update(pairs, collisions, timestamp); Pairs.removeOld(pairs, timestamp); @@ -2963,12 +3105,12 @@ var Engine = {}; // iteratively resolve velocity between collisions Resolver.preSolveVelocity(pairs.list); for (i = 0; i < engine.velocityIterations; i++) { - Resolver.solveVelocity(pairs.list); + Resolver.solveVelocity(pairs.list, timing.timeScale); } // iteratively resolve position between collisions for (i = 0; i < engine.positionIterations; i++) { - Resolver.solvePosition(pairs.list); + Resolver.solvePosition(pairs.list, timing.timeScale); } Resolver.postSolvePosition(allBodies); @@ -2982,8 +3124,27 @@ var Engine = {}; if (world.isModified) Composite.setModified(world, false, false, true); + Events.trigger(engine, 'afterUpdate', event); + return engine; }; + + /** + * Renders the world by calling its defined renderer `engine.render.controller`. Triggers `beforeRender` and `afterRender` events. + * @method render + * @param {engine} engineA + * @param {engine} engineB + */ + Engine.render = function(engine) { + // create an event object + var event = { + timestamp: engine.timing.timestamp + }; + + Events.trigger(engine, 'beforeRender', event); + engine.render.controller.world(engine); + Events.trigger(engine, 'afterRender', event); + }; /** * Description @@ -3004,7 +3165,7 @@ var Engine = {}; for (var i = 0; i < bodies.length; i++) { var body = bodies[i]; Sleeping.set(body, false); - body.id = Body.nextId(); + body.id = Common.nextId(); } } }; @@ -3037,45 +3198,18 @@ var Engine = {}; var mouse = engine.input.mouse, mouseEvents = mouse.sourceEvents; - /** - * Fired when the mouse has moved (or a touch moves) during the last step - * - * @event mousemove - * @param {} event An event object - * @param {mouse} event.mouse The engine's mouse instance - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (mouseEvents.mousemove) { Events.trigger(engine, 'mousemove', { mouse: mouse }); } - /** - * Fired when the mouse is down (or a touch has started) during the last step - * - * @event mousedown - * @param {} event An event object - * @param {mouse} event.mouse The engine's mouse instance - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (mouseEvents.mousedown) { Events.trigger(engine, 'mousedown', { mouse: mouse }); } - /** - * Fired when the mouse is up (or a touch has ended) during the last step - * - * @event mouseup - * @param {} event An event object - * @param {mouse} event.mouse The engine's mouse instance - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (mouseEvents.mouseup) { Events.trigger(engine, 'mouseup', { mouse: mouse @@ -3095,48 +3229,18 @@ var Engine = {}; var _triggerCollisionEvents = function(engine) { var pairs = engine.pairs; - /** - * Fired after engine update, provides a list of all pairs that have started to collide in the current tick (if any) - * - * @event collisionStart - * @param {} event An event object - * @param {} event.pairs List of affected pairs - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (pairs.collisionStart.length > 0) { Events.trigger(engine, 'collisionStart', { pairs: pairs.collisionStart }); } - /** - * Fired after engine update, provides a list of all pairs that are colliding in the current tick (if any) - * - * @event collisionActive - * @param {} event An event object - * @param {} event.pairs List of affected pairs - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (pairs.collisionActive.length > 0) { Events.trigger(engine, 'collisionActive', { pairs: pairs.collisionActive }); } - /** - * Fired after engine update, provides a list of all pairs that have ended collision in the current tick (if any) - * - * @event collisionEnd - * @param {} event An event object - * @param {} event.pairs List of affected pairs - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (pairs.collisionEnd.length > 0) { Events.trigger(engine, 'collisionEnd', { pairs: pairs.collisionEnd @@ -3144,6 +3248,145 @@ var Engine = {}; } }; + /* + * + * Events Documentation + * + */ + + /** + * Fired at the start of a tick, before any updates to the engine or timing + * + * @event beforeTick + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine timing updated, but just before engine state updated + * + * @event tick + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired just before an update + * + * @event beforeUpdate + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine update and all collision events + * + * @event afterUpdate + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired just before rendering + * + * @event beforeRender + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after rendering + * + * @event afterRender + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine update and after rendering + * + * @event afterTick + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired when the mouse has moved (or a touch moves) during the last step + * + * @event mousemove + * @param {} event An event object + * @param {mouse} event.mouse The engine's mouse instance + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired when the mouse is down (or a touch has started) during the last step + * + * @event mousedown + * @param {} event An event object + * @param {mouse} event.mouse The engine's mouse instance + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired when the mouse is up (or a touch has ended) during the last step + * + * @event mouseup + * @param {} event An event object + * @param {mouse} event.mouse The engine's mouse instance + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine update, provides a list of all pairs that have started to collide in the current tick (if any) + * + * @event collisionStart + * @param {} event An event object + * @param {} event.pairs List of affected pairs + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine update, provides a list of all pairs that are colliding in the current tick (if any) + * + * @event collisionActive + * @param {} event An event object + * @param {} event.pairs List of affected pairs + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine update, provides a list of all pairs that have ended collision in the current tick (if any) + * + * @event collisionEnd + * @param {} event An event object + * @param {} event.pairs List of affected pairs + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + })(); ; // End src/core/Engine.js @@ -3179,23 +3422,43 @@ var Events = {}; object.events[name] = object.events[name] || []; object.events[name].push(callback); } + + return callback; }; /** - * Clears all callbacks for the given event names if supplied, otherwise all events + * Removes the given event callback. If no callback, clears all callbacks in eventNames. If no eventNames, clears all events. * @method off * @param {} object * @param {string} eventNames + * @param {function} callback */ - Events.off = function(object, eventNames) { + Events.off = function(object, eventNames, callback) { if (!eventNames) { object.events = {}; return; } + // handle Events.off(object, callback) + if (typeof eventNames === 'function') { + callback = eventNames; + eventNames = Common.keys(object.events).join(' '); + } + var names = eventNames.split(' '); + for (var i = 0; i < names.length; i++) { - object.events[names[i]] = []; + var callbacks = object.events[names[i]], + newCallbacks = []; + + if (callback) { + for (var j = 0; j < callbacks.length; j++) { + if (callbacks[j] !== callback) + newCallbacks.push(callbacks[j]); + } + } + + object.events[names[i]] = newCallbacks; } }; @@ -3348,15 +3611,20 @@ var Mouse; var mouse = this; this.element = element || document.body; + this.absolute = { x: 0, y: 0 }; this.position = { x: 0, y: 0 }; this.mousedownPosition = { x: 0, y: 0 }; this.mouseupPosition = { x: 0, y: 0 }; + this.offset = { x: 0, y: 0 }; + this.scale = { x: 1, y: 1 }; + this.wheelDelta = 0; this.button = -1; this.sourceEvents = { mousemove: null, mousedown: null, - mouseup: null + mouseup: null, + mousewheel: null }; this.mousemove = function(event) { @@ -3368,7 +3636,10 @@ var Mouse; event.preventDefault(); } - mouse.position = position; + mouse.absolute.x = position.x; + mouse.absolute.y = position.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; mouse.sourceEvents.mousemove = event; }; @@ -3383,7 +3654,12 @@ var Mouse; mouse.button = event.button; } - mouse.position = mouse.mousedownPosition = position; + mouse.absolute.x = position.x; + mouse.absolute.y = position.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; + mouse.mousedownPosition.x = mouse.position.x; + mouse.mousedownPosition.y = mouse.position.y; mouse.sourceEvents.mousedown = event; }; @@ -3396,10 +3672,20 @@ var Mouse; } mouse.button = -1; - mouse.position = mouse.mouseupPosition = position; + mouse.absolute.x = position.x; + mouse.absolute.y = position.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; + mouse.mouseupPosition.x = mouse.position.x; + mouse.mouseupPosition.y = mouse.position.y; mouse.sourceEvents.mouseup = event; }; + this.mousewheel = function(event) { + mouse.wheelDelta = Math.max(-1, Math.min(1, event.wheelDelta || -event.detail)); + event.preventDefault(); + }; + Mouse.setElement(mouse, mouse.element); }; @@ -3426,6 +3712,9 @@ var Mouse; element.addEventListener('mousedown', mouse.mousedown); element.addEventListener('mouseup', mouse.mouseup); + element.addEventListener("mousewheel", mouse.mousewheel); + element.addEventListener("DOMMouseScroll", mouse.mousewheel); + element.addEventListener('touchmove', mouse.mousemove); element.addEventListener('touchstart', mouse.mousedown); element.addEventListener('touchend', mouse.mouseup); @@ -3440,6 +3729,32 @@ var Mouse; mouse.sourceEvents.mousemove = null; mouse.sourceEvents.mousedown = null; mouse.sourceEvents.mouseup = null; + mouse.sourceEvents.mousewheel = null; + mouse.wheelDelta = 0; + }; + + /** + * Sets the offset + * @method setOffset + * @param {mouse} mouse + */ + Mouse.setOffset = function(mouse, offset) { + mouse.offset.x = offset.x; + mouse.offset.y = offset.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; + }; + + /** + * Sets the scale + * @method setScale + * @param {mouse} mouse + */ + Mouse.setScale = function(mouse, scale) { + mouse.scale.x = scale.x; + mouse.scale.y = scale.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; }; /** @@ -3452,8 +3767,9 @@ var Mouse; */ 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, + rootNode = (document.documentElement || document.body.parentNode || document.body), + scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset : rootNode.scrollLeft, + scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset : rootNode.scrollTop, touches = event.changedTouches, x, y; @@ -3620,12 +3936,18 @@ var Bodies = {}; options = options || {}; var rectangle = { + label: 'Rectangle Body', position: { x: x, y: y }, - render: { - path: 'L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height - } + vertices: Vertices.fromPath('L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height) }; + if (options.chamfer) { + var chamfer = options.chamfer; + rectangle.vertices = Vertices.chamfer(rectangle.vertices, chamfer.radius, + chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); + delete options.chamfer; + } + return Body.create(Common.extend({}, rectangle, options)); }; @@ -3651,12 +3973,18 @@ var Bodies = {}; x3 = x2 + x1; var trapezoid = { + label: 'Trapezoid Body', position: { x: x, y: y }, - render: { - path: 'L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0' - } + vertices: Vertices.fromPath('L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0') }; + if (options.chamfer) { + var chamfer = options.chamfer; + trapezoid.vertices = Vertices.chamfer(trapezoid.vertices, chamfer.radius, + chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); + delete options.chamfer; + } + return Body.create(Common.extend({}, trapezoid, options)); }; @@ -3672,6 +4000,7 @@ var Bodies = {}; */ Bodies.circle = function(x, y, radius, options, maxSides) { options = options || {}; + options.label = 'Circle Body'; // approximate circles with polygons until true circles implemented in SAT @@ -3717,12 +4046,18 @@ var Bodies = {}; } var polygon = { + label: 'Polygon Body', position: { x: x, y: y }, - render: { - path: path - } + vertices: Vertices.fromPath(path) }; + if (options.chamfer) { + var chamfer = options.chamfer; + polygon.vertices = Vertices.chamfer(polygon.vertices, chamfer.radius, + chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); + delete options.chamfer; + } + return Body.create(Common.extend({}, polygon, options)); }; @@ -3757,7 +4092,7 @@ var Composites = {}; * @return {composite} A new composite containing objects created in the callback */ Composites.stack = function(xx, yy, columns, rows, columnGap, rowGap, callback) { - var stack = Composite.create(), + var stack = Composite.create({ label: 'Stack' }), x = xx, y = yy, lastBody, @@ -3827,6 +4162,8 @@ var Composites = {}; Composite.addConstraint(composite, Constraint.create(constraint)); } + + composite.label += ' Chain'; return composite; }; @@ -3876,6 +4213,8 @@ var Composites = {}; } } } + + composite.label += ' Mesh'; return composite; }; @@ -3931,7 +4270,7 @@ var Composites = {}; * @return {composite} A new composite newtonsCradle body */ Composites.newtonsCradle = function(xx, yy, number, size, length) { - var newtonsCradle = Composite.create(); + var newtonsCradle = Composite.create({ label: 'Newtons Cradle' }); for (var i = 0; i < number; i++) { var separation = 1.9, @@ -3963,8 +4302,14 @@ var Composites = {}; 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 car = Composite.create({ label: 'Car' }), + body = Bodies.trapezoid(xx, yy, width, height, 0.3, { + groupId: groupId, + friction: 0.01, + chamfer: { + radius: 10 + } + }); var wheelA = Bodies.circle(xx + wheelAOffset, yy + wheelYOffset, wheelSize, { groupId: groupId, @@ -4028,6 +4373,8 @@ var Composites = {}; Composites.mesh(softBody, columns, rows, crossBrace, constraintOptions); + softBody.label = 'Soft Body'; + return softBody; }; @@ -4125,7 +4472,10 @@ var Bounds = {}; min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }; - Bounds.update(bounds, vertices); + + if (vertices) + Bounds.update(bounds, vertices); + return bounds; }; @@ -4188,6 +4538,35 @@ var Bounds = {}; return (boundsA.min.x <= boundsB.max.x && boundsA.max.x >= boundsB.min.x && boundsA.max.y >= boundsB.min.y && boundsA.min.y <= boundsB.max.y); }; + + /** + * Translates the bounds by the given vector + * @method translate + * @param {bounds} bounds + * @param {vector} vector + */ + Bounds.translate = function(bounds, vector) { + bounds.min.x += vector.x; + bounds.max.x += vector.x; + bounds.min.y += vector.y; + bounds.max.y += vector.y; + }; + + /** + * Shifts the bounds to the given position + * @method shift + * @param {bounds} bounds + * @param {vector} position + */ + Bounds.shift = function(bounds, position) { + var deltaX = bounds.max.x - bounds.min.x, + deltaY = bounds.max.y - bounds.min.y; + + bounds.min.x = position.x; + bounds.max.x = position.x + deltaX; + bounds.min.y = position.y; + bounds.max.y = position.y + deltaY; + }; })(); @@ -4361,6 +4740,17 @@ var Vector = {}; return { x: -vector.x, y: -vector.y }; }; + /** + * Returns the angle in radians between the two vectors relative to the x-axis + * @method angle + * @param {vector} vectorA + * @param {vector} vectorB + * @return {number} The angle in radians + */ + Vector.angle = function(vectorA, vectorB) { + return Math.atan2(vectorB.y - vectorA.y, vectorB.x - vectorA.x); + }; + })(); ; // End src/geometry/Vector.js @@ -4418,23 +4808,30 @@ var Vertices = {}; * @return {vector} The centre point */ Vertices.centre = function(vertices) { - var cx = 0, cy = 0; + var area = Vertices.area(vertices, true), + centre = { x: 0, y: 0 }, + cross, + temp, + j; for (var i = 0; i < vertices.length; i++) { - cx += vertices[i].x; - cy += vertices[i].y; + j = (i + 1) % vertices.length; + cross = Vector.cross(vertices[i], vertices[j]); + temp = Vector.mult(Vector.add(vertices[i], vertices[j]), cross); + centre = Vector.add(centre, temp); } - return { x: cx / vertices.length, y: cy / vertices.length }; + return Vector.div(centre, 6 * area); }; /** * Description * @method area * @param {vertices} vertices + * @param {bool} signed * @return {number} The area */ - Vertices.area = function(vertices) { + Vertices.area = function(vertices, signed) { var area = 0, j = vertices.length - 1; @@ -4443,6 +4840,9 @@ var Vertices = {}; j = i; } + if (signed) + return area / 2; + return Math.abs(area) / 2; }; @@ -4537,277 +4937,112 @@ var Vertices = {}; return true; }; + /** + * Scales the vertices from a point (default is centre) + * @method scale + * @param {vertices} vertices + * @param {number} scaleX + * @param {number} scaleY + * @param {vector} point + */ + Vertices.scale = function(vertices, scaleX, scaleY, point) { + if (scaleX === 1 && scaleY === 1) + return vertices; + + point = point || Vertices.centre(vertices); + + var vertex, + delta; + + for (var i = 0; i < vertices.length; i++) { + vertex = vertices[i]; + delta = Vector.sub(vertex, point); + vertices[i].x = point.x + delta.x * scaleX; + vertices[i].y = point.y + delta.y * scaleY; + } + + return vertices; + }; + + /** + * Chamfers a set of vertices by giving them rounded corners, returns a new set of vertices. + * The radius parameter is a single number or an array to specify the radius for each vertex. + * @method chamfer + * @param {vertices} vertices + * @param {number[]} radius + * @param {number} quality + * @param {number} qualityMin + * @param {number} qualityMax + */ + Vertices.chamfer = function(vertices, radius, quality, qualityMin, qualityMax) { + radius = radius || [8]; + + if (!radius.length) + radius = [radius]; + + // quality defaults to -1, which is auto + quality = (typeof quality !== 'undefined') ? quality : -1; + qualityMin = qualityMin || 2; + qualityMax = qualityMax || 14; + + var centre = Vertices.centre(vertices), + newVertices = []; + + for (var i = 0; i < vertices.length; i++) { + var prevVertex = vertices[i - 1 >= 0 ? i - 1 : vertices.length - 1], + vertex = vertices[i], + nextVertex = vertices[(i + 1) % vertices.length], + currentRadius = radius[i < radius.length ? i : radius.length - 1]; + + if (currentRadius === 0) { + newVertices.push(vertex); + continue; + } + + var prevNormal = Vector.normalise({ + x: vertex.y - prevVertex.y, + y: prevVertex.x - vertex.x + }); + + var nextNormal = Vector.normalise({ + x: nextVertex.y - vertex.y, + y: vertex.x - nextVertex.x + }); + + var diagonalRadius = Math.sqrt(2 * Math.pow(currentRadius, 2)), + radiusVector = Vector.mult(Common.clone(prevNormal), currentRadius), + midNormal = Vector.normalise(Vector.mult(Vector.add(prevNormal, nextNormal), 0.5)), + scaledVertex = Vector.sub(vertex, Vector.mult(midNormal, diagonalRadius)); + + var precision = quality; + + if (quality === -1) { + // automatically decide precision + precision = Math.pow(currentRadius, 0.32) * 1.75; + } + + precision = Common.clamp(precision, qualityMin, qualityMax); + + // use an even value for precision, more likely to reduce axes by using symmetry + if (precision % 2 === 1) + precision += 1; + + var alpha = Math.acos(Vector.dot(prevNormal, nextNormal)), + theta = alpha / precision; + + for (var j = 0; j < precision; j++) { + newVertices.push(Vector.add(Vector.rotate(radiusVector, theta * j), scaledVertex)); + } + } + + return newVertices; + }; + })(); ; // End src/geometry/Vertices.js -// Begin src/render/Gui.js - -/** -* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) -* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. -* -* @class Gui -*/ - -var Gui = {}; - -(function() { - - /** - * Description - * @method create - * @param {engine} engine - * @param {object} options - * @return {gui} A container for a configured dat.gui - */ - Gui.create = function(engine, options) { - var _datGuiSupported = window.dat && window.localStorage, - _serializer; - - if (!_datGuiSupported) { - console.log("Could not create GUI. Check dat.gui library is loaded first."); - return; - } - - var datGui = new dat.GUI(options); - - if (Resurrect) { - _serializer = new Resurrect({ prefix: '$' }); - _serializer.parse = _serializer.resurrect; - } else { - _serializer = JSON; - } - - var gui = { - datGui: datGui, - amount: 1, - size: 40, - sides: 4, - density: 0.001, - restitution: 0, - friction: 0.1, - frictionAir: 0.01, - renderer: 'canvas' - }; - - 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.add(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); - - // clear scene graph (if defined in controller) - var renderController = engine.render.controller; - if (renderController.clear) - renderController.clear(engine.render); - - /** - * Fired after the gui's clear button pressed - * - * @event clear - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(gui, 'clear'); - }, - - save: function() { - if (localStorage && _serializer) { - localStorage.setItem('world', _serializer.stringify(engine.world)); - } - - /** - * Fired after the gui's save button pressed - * - * @event save - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(gui, 'save'); - }, - - load: function() { - var loadedWorld; - - if (localStorage && _serializer) { - loadedWorld = _serializer.parse(localStorage.getItem('world')); - } - - if (loadedWorld) { - Engine.merge(engine, { world: loadedWorld }); - } - - /** - * Fired after the gui's load button pressed - * - * @event load - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(gui, 'load'); - } - }; - - var metrics = datGui.addFolder('Metrics'); - metrics.add(engine.timing, 'fps').listen(); - - if (engine.metrics.extended) { - 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.add(engine.metrics, 'narrowReuse').listen(); - metrics.close(); - } else { - metrics.open(); - } - - 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']) - .onFinishChange(function(value) { - Composite.setModified(engine.world, true, false, false); - }); - - 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(gui, 'renderer', ['canvas', 'webgl']) - .onFinishChange(function(value) { - var controller; - - if (value === 'canvas') - controller = Render; - - if (value === 'webgl') - controller = RenderPixi; - - // remove old canvas - engine.render.element.removeChild(engine.render.canvas); - - // create new renderer using the same options object - var options = engine.render.options; - - engine.render = controller.create({ - element: engine.render.element, - options: options - }); - - engine.render.options = options; - - // bind the mouse to the new canvas - Mouse.setElement(engine.input.mouse, engine.render.canvas); - }); - - render.add(engine.render.options, 'wireframes'); - render.add(engine.render.options, 'showDebug'); - render.add(engine.render.options, 'showPositions'); - render.add(engine.render.options, 'showBroadphase'); - render.add(engine.render.options, 'showBounds'); - render.add(engine.render.options, 'showVelocity'); - render.add(engine.render.options, 'showCollisions'); - render.add(engine.render.options, 'showAxes'); - render.add(engine.render.options, 'showAngleIndicator'); - render.add(engine.render.options, 'showSleeping'); - render.add(engine.render.options, 'showIds'); - render.add(engine.render.options, 'showShadows'); - render.add(engine.render.options, 'enabled'); - render.open(); - - //datGui.remember(world) - - return gui; - }; - - /** - * Description - * @method update - * @param {gui} gui - * @param {datGui} datGui - */ - Gui.update = function(gui, datGui) { - var i; - datGui = datGui || gui.datGui; - - for (i in datGui.__folders) { - Gui.update(gui, datGui.__folders[i]); - } - - for (i in datGui.__controllers) { - var controller = datGui.__controllers[i]; - if (controller.updateDisplay) - controller.updateDisplay(); - } - }; - - /** - * Description - * @method closeAll - * @param {gui} gui - */ - Gui.closeAll = function(gui) { - var datGui = gui.datGui; - - for (var i in datGui.__folders) { - datGui.__folders[i].close(); - } - }; - -})(); - -; // End src/render/Gui.js - - // Begin src/render/Render.js /** @@ -4840,6 +5075,7 @@ var Render = {}; height: 600, background: '#fafafa', wireframeBackground: '#222', + hasBounds: false, enabled: true, wireframes: true, showSleeping: true, @@ -4862,6 +5098,17 @@ var Render = {}; render.context = render.canvas.getContext('2d'); render.textures = {}; + render.bounds = render.bounds || { + min: { + x: 0, + y: 0 + }, + max: { + x: render.options.width, + y: render.options.height + } + }; + Render.setBackground(render, render.options.background); if (Common.isElement(render.element)) { @@ -4913,8 +5160,10 @@ var Render = {}; canvas = render.canvas, context = render.context, options = render.options, - bodies = Composite.allBodies(world), - constraints = Composite.allConstraints(world), + allBodies = Composite.allBodies(world), + allConstraints = Composite.allConstraints(world), + bodies = [], + constraints = [], i; if (options.wireframes) { @@ -4929,8 +5178,45 @@ var Render = {}; context.fillRect(0, 0, canvas.width, canvas.height); context.globalCompositeOperation = 'source-over'; - /*if (options.showShadows && !options.wireframes) - Render.bodyShadows(engine, bodies, context);*/ + // handle bounds + var boundsWidth = render.bounds.max.x - render.bounds.min.x, + boundsHeight = render.bounds.max.y - render.bounds.min.y, + boundsScaleX = boundsWidth / render.options.width, + boundsScaleY = boundsHeight / render.options.height; + + if (options.hasBounds) { + // filter out bodies that are not in view + for (i = 0; i < allBodies.length; i++) { + var body = allBodies[i]; + if (Bounds.overlaps(body.bounds, render.bounds)) + bodies.push(body); + } + + // filter out constraints that are not in view + for (i = 0; i < allConstraints.length; i++) { + var constraint = allConstraints[i], + bodyA = constraint.bodyA, + bodyB = constraint.bodyB, + pointAWorld = constraint.pointA, + pointBWorld = constraint.pointB; + + if (bodyA) pointAWorld = Vector.add(bodyA.position, constraint.pointA); + if (bodyB) pointBWorld = Vector.add(bodyB.position, constraint.pointB); + + if (!pointAWorld || !pointBWorld) + continue; + + if (Bounds.contains(render.bounds, pointAWorld) || Bounds.contains(render.bounds, pointBWorld)) + constraints.push(constraint); + } + + // transform the view + context.scale(1 / boundsScaleX, 1 / boundsScaleY); + context.translate(-render.bounds.min.x, -render.bounds.min.y); + } else { + constraints = allConstraints; + bodies = allBodies; + } if (!options.wireframes || (engine.enableSleeping && options.showSleeping)) { // fully featured rendering of bodies @@ -4965,6 +5251,11 @@ var Render = {}; if (options.showDebug) Render.debug(engine, context); + + if (options.hasBounds) { + // revert view transforms + context.setTransform(1, 0, 0, 1, 0, 0); + } }; /** @@ -5500,6 +5791,92 @@ var Render = {}; c.stroke(); }; + /** + * Description + * @method inspector + * @param {inspector} inspector + * @param {RenderingContext} context + */ + Render.inspector = function(inspector, context) { + var engine = inspector.engine, + mouse = engine.input.mouse, + selected = inspector.selected, + c = context, + render = engine.render, + options = render.options, + bounds; + + if (options.hasBounds) { + var boundsWidth = render.bounds.max.x - render.bounds.min.x, + boundsHeight = render.bounds.max.y - render.bounds.min.y, + boundsScaleX = boundsWidth / render.options.width, + boundsScaleY = boundsHeight / render.options.height; + + context.scale(1 / boundsScaleX, 1 / boundsScaleY); + context.translate(-render.bounds.min.x, -render.bounds.min.y); + } + + for (var i = 0; i < selected.length; i++) { + var item = selected[i].data; + + context.translate(0.5, 0.5); + context.lineWidth = 1; + context.strokeStyle = 'rgba(255,165,0,0.9)'; + context.setLineDash([1,2]); + + switch (item.type) { + + case 'body': + + // render body selections + bounds = item.bounds; + context.beginPath(); + context.rect(Math.floor(bounds.min.x - 3), Math.floor(bounds.min.y - 3), + Math.floor(bounds.max.x - bounds.min.x + 6), Math.floor(bounds.max.y - bounds.min.y + 6)); + context.closePath(); + context.stroke(); + + break; + + case 'constraint': + + // render constraint selections + var point = item.pointA; + if (item.bodyA) + point = item.pointB; + context.beginPath(); + context.arc(point.x, point.y, 10, 0, 2 * Math.PI); + context.closePath(); + context.stroke(); + + break; + + } + + context.setLineDash([0]); + context.translate(-0.5, -0.5); + } + + // render selection region + if (inspector.selectStart !== null) { + context.translate(0.5, 0.5); + context.lineWidth = 1; + context.strokeStyle = 'rgba(255,165,0,0.6)'; + context.fillStyle = 'rgba(255,165,0,0.1)'; + bounds = inspector.selectBounds; + context.beginPath(); + context.rect(Math.floor(bounds.min.x), Math.floor(bounds.min.y), + Math.floor(bounds.max.x - bounds.min.x), Math.floor(bounds.max.y - bounds.min.y)); + context.closePath(); + context.stroke(); + context.fill(); + context.translate(-0.5, -0.5); + } + + if (options.hasBounds) + context.setTransform(1, 0, 0, 1, 0, 0); + }; + /** * Description * @method _createCanvas @@ -5953,10 +6330,10 @@ Matter.Axes = Axes; Matter.Bounds = Bounds; Matter.Vector = Vector; Matter.Vertices = Vertices; -Matter.Gui = Gui; Matter.Render = Render; Matter.RenderPixi = RenderPixi; Matter.Events = Events; +Matter.Query = Query; // CommonJS module if (typeof exports !== 'undefined') { diff --git a/build/matter-0.8.0.min.js b/build/matter-0.8.0.min.js new file mode 100644 index 0000000..f485190 --- /dev/null +++ b/build/matter-0.8.0.min.js @@ -0,0 +1,8 @@ +/** +* matter-0.8.0.min.js 0.8.0-alpha 2014-05-04 +* http://brm.io/matter-js/ +* License: MIT +*/ + +!function(){var a={},b={};!function(){var a=1;b.create=function(a){var b={id:o.nextId(),type:"body",label:"Body",angle:0,vertices:z.fromPath("L 0 0 L 40 0 L 40 40 L 0 40"),position:{x:0,y:0},force:{x:0,y:0},torque:0,positionImpulse:{x:0,y:0},constraintImpulse:{x:0,y:0,angle:0},speed:0,angularSpeed:0,velocity:{x:0,y:0},angularVelocity:0,isStatic:!1,isSleeping:!1,motion:0,sleepThreshold:60,density:.001,restitution:0,friction:.1,frictionAir:.01,groupId:0,slop:.05,timeScale:1,render:{visible:!0,sprite:{xScale:1,yScale:1},lineWidth:1.5}},d=o.extend(b,a);return c(d),d},b.nextGroupId=function(){return a++};var c=function(a){a.axes=a.axes||w.fromVertices(a.vertices),a.area=z.area(a.vertices),a.bounds=x.create(a.vertices),a.mass=a.mass||a.density*a.area,a.inverseMass=1/a.mass,a.inertia=a.inertia||z.inertia(a.vertices,a.mass),a.inverseInertia=1/a.inertia,a.positionPrev=a.positionPrev||{x:a.position.x,y:a.position.y},a.anglePrev=a.anglePrev||a.angle,a.render.fillStyle=a.render.fillStyle||(a.isStatic?"#eeeeee":o.choose(["#556270","#4ECDC4","#C7F464","#FF6B6B","#C44D58"])),a.render.strokeStyle=a.render.strokeStyle||o.shadeColor(a.render.fillStyle,-20),z.create(a.vertices,a);var c=z.centre(a.vertices);z.translate(a.vertices,a.position),z.translate(a.vertices,c,-1),z.rotate(a.vertices,a.angle,a.position),w.rotate(a.axes,a.angle),x.update(a.bounds,a.vertices,a.velocity),b.setStatic(a,a.isStatic),t.set(a,a.isSleeping)};b.setStatic=function(a,b){a.isStatic=b,b&&(a.restitution=0,a.friction=1,a.mass=a.inertia=a.density=1/0,a.inverseMass=a.inverseInertia=0,a.render.lineWidth=1,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.anglePrev=a.angle,a.angularVelocity=0,a.speed=0,a.angularSpeed=0,a.motion=0)},b.resetForcesAll=function(a){for(var b=0;bf.max.x||h.bounds.max.yf.max.y||b.update(h,c,d,e)}},b.update=function(a,b,c,d){var e=Math.pow(b*c*a.timeScale,2),f=1-a.frictionAir*c*a.timeScale,g=a.position.x-a.positionPrev.x,h=a.position.y-a.positionPrev.y;a.velocity.x=g*f*d+a.force.x/a.mass*e,a.velocity.y=h*f*d+a.force.y/a.mass*e,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.position.x+=a.velocity.x,a.position.y+=a.velocity.y,a.angularVelocity=(a.angle-a.anglePrev)*f*d+a.torque/a.inertia*e,a.anglePrev=a.angle,a.angle+=a.angularVelocity,a.speed=y.magnitude(a.velocity),a.angularSpeed=Math.abs(a.angularVelocity),z.translate(a.vertices,a.velocity),0!==a.angularVelocity&&(z.rotate(a.vertices,a.angularVelocity,a.position),w.rotate(a.axes,a.angularVelocity)),x.update(a.bounds,a.vertices,a.velocity)},b.applyForce=function(a,b,c){a.force.x+=c.x,a.force.y+=c.y;var d={x:b.x-a.position.x,y:b.y-a.position.y};a.torque+=(d.x*c.y-d.y*c.x)*a.inverseInertia},b.translate=function(a,b){a.positionPrev.x+=b.x,a.positionPrev.y+=b.y,a.position.x+=b.x,a.position.y+=b.y,z.translate(a.vertices,b),x.update(a.bounds,a.vertices,a.velocity)},b.rotate=function(a,b){a.anglePrev+=b,a.angle+=b,z.rotate(a.vertices,b,a.position),w.rotate(a.axes,b),x.update(a.bounds,a.vertices,a.velocity)},b.scale=function(a,b,c,d){z.scale(a.vertices,b,c,d),a.axes=w.fromVertices(a.vertices),a.area=z.area(a.vertices),a.mass=a.density*a.area,a.inverseMass=1/a.mass,z.translate(a.vertices,{x:-a.position.x,y:-a.position.y}),a.inertia=z.inertia(a.vertices,a.mass),a.inverseInertia=1/a.inertia,z.translate(a.vertices,{x:a.position.x,y:a.position.y}),x.update(a.bounds,a.vertices,a.velocity)}}();var c={};!function(){c.create=function(a){return o.extend({id:o.nextId(),type:"composite",parent:null,isModified:!1,bodies:[],constraints:[],composites:[],label:"Composite"},a)},c.setModified=function(a,b,d,e){if(a.isModified=b,d&&a.parent&&c.setModified(a.parent,b,d,e),e)for(var f=0;fq.bounds.width||u.bounds.max.y<0||u.bounds.min.y>q.bounds.height)){var v=b(c,u);if(!u.region||v.id!==u.region.id||k){s.broadphaseTests+=1,(!u.region||k)&&(u.region=v);var w=a(v,u.region);for(m=w.startCol;m<=w.endCol;m++)for(n=w.startRow;n<=w.endRow;n++){p=d(m,n),o=r[p];var x=m>=v.startCol&&m<=v.endCol&&n>=v.startRow&&n<=v.endRow,y=m>=u.region.startCol&&m<=u.region.endCol&&n>=u.region.startRow&&n<=u.region.endRow;!x&&y&&y&&o&&i(c,o,u),(u.region===v||x&&!y||k)&&(o||(o=e(r,p)),f(c,o,u))}u.region=v,t=!0}}}t&&(c.pairsList=j(c))},g.clear=function(a){a.buckets={},a.pairs={},a.pairsList=[]};var a=function(a,b){var d=Math.min(a.startCol,b.startCol),e=Math.max(a.endCol,b.endCol),f=Math.min(a.startRow,b.startRow),g=Math.max(a.endRow,b.endRow);return c(d,e,f,g)},b=function(a,b){var d=b.bounds,e=Math.floor(d.min.x/a.bucketWidth),f=Math.floor(d.max.x/a.bucketWidth),g=Math.floor(d.min.y/a.bucketHeight),h=Math.floor(d.max.y/a.bucketHeight);return c(e,f,g,h)},c=function(a,b,c,d){return{id:a+","+b+","+c+","+d,startCol:a,endCol:b,startRow:c,endRow:d}},d=function(a,b){return a+","+b},e=function(a,b){var c=a[b]=[];return c},f=function(a,b,c){for(var d=0;d0?d.push(c):delete a.pairs[b[e]];return d}}();var h={};!function(){h.create=function(a,b){var c=a.bodyA,d=a.bodyB,e={id:h.id(c,d),bodyA:c,bodyB:d,contacts:{},activeContacts:[],separation:0,isActive:!0,timeCreated:b,timeUpdated:b,inverseMass:c.inverseMass+d.inverseMass,friction:Math.min(c.friction,d.friction),restitution:Math.max(c.restitution,d.restitution),slop:Math.max(c.slop,d.slop)};return h.update(e,a,b),e},h.update=function(a,b,c){var d=a.contacts,f=b.supports,g=a.activeContacts;if(a.collision=b,g.length=0,b.collided){for(var i=0;ia&&j.push(g);for(g=0;gD*g.friction*e&&(E=D*g.friction*e*B);var F=y.cross(s,k),G=y.cross(t,k),H=n/(g.inverseMass+i.inverseInertia*F*F+j.inverseInertia*G*G);if(C*=H,E*=H,0>x&&x*x>a*e)q.normalImpulse=0,q.tangentImpulse=0;else{var I=q.normalImpulse;q.normalImpulse=Math.min(q.normalImpulse+C,0),C=q.normalImpulse-I;var J=q.tangentImpulse;q.tangentImpulse=o.clamp(q.tangentImpulse+E,-A,A),E=q.tangentImpulse-J}d.x=k.x*C+l.x*E,d.y=k.y*C+l.y*E,i.isStatic||i.isSleeping||(i.positionPrev.x+=d.x*i.inverseMass,i.positionPrev.y+=d.y*i.inverseMass,i.anglePrev+=y.cross(s,d)*i.inverseInertia),j.isStatic||j.isSleeping||(j.positionPrev.x-=d.x*j.inverseMass,j.positionPrev.y-=d.y*j.inverseMass,j.anglePrev-=y.cross(t,d)*j.inverseInertia)}}}}}();var l={};!function(){l.collides=function(b,d,e){var f,g,h,i,j=e,k=!1;if(j){var l=b.speed*b.speed+b.angularSpeed*b.angularSpeed+d.speed*d.speed+d.angularSpeed*d.angularSpeed;k=j&&j.collided&&.2>l,i=j}else i={collided:!1,bodyA:b,bodyB:d};if(j&&k){var m=[j.bodyA.axes[j.axisNumber]];if(h=a(j.bodyA.vertices,j.bodyB.vertices,m),i.reused=!0,h.overlap<=0)return i.collided=!1,i}else{if(f=a(b.vertices,d.vertices,b.axes),f.overlap<=0)return i.collided=!1,i;if(g=a(d.vertices,b.vertices,d.axes),g.overlap<=0)return i.collided=!1,i;f.overlap0&&(i.normal=y.neg(i.normal)),i.tangent=y.perp(i.normal),i.penetration={x:i.normal.x*i.depth,y:i.normal.y*i.depth};var n=c(b,d,i.normal),o=[n[0]];if(z.contains(b.vertices,n[1]))o.push(n[1]);else{var p=c(d,b,y.neg(i.normal));z.contains(d.vertices,p[0])&&o.push(p[0]),o.length<2&&z.contains(d.vertices,p[1])&&o.push(p[1])}return i.supports=o,i.supportCorrected=y.sub(n[0],i.penetration),i};var a=function(a,c,d){for(var e,f,g={},h={},i={overlap:Number.MAX_VALUE},j=0;j=e)return i.overlap=e,i;ee?e=g:d>g&&(d=g)}a.min=d,a.max=e},c=function(a,b,c){for(var d,e,f=Number.MAX_VALUE,g={x:0,y:0},h=b.vertices,i=a.position,j=h[0],k=h[1],l=0;ld&&(f=d,j=e);var m=j.index-1>=0?j.index-1:h.length-1;e=h[m],g.x=e.x-i.x,g.y=e.y-i.y,f=-y.dot(c,g),k=e;var n=(j.index+1)%h.length;return e=h[n],g.x=e.x-i.x,g.y=e.y-i.y,d=-y.dot(c,g),f>d&&(f=d,k=e),[j,k]}}();var m={};!function(){var a=1e-6,b=.001;m.create=function(b){var c=b;c.bodyA&&!c.pointA&&(c.pointA={x:0,y:0}),c.bodyB&&!c.pointB&&(c.pointB={x:0,y:0});var d=c.bodyA?y.add(c.bodyA.position,c.pointA):c.pointA,e=c.bodyB?y.add(c.bodyB.position,c.pointB):c.pointB,f=y.magnitude(y.sub(d,e));c.length=c.length||f||a;var g={visible:!0,lineWidth:2,strokeStyle:"#666"};return c.render=o.extend(g,c.render),c.id=c.id||o.nextId(),c.label=c.label||"Constraint",c.type="constraint",c.stiffness=c.stiffness||1,c.angularStiffness=c.angularStiffness||0,c.angleA=c.bodyA?c.bodyA.angle:c.angleA,c.angleB=c.bodyB?c.bodyB.angle:c.angleB,c},m.solveAll=function(a,b){for(var c=0;c0&&(B=0);var C,D={x:n.x*B,y:n.y*B};e&&!e.isStatic&&(C=y.cross(s,D)*e.inverseInertia*(1-c.angularStiffness),t.set(e,!1),C=o.clamp(C,-.01,.01),e.constraintImpulse.x-=p.x,e.constraintImpulse.y-=p.y,e.constraintImpulse.angle+=C,e.position.x-=p.x,e.position.y-=p.y,e.angle+=C),f&&!f.isStatic&&(C=y.cross(u,D)*f.inverseInertia*(1-c.angularStiffness),t.set(f,!1),C=o.clamp(C,-.01,.01),f.constraintImpulse.x+=p.x,f.constraintImpulse.y+=p.y,f.constraintImpulse.angle-=C,f.position.x+=p.x,f.position.y+=p.y,f.angle-=C)}}},m.postSolveAll=function(a){for(var b=0;b>16)+d,f=(c>>8&255)+d,g=(255&c)+d;return"#"+(16777216+65536*(255>e?1>e?0:e:255)+256*(255>f?1>f?0:f:255)+(255>g?1>g?0:g:255)).toString(16).slice(1)},o.shuffle=function(a){for(var b=a.length-1;b>0;b--){var c=Math.floor(Math.random()*(b+1)),d=a[b];a[b]=a[c],a[c]=d}return a},o.choose=function(a){return a[Math.floor(Math.random()*a.length)]},o.isElement=function(a){try{return a instanceof HTMLElement}catch(b){return"object"==typeof a&&1===a.nodeType&&"object"==typeof a.style&&"object"==typeof a.ownerDocument}},o.clamp=function(a,b,c){return b>a?b:a>c?c:a},o.sign=function(a){return 0>a?-1:1},o.now=function(){var a=window.performance;return a?(a.now=a.now||a.webkitNow||a.msNow||a.oNow||a.mozNow,+a.now()):+new Date},o.random=function(a,b){return a+Math.random()*(b-a)},o.colorToNumber=function(a){return a=a.replace("#",""),3==a.length&&(a=a.charAt(0)+a.charAt(0)+a.charAt(1)+a.charAt(1)+a.charAt(2)+a.charAt(2)),parseInt(a,16)},o.log=function(a,b){if(console&&console.log){var c;switch(b){case"warn":c="color: coral";break;case"error":c="color: red"}console.log("%c [Matter] "+b+": "+a,c)}},o.nextId=function(){return o._nextId++}}();var p={};!function(){var a=60,e=a,h=1e3/a,j=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(function(){a(o.now())},h)};p.create=function(b,c){c=o.isElement(b)?c:b,b=o.isElement(b)?b:null;var e={enabled:!0,positionIterations:6,velocityIterations:4,constraintIterations:2,enableSleeping:!1,timeScale:1,input:{},events:[],timing:{fps:a,timestamp:0,delta:h,correction:1,deltaMin:1e3/a,deltaMax:1e3/(.5*a),timeScale:1,isFixed:!1},render:{element:b,controller:A}},j=o.extend(e,c);return j.render=j.render.controller.create(j.render),j.world=d.create(j.world),j.pairs=i.create(),j.metrics=j.metrics||r.create(),j.input.mouse=j.input.mouse||s.create(j.render.canvas),j.broadphase=j.broadphase||{current:"grid",grid:{controller:g,instance:g.create(),detector:f.collisions},bruteForce:{detector:f.bruteForce}},j},p.run=function(a){var b,c=0,d=0,f=[],g=1;!function h(i){if(j(h),a.enabled){var k,m,o=a.timing,r={timestamp:i};q.trigger(a,"beforeTick",r),o.isFixed?k=o.delta:(k=i-b||o.delta,b=i,f.push(k),f=f.slice(-e),k=Math.min.apply(null,f),k=ko.deltaMax?o.deltaMax:k,m=k/o.delta,o.delta=k),0!==g&&(m*=o.timeScale/g),0===o.timeScale&&(m=0),g=o.timeScale,d+=1,i-c>=1e3&&(o.fps=d*((i-c)/1e3),c=i,d=0),q.trigger(a,"tick",r),a.world.isModified&&a.render.controller.clear(a.render),p.update(a,k,m),n(a),l(a),p.render(a),q.trigger(a,"afterTick",r)}}()},p.update=function(a,d,e){e="undefined"!=typeof e?e:1;var f,g=a.world,h=a.timing,j=a.broadphase[a.broadphase.current],l=[];h.timestamp+=d*h.timeScale,h.correction=e;var n={timestamp:a.timing.timestamp};q.trigger(a,"beforeUpdate",n);var o=c.allBodies(g),p=c.allConstraints(g);for(r.reset(a.metrics),a.enableSleeping&&t.update(o),b.applyGravityAll(o,g.gravity),b.updateAll(o,d,h.timeScale,e,g.bounds),f=0;f0&&q.trigger(a,"collisionStart",{pairs:b.collisionStart}),b.collisionActive.length>0&&q.trigger(a,"collisionActive",{pairs:b.collisionActive}),b.collisionEnd.length>0&&q.trigger(a,"collisionEnd",{pairs:b.collisionEnd})}}();var q={};!function(){q.on=function(a,b,c){for(var d,e=b.split(" "),f=0;f0||e.force.y>0)t.set(e,!1);else{var g=Math.min(e.motion,f),h=Math.max(e.motion,f);e.motion=c*g+(1-c)*h,e.sleepThreshold>0&&e.motion=e.sleepThreshold&&t.set(e,!0)):e.sleepCounter>0&&(e.sleepCounter-=1)}}},t.afterCollisions=function(b){for(var c=0;ca&&t.set(h,!1)}}}},t.set=function(a,b){b?(a.isSleeping=!0,a.sleepCounter=a.sleepThreshold,a.positionImpulse.x=0,a.positionImpulse.y=0,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.anglePrev=a.angle,a.speed=0,a.angularSpeed=0,a.motion=0):(a.isSleeping=!1,a.sleepCounter=0)}}();var u={};!function(){u.rectangle=function(a,c,d,e,f){f=f||{};var g={label:"Rectangle Body",position:{x:a,y:c},vertices:z.fromPath("L 0 0 L "+d+" 0 L "+d+" "+e+" L 0 "+e)};if(f.chamfer){var h=f.chamfer;g.vertices=z.chamfer(g.vertices,h.radius,h.quality,h.qualityMin,h.qualityMax),delete f.chamfer}return b.create(o.extend({},g,f))},u.trapezoid=function(a,c,d,e,f,g){g=g||{},f*=.5;var h=(1-2*f)*d,i=d*f,j=i+h,k=j+i,l={label:"Trapezoid Body",position:{x:a,y:c},vertices:z.fromPath("L 0 0 L "+i+" "+-e+" L "+j+" "+-e+" L "+k+" 0")}; +if(g.chamfer){var m=g.chamfer;l.vertices=z.chamfer(l.vertices,m.radius,m.quality,m.qualityMin,m.qualityMax),delete g.chamfer}return b.create(o.extend({},l,g))},u.circle=function(a,b,c,d,e){d=d||{},d.label="Circle Body",e=e||25;var f=Math.ceil(Math.max(10,Math.min(e,c)));return f%2===1&&(f+=1),d.circleRadius=c,u.polygon(a,b,f,c,d)},u.polygon=function(a,c,d,e,f){if(f=f||{},3>d)return u.circle(a,c,e,f);for(var g=2*Math.PI/d,h="",i=.5*g,j=0;d>j;j+=1){var k=i+j*g,l=Math.cos(k)*e,m=Math.sin(k)*e;h+="L "+l.toFixed(3)+" "+m.toFixed(3)+" "}var n={label:"Polygon Body",position:{x:a,y:c},vertices:z.fromPath(h)};if(f.chamfer){var p=f.chamfer;n.vertices=z.chamfer(n.vertices,p.radius,p.quality,p.qualityMin,p.qualityMax),delete f.chamfer}return b.create(o.extend({},n,f))}}();var v={};!function(){v.stack=function(a,d,e,f,g,h,i){for(var j,k=c.create({label:"Stack"}),l=a,m=d,n=0,o=0;f>o;o++){for(var p=0,q=0;e>q;q++){var r=i(l,m,q,o,j,n);if(r){var s=r.bounds.max.y-r.bounds.min.y,t=r.bounds.max.x-r.bounds.min.x;s>p&&(p=s),b.translate(r,{x:.5*t,y:.5*s}),l=r.bounds.max.x+g,c.addBody(k,r),j=r,n+=1}}m+=p+h,l=a}return k},v.chain=function(a,b,d,e,f,g){for(var h=a.bodies,i=1;ig;g++){for(h=0;b>h;h++)h>0&&(i=l[h-1+g*b],j=l[h+g*b],c.addConstraint(a,m.create(o.extend({bodyA:i,bodyB:j},f))));for(h=0;b>h;h++)g>0&&(i=l[h+(g-1)*b],j=l[h+g*b],c.addConstraint(a,m.create(o.extend({bodyA:i,bodyB:j},f))),e&&h>0&&(k=l[h-1+(g-1)*b],c.addConstraint(a,m.create(o.extend({bodyA:k,bodyB:j},f)))),e&&b-1>h&&(k=l[h+1+(g-1)*b],c.addConstraint(a,m.create(o.extend({bodyA:k,bodyB:j},f)))))}return a.label+=" Mesh",a},v.pyramid=function(a,c,d,e,f,g,h){return v.stack(a,c,d,e,f,g,function(c,g,i,j,k,l){var m=Math.min(e,Math.ceil(d/2)),n=k?k.bounds.max.x-k.bounds.min.x:0;if(!(j>m)){j=m-j;var o=j,p=d-1-j;if(!(o>i||i>p)){1===l&&b.translate(k,{x:(i+(d%2===1?1:-1))*n,y:0});var q=k?i*n:0;return h(a+q+i*f,g,i,j,k,l)}}})},v.newtonsCradle=function(a,b,d,e,f){for(var g=c.create({label:"Newtons Cradle"}),h=0;d>h;h++){var i=1.9,j=u.circle(a+h*e*i,b+f,e,{inertia:99999,restitution:1,friction:0,frictionAir:1e-4,slop:.01}),k=m.create({pointA:{x:a+h*e*i,y:b},bodyB:j});c.addBody(g,j),c.addConstraint(g,k)}return g},v.car=function(a,d,e,f,g){var h=b.nextGroupId(),i=-20,j=.5*-e+i,k=.5*e-i,l=0,n=c.create({label:"Car"}),o=u.trapezoid(a,d,e,f,.3,{groupId:h,friction:.01,chamfer:{radius:10}}),p=u.circle(a+j,d+l,g,{groupId:h,restitution:.5,friction:.9,density:.01}),q=u.circle(a+k,d+l,g,{groupId:h,restitution:.5,friction:.9,density:.01}),r=m.create({bodyA:o,pointA:{x:j,y:l},bodyB:p,stiffness:.5}),s=m.create({bodyA:o,pointA:{x:k,y:l},bodyB:q,stiffness:.5});return c.addBody(n,o),c.addBody(n,p),c.addBody(n,q),c.addConstraint(n,r),c.addConstraint(n,s),n},v.softBody=function(a,b,c,d,e,f,g,h,i,j){i=o.extend({inertia:1/0},i),j=o.extend({stiffness:.4},j);var k=v.stack(a,b,c,d,e,f,function(a,b){return u.circle(a,b,h,i)});return v.mesh(k,c,d,g,j),k.label="Soft Body",k}}();var w={};!function(){w.fromVertices=function(a){for(var b={},c=0;ca.max.x&&(a.max.x=e.x),e.xa.max.y&&(a.max.y=e.y),e.y0?a.max.x+=c.x:a.min.x+=c.x,c.y>0?a.max.y+=c.y:a.min.y+=c.y)},x.contains=function(a,b){return b.x>=a.min.x&&b.x<=a.max.x&&b.y>=a.min.y&&b.y<=a.max.y},x.overlaps=function(a,b){return a.min.x<=b.max.x&&a.max.x>=b.min.x&&a.max.y>=b.min.y&&a.min.y<=b.max.y},x.translate=function(a,b){a.min.x+=b.x,a.max.x+=b.x,a.min.y+=b.y,a.max.y+=b.y},x.shift=function(a,b){var c=a.max.x-a.min.x,d=a.max.y-a.min.y;a.min.x=b.x,a.max.x=b.x+c,a.min.y=b.y,a.max.y=b.y+d}}();var y={};!function(){y.magnitude=function(a){return Math.sqrt(a.x*a.x+a.y*a.y)},y.magnitudeSquared=function(a){return a.x*a.x+a.y*a.y},y.rotate=function(a,b){var c=Math.cos(b),d=Math.sin(b);return{x:a.x*c-a.y*d,y:a.x*d+a.y*c}},y.rotateAbout=function(a,b,c){var d=Math.cos(b),e=Math.sin(b);return{x:c.x+((a.x-c.x)*d-(a.y-c.y)*e),y:c.y+((a.x-c.x)*e+(a.y-c.y)*d)}},y.normalise=function(a){var b=y.magnitude(a);return 0===b?{x:0,y:0}:{x:a.x/b,y:a.y/b}},y.dot=function(a,b){return a.x*b.x+a.y*b.y},y.cross=function(a,b){return a.x*b.y-a.y*b.x},y.add=function(a,b){return{x:a.x+b.x,y:a.y+b.y}},y.sub=function(a,b){return{x:a.x-b.x,y:a.y-b.y}},y.mult=function(a,b){return{x:a.x*b,y:a.y*b}},y.div=function(a,b){return{x:a.x/b,y:a.y/b}},y.perp=function(a,b){return b=b===!0?-1:1,{x:b*-a.y,y:b*a.x}},y.neg=function(a){return{x:-a.x,y:-a.y}},y.angle=function(a,b){return Math.atan2(b.y-a.y,b.x-a.x)}}();var z={};!function(){z.create=function(a,b){for(var c=0;c0)return!1}return!0},z.scale=function(a,b,c,d){if(1===b&&1===c)return a;d=d||z.centre(a);for(var e,f,g=0;g=0?g-1:a.length-1],i=a[g],j=a[(g+1)%a.length],k=b[gv;v++)f.push(y.add(y.rotate(p,u*v),r))}else f.push(i)}return f}}();var A={};!function(){A.create=function(b){var c={controller:A,element:null,canvas:null,options:{width:800,height:600,background:"#fafafa",wireframeBackground:"#222",hasBounds:!1,enabled:!0,wireframes:!0,showSleeping:!0,showDebug:!1,showBroadphase:!1,showBounds:!1,showVelocity:!1,showCollisions:!1,showAxes:!1,showPositions:!1,showAngleIndicator:!1,showIds:!1,showShadows:!1}},d=o.extend(c,b);return d.canvas=d.canvas||a(d.options.width,d.options.height),d.context=d.canvas.getContext("2d"),d.textures={},d.bounds=d.bounds||{min:{x:0,y:0},max:{x:d.options.width,y:d.options.height}},A.setBackground(d,d.options.background),o.isElement(d.element)?d.element.appendChild(d.canvas):o.log('No "render.element" passed, "render.canvas" was not inserted into document.',"warn"),d},A.clear=function(){},A.setBackground=function(a,b){if(a.currentBackground!==b){var c=b;/(jpg|gif|png)$/.test(b)&&(c="url("+b+")"),a.canvas.style.background=c,a.canvas.style.backgroundSize="contain",a.currentBackground=b}},A.world=function(a){var b,d=a.render,e=a.world,f=d.canvas,g=d.context,h=d.options,i=c.allBodies(e),j=c.allConstraints(e),k=[],l=[];h.wireframes?A.setBackground(d,h.wireframeBackground):A.setBackground(d,h.background),g.globalCompositeOperation="source-in",g.fillStyle="transparent",g.fillRect(0,0,f.width,f.height),g.globalCompositeOperation="source-over";var m=d.bounds.max.x-d.bounds.min.x,n=d.bounds.max.y-d.bounds.min.y,o=m/d.options.width,p=n/d.options.height;if(h.hasBounds){for(b=0;b=500){var k="";k+="fps: "+Math.round(a.timing.fps)+j,a.metrics.extended&&(k+="delta: "+a.timing.delta.toFixed(3)+j,k+="correction: "+a.timing.correction.toFixed(3)+j,k+="bodies: "+i.length+j,a.broadphase.controller===g&&(k+="buckets: "+a.metrics.buckets+j),k+="\n",k+="collisions: "+a.metrics.collisions+j,k+="pairs: "+a.pairs.list.length+j,k+="broad: "+a.metrics.broadEff+j,k+="mid: "+a.metrics.midEff+j,k+="narrow: "+a.metrics.narrowEff+j),f.debugString=k,f.debugTimestamp=a.timing.timestamp}if(f.debugString){d.font="12px Arial",d.fillStyle=h.wireframes?"rgba(255,255,255,0.5)":"rgba(0,0,0,0.5)";for(var l=f.debugString.split("\n"),m=0;m0){var l=d.activeContacts[0].vertex.x,m=d.activeContacts[0].vertex.y;2===d.activeContacts.length&&(l=(d.activeContacts[0].vertex.x+d.activeContacts[1].vertex.x)/2,m=(d.activeContacts[0].vertex.y+d.activeContacts[1].vertex.y)/2),h.moveTo(l-8*e.normal.x,m-8*e.normal.y),h.lineTo(l,m)}h.strokeStyle=i.wireframes?"rgba(255,165,0,0.7)":"orange",h.lineWidth=1,h.stroke()},A.grid=function(a,b,c){var d=c,e=a.render.options;d.strokeStyle=e.wireframes?"rgba(255,180,0,0.1)":"rgba(255,180,0,0.5)",d.beginPath();for(var f=o.keys(b.buckets),g=0;g worldBounds.max.y) continue; - Body.update(body, deltaTime, correction); + Body.update(body, deltaTime, timeScale, correction); } }; @@ -236,13 +236,14 @@ var Body = {}; * @method update * @param {body} body * @param {number} deltaTime + * @param {number} timeScale * @param {number} correction */ - Body.update = function(body, deltaTime, correction) { - var deltaTimeSquared = deltaTime * deltaTime * body.timeScale; + Body.update = function(body, deltaTime, timeScale, correction) { + var deltaTimeSquared = Math.pow(deltaTime * timeScale * body.timeScale, 2); // from the previous step - var frictionAir = 1 - body.frictionAir, + var frictionAir = 1 - body.frictionAir * timeScale * body.timeScale, velocityPrevX = body.position.x - body.positionPrev.x, velocityPrevY = body.position.y - body.positionPrev.y; @@ -1843,8 +1844,9 @@ var Resolver = {}; * @method solveVelocity * @param {pair[]} pairs */ - Resolver.solveVelocity = function(pairs) { - var impulse = {}; + Resolver.solveVelocity = function(pairs, timeScale) { + var impulse = {}, + timeScaleSquared = timeScale * timeScale; for (var i = 0; i < pairs.length; i++) { var pair = pairs[i]; @@ -1889,8 +1891,8 @@ var Resolver = {}; // coulomb friction var tangentImpulse = tangentVelocity; - if (tangentSpeed > normalForce * pair.friction) - tangentImpulse = normalForce * pair.friction * tangentVelocityDirection; + if (tangentSpeed > normalForce * pair.friction * timeScaleSquared) + tangentImpulse = normalForce * pair.friction * timeScaleSquared * tangentVelocityDirection; // modify impulses accounting for mass, inertia and offset var oAcN = Vector.cross(offsetA, normal), @@ -1900,7 +1902,7 @@ var Resolver = {}; tangentImpulse *= share; // handle high velocity and resting collisions separately - if (normalVelocity < 0 && normalVelocity * normalVelocity > _restingThresh) { + if (normalVelocity < 0 && normalVelocity * normalVelocity > _restingThresh * timeScaleSquared) { // high velocity so clear cached contact impulse contact.normalImpulse = 0; contact.tangentImpulse = 0; @@ -2895,7 +2897,8 @@ var Engine = {}; correction: 1, deltaMin: 1000 / _fps, deltaMax: 1000 / (_fps * 0.5), - timeScale: 1 + timeScale: 1, + isFixed: false }, render: { element: element, @@ -2927,48 +2930,60 @@ var Engine = {}; }; /** - * Description + * An optional utility function that provides a game loop, that handles updating the engine for you. + * Calls `Engine.update` and `Engine.render` on the `requestAnimationFrame` event automatically. + * Handles time correction and non-fixed dynamic timing (if enabled). + * Triggers `beforeTick`, `tick` and `afterTick` events. * @method run * @param {engine} engine */ Engine.run = function(engine) { - var timing = engine.timing, - delta, - correction, - counterTimestamp = 0, + var counterTimestamp = 0, frameCounter = 0, deltaHistory = [], + timePrev, timeScalePrev = 1; - (function render(timestamp){ + (function render(time){ _requestAnimationFrame(render); if (!engine.enabled) return; - // timestamp is undefined on the first update - timestamp = timestamp || 0; + var timing = engine.timing, + delta, + correction; // create an event object var event = { - timestamp: timestamp + timestamp: time }; Events.trigger(engine, 'beforeTick', event); - delta = (timestamp - timing.timestamp) || _delta; + if (timing.isFixed) { + // fixed timestep + delta = timing.delta; + } else { + // dynamic timestep based on wall clock between calls + delta = (time - timePrev) || timing.delta; + timePrev = time; - // optimistically filter delta over a few frames, to improve stability - deltaHistory.push(delta); - deltaHistory = deltaHistory.slice(-_deltaSampleSize); - delta = Math.min.apply(null, deltaHistory); - - // limit delta - delta = delta < timing.deltaMin ? timing.deltaMin : delta; - delta = delta > timing.deltaMax ? timing.deltaMax : delta; + // optimistically filter delta over a few frames, to improve stability + deltaHistory.push(delta); + deltaHistory = deltaHistory.slice(-_deltaSampleSize); + delta = Math.min.apply(null, deltaHistory); + + // limit delta + delta = delta < timing.deltaMin ? timing.deltaMin : delta; + delta = delta > timing.deltaMax ? timing.deltaMax : delta; - // time correction for delta - correction = delta / timing.delta; + // time correction for delta + correction = delta / timing.delta; + + // update engine timing object + timing.delta = delta; + } // time correction for time scaling if (timeScalePrev !== 0) @@ -2978,21 +2993,16 @@ var Engine = {}; correction = 0; timeScalePrev = timing.timeScale; - - // update engine timing object - timing.timestamp = timestamp; - timing.correction = correction; - timing.delta = delta; // fps counter frameCounter += 1; - if (timestamp - counterTimestamp >= 1000) { - timing.fps = frameCounter * ((timestamp - counterTimestamp) / 1000); - counterTimestamp = timestamp; + if (time - counterTimestamp >= 1000) { + timing.fps = frameCounter * ((time - counterTimestamp) / 1000); + counterTimestamp = time; frameCounter = 0; } - Events.trigger(engine, 'tick beforeUpdate', event); + Events.trigger(engine, 'tick', event); // if world has been modified, clear the render scene graph if (engine.world.isModified) @@ -3005,18 +3015,15 @@ var Engine = {}; _triggerCollisionEvents(engine); _triggerMouseEvents(engine); - Events.trigger(engine, 'afterUpdate beforeRender', event); - // render - if (engine.render.options.enabled) - engine.render.controller.world(engine); + Engine.render(engine); - Events.trigger(engine, 'afterTick afterRender', event); + Events.trigger(engine, 'afterTick', event); })(); }; /** - * Description + * Moves the simulation forward in time by `delta` ms. Triggers `beforeUpdate` and `afterUpdate` events. * @method update * @param {engine} engine * @param {number} delta @@ -3024,12 +3031,25 @@ var Engine = {}; * @return engine */ Engine.update = function(engine, delta, correction) { + correction = (typeof correction !== 'undefined') ? correction : 1; + var world = engine.world, timing = engine.timing, broadphase = engine.broadphase[engine.broadphase.current], broadphasePairs = [], i; + // increment timestamp + timing.timestamp += delta * timing.timeScale; + timing.correction = correction; + + // create an event object + var event = { + timestamp: engine.timing.timestamp + }; + + Events.trigger(engine, 'beforeUpdate', event); + // get lists of all bodies and constraints, no matter what composites they are in var allBodies = Composite.allBodies(world), allConstraints = Composite.allConstraints(world); @@ -3045,7 +3065,7 @@ var Engine = {}; Body.applyGravityAll(allBodies, world.gravity); // update all body position and rotation by integration - Body.updateAll(allBodies, delta * timing.timeScale, correction, world.bounds); + Body.updateAll(allBodies, delta, timing.timeScale, correction, world.bounds); // update all constraints for (i = 0; i < engine.constraintIterations; i++) { @@ -3085,7 +3105,7 @@ var Engine = {}; // iteratively resolve velocity between collisions Resolver.preSolveVelocity(pairs.list); for (i = 0; i < engine.velocityIterations; i++) { - Resolver.solveVelocity(pairs.list); + Resolver.solveVelocity(pairs.list, timing.timeScale); } // iteratively resolve position between collisions @@ -3104,8 +3124,27 @@ var Engine = {}; if (world.isModified) Composite.setModified(world, false, false, true); + Events.trigger(engine, 'afterUpdate', event); + return engine; }; + + /** + * Renders the world by calling its defined renderer `engine.render.controller`. Triggers `beforeRender` and `afterRender` events. + * @method render + * @param {engine} engineA + * @param {engine} engineB + */ + Engine.render = function(engine) { + // create an event object + var event = { + timestamp: engine.timing.timestamp + }; + + Events.trigger(engine, 'beforeRender', event); + engine.render.controller.world(engine); + Events.trigger(engine, 'afterRender', event); + }; /** * Description @@ -3572,16 +3611,20 @@ var Mouse; var mouse = this; this.element = element || document.body; + this.absolute = { x: 0, y: 0 }; this.position = { x: 0, y: 0 }; this.mousedownPosition = { x: 0, y: 0 }; this.mouseupPosition = { x: 0, y: 0 }; this.offset = { x: 0, y: 0 }; + this.scale = { x: 1, y: 1 }; + this.wheelDelta = 0; this.button = -1; this.sourceEvents = { mousemove: null, mousedown: null, - mouseup: null + mouseup: null, + mousewheel: null }; this.mousemove = function(event) { @@ -3593,8 +3636,10 @@ var Mouse; event.preventDefault(); } - mouse.position.x = position.x + mouse.offset.x; - mouse.position.y = position.y + mouse.offset.y; + mouse.absolute.x = position.x; + mouse.absolute.y = position.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; mouse.sourceEvents.mousemove = event; }; @@ -3609,10 +3654,12 @@ var Mouse; mouse.button = event.button; } - mouse.position.x = position.x + mouse.offset.x; - mouse.position.y = position.y + mouse.offset.y; - mouse.mousedownPosition.x = position.x + mouse.offset.x; - mouse.mousedownPosition.y = position.y + mouse.offset.y; + mouse.absolute.x = position.x; + mouse.absolute.y = position.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; + mouse.mousedownPosition.x = mouse.position.x; + mouse.mousedownPosition.y = mouse.position.y; mouse.sourceEvents.mousedown = event; }; @@ -3625,13 +3672,20 @@ var Mouse; } mouse.button = -1; - mouse.position.x = position.x + mouse.offset.x; - mouse.position.y = position.y + mouse.offset.y; - mouse.mouseupPosition.x = position.x + mouse.offset.x; - mouse.mouseupPosition.y = position.y + mouse.offset.y; + mouse.absolute.x = position.x; + mouse.absolute.y = position.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; + mouse.mouseupPosition.x = mouse.position.x; + mouse.mouseupPosition.y = mouse.position.y; mouse.sourceEvents.mouseup = event; }; + this.mousewheel = function(event) { + mouse.wheelDelta = Math.max(-1, Math.min(1, event.wheelDelta || -event.detail)); + event.preventDefault(); + }; + Mouse.setElement(mouse, mouse.element); }; @@ -3658,6 +3712,9 @@ var Mouse; element.addEventListener('mousedown', mouse.mousedown); element.addEventListener('mouseup', mouse.mouseup); + element.addEventListener("mousewheel", mouse.mousewheel); + element.addEventListener("DOMMouseScroll", mouse.mousewheel); + element.addEventListener('touchmove', mouse.mousemove); element.addEventListener('touchstart', mouse.mousedown); element.addEventListener('touchend', mouse.mouseup); @@ -3672,6 +3729,32 @@ var Mouse; mouse.sourceEvents.mousemove = null; mouse.sourceEvents.mousedown = null; mouse.sourceEvents.mouseup = null; + mouse.sourceEvents.mousewheel = null; + mouse.wheelDelta = 0; + }; + + /** + * Sets the offset + * @method setOffset + * @param {mouse} mouse + */ + Mouse.setOffset = function(mouse, offset) { + mouse.offset.x = offset.x; + mouse.offset.y = offset.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; + }; + + /** + * Sets the scale + * @method setScale + * @param {mouse} mouse + */ + Mouse.setScale = function(mouse, scale) { + mouse.scale.x = scale.x; + mouse.scale.y = scale.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; }; /** @@ -3684,8 +3767,9 @@ var Mouse; */ 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, + rootNode = (document.documentElement || document.body.parentNode || document.body), + scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset : rootNode.scrollLeft, + scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset : rootNode.scrollTop, touches = event.changedTouches, x, y; @@ -3854,11 +3938,16 @@ var Bodies = {}; var rectangle = { label: 'Rectangle Body', position: { x: x, y: y }, - render: { - path: 'L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height - } + vertices: Vertices.fromPath('L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height) }; + if (options.chamfer) { + var chamfer = options.chamfer; + rectangle.vertices = Vertices.chamfer(rectangle.vertices, chamfer.radius, + chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); + delete options.chamfer; + } + return Body.create(Common.extend({}, rectangle, options)); }; @@ -3886,11 +3975,16 @@ var Bodies = {}; var trapezoid = { label: 'Trapezoid Body', position: { x: x, y: y }, - render: { - path: 'L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0' - } + vertices: Vertices.fromPath('L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0') }; + if (options.chamfer) { + var chamfer = options.chamfer; + trapezoid.vertices = Vertices.chamfer(trapezoid.vertices, chamfer.radius, + chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); + delete options.chamfer; + } + return Body.create(Common.extend({}, trapezoid, options)); }; @@ -3954,11 +4048,16 @@ var Bodies = {}; var polygon = { label: 'Polygon Body', position: { x: x, y: y }, - render: { - path: path - } + vertices: Vertices.fromPath(path) }; + if (options.chamfer) { + var chamfer = options.chamfer; + polygon.vertices = Vertices.chamfer(polygon.vertices, chamfer.radius, + chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); + delete options.chamfer; + } + return Body.create(Common.extend({}, polygon, options)); }; @@ -4204,7 +4303,13 @@ var Composites = {}; wheelYOffset = 0; var car = Composite.create({ label: 'Car' }), - body = Bodies.trapezoid(xx, yy, width, height, 0.3, { groupId: groupId, friction: 0.01 }); + body = Bodies.trapezoid(xx, yy, width, height, 0.3, { + groupId: groupId, + friction: 0.01, + chamfer: { + radius: 10 + } + }); var wheelA = Bodies.circle(xx + wheelAOffset, yy + wheelYOffset, wheelSize, { groupId: groupId, @@ -4703,23 +4808,30 @@ var Vertices = {}; * @return {vector} The centre point */ Vertices.centre = function(vertices) { - var cx = 0, cy = 0; + var area = Vertices.area(vertices, true), + centre = { x: 0, y: 0 }, + cross, + temp, + j; for (var i = 0; i < vertices.length; i++) { - cx += vertices[i].x; - cy += vertices[i].y; + j = (i + 1) % vertices.length; + cross = Vector.cross(vertices[i], vertices[j]); + temp = Vector.mult(Vector.add(vertices[i], vertices[j]), cross); + centre = Vector.add(centre, temp); } - return { x: cx / vertices.length, y: cy / vertices.length }; + return Vector.div(centre, 6 * area); }; /** * Description * @method area * @param {vertices} vertices + * @param {bool} signed * @return {number} The area */ - Vertices.area = function(vertices) { + Vertices.area = function(vertices, signed) { var area = 0, j = vertices.length - 1; @@ -4728,6 +4840,9 @@ var Vertices = {}; j = i; } + if (signed) + return area / 2; + return Math.abs(area) / 2; }; @@ -4849,6 +4964,80 @@ var Vertices = {}; return vertices; }; + /** + * Chamfers a set of vertices by giving them rounded corners, returns a new set of vertices. + * The radius parameter is a single number or an array to specify the radius for each vertex. + * @method chamfer + * @param {vertices} vertices + * @param {number[]} radius + * @param {number} quality + * @param {number} qualityMin + * @param {number} qualityMax + */ + Vertices.chamfer = function(vertices, radius, quality, qualityMin, qualityMax) { + radius = radius || [8]; + + if (!radius.length) + radius = [radius]; + + // quality defaults to -1, which is auto + quality = (typeof quality !== 'undefined') ? quality : -1; + qualityMin = qualityMin || 2; + qualityMax = qualityMax || 14; + + var centre = Vertices.centre(vertices), + newVertices = []; + + for (var i = 0; i < vertices.length; i++) { + var prevVertex = vertices[i - 1 >= 0 ? i - 1 : vertices.length - 1], + vertex = vertices[i], + nextVertex = vertices[(i + 1) % vertices.length], + currentRadius = radius[i < radius.length ? i : radius.length - 1]; + + if (currentRadius === 0) { + newVertices.push(vertex); + continue; + } + + var prevNormal = Vector.normalise({ + x: vertex.y - prevVertex.y, + y: prevVertex.x - vertex.x + }); + + var nextNormal = Vector.normalise({ + x: nextVertex.y - vertex.y, + y: vertex.x - nextVertex.x + }); + + var diagonalRadius = Math.sqrt(2 * Math.pow(currentRadius, 2)), + radiusVector = Vector.mult(Common.clone(prevNormal), currentRadius), + midNormal = Vector.normalise(Vector.mult(Vector.add(prevNormal, nextNormal), 0.5)), + scaledVertex = Vector.sub(vertex, Vector.mult(midNormal, diagonalRadius)); + + var precision = quality; + + if (quality === -1) { + // automatically decide precision + precision = Math.pow(currentRadius, 0.32) * 1.75; + } + + precision = Common.clamp(precision, qualityMin, qualityMax); + + // use an even value for precision, more likely to reduce axes by using symmetry + if (precision % 2 === 1) + precision += 1; + + var alpha = Math.acos(Vector.dot(prevNormal, nextNormal)), + theta = alpha / precision; + + for (var j = 0; j < precision; j++) { + newVertices.push(Vector.add(Vector.rotate(radiusVector, theta * j), scaledVertex)); + } + } + + return newVertices; + }; + })(); ; // End src/geometry/Vertices.js @@ -4990,6 +5179,11 @@ var Render = {}; context.globalCompositeOperation = 'source-over'; // handle bounds + var boundsWidth = render.bounds.max.x - render.bounds.min.x, + boundsHeight = render.bounds.max.y - render.bounds.min.y, + boundsScaleX = boundsWidth / render.options.width, + boundsScaleY = boundsHeight / render.options.height; + if (options.hasBounds) { // filter out bodies that are not in view for (i = 0; i < allBodies.length; i++) { @@ -5016,7 +5210,8 @@ var Render = {}; constraints.push(constraint); } - // translate the view + // transform the view + context.scale(1 / boundsScaleX, 1 / boundsScaleY); context.translate(-render.bounds.min.x, -render.bounds.min.y); } else { constraints = allConstraints; @@ -5057,8 +5252,10 @@ var Render = {}; if (options.showDebug) Render.debug(engine, context); - if (options.hasBounds) - context.translate(render.bounds.min.x, render.bounds.min.y); + if (options.hasBounds) { + // revert view transforms + context.setTransform(1, 0, 0, 1, 0, 0); + } }; /** @@ -5609,8 +5806,15 @@ var Render = {}; options = render.options, bounds; - if (options.hasBounds) + if (options.hasBounds) { + var boundsWidth = render.bounds.max.x - render.bounds.min.x, + boundsHeight = render.bounds.max.y - render.bounds.min.y, + boundsScaleX = boundsWidth / render.options.width, + boundsScaleY = boundsHeight / render.options.height; + + context.scale(1 / boundsScaleX, 1 / boundsScaleY); context.translate(-render.bounds.min.x, -render.bounds.min.y); + } for (var i = 0; i < selected.length; i++) { var item = selected[i].data; @@ -5670,7 +5874,7 @@ var Render = {}; } if (options.hasBounds) - context.translate(render.bounds.min.x, render.bounds.min.y); + context.setTransform(1, 0, 0, 1, 0, 0); }; /** @@ -6092,1414 +6296,6 @@ var RenderPixi = {}; ; // End src/render/RenderPixi.js -// Begin src/tools/Gui.js - -/** -* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) -* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. -* -* @class Gui -*/ - -var Gui = {}; - -(function() { - - /** - * Description - * @method create - * @param {engine} engine - * @param {object} options - * @return {gui} A container for a configured dat.gui - */ - Gui.create = function(engine, options) { - var _datGuiSupported = window.dat && window.localStorage; - - if (!_datGuiSupported) { - console.log("Could not create GUI. Check dat.gui library is loaded first."); - return; - } - - var datGui = new dat.GUI(options); - - var gui = { - engine: engine, - datGui: datGui, - amount: 1, - size: 40, - sides: 4, - density: 0.001, - restitution: 0, - friction: 0.1, - frictionAir: 0.01, - offset: { x: 0, y: 0 }, - renderer: 'canvas' - }; - - if (Resurrect) { - gui.serializer = new Resurrect({ prefix: '$', cleanup: true }); - gui.serializer.parse = gui.serializer.resurrect; - } - - _initDatGui(gui); - - return gui; - }; - - /** - * Description - * @method update - * @param {gui} gui - * @param {datGui} datGui - */ - Gui.update = function(gui, datGui) { - var i; - datGui = datGui || gui.datGui; - - for (i in datGui.__folders) { - Gui.update(gui, datGui.__folders[i]); - } - - for (i in datGui.__controllers) { - var controller = datGui.__controllers[i]; - if (controller.updateDisplay) - controller.updateDisplay(); - } - }; - - /** - * Description - * @method closeAll - * @param {gui} gui - */ - Gui.closeAll = function(gui) { - var datGui = gui.datGui; - - for (var i in datGui.__folders) { - datGui.__folders[i].close(); - } - }; - - /** - * Saves world state to local storage - * @method saveState - * @param {object} serializer - * @param {engine} engine - * @param {string} key - */ - Gui.saveState = function(serializer, engine, key) { - if (localStorage && serializer) - localStorage.setItem(key, Gui.serialise(serializer, engine.world)); - }; - - /** - * Loads world state from local storage - * @method loadState - * @param {object} serializer - * @param {engine} engine - * @param {string} key - */ - Gui.loadState = function(serializer, engine, key) { - var loadedWorld; - - if (localStorage && serializer) - loadedWorld = serializer.parse(localStorage.getItem(key)); - - if (loadedWorld) - Engine.merge(engine, { world: loadedWorld }); - }; - - /** - * Serialises the object using the given serializer and a Matter-specific replacer - * @method serialise - * @param {object} serializer - * @param {object} object - * @param {number} indent - * @return {string} The serialised object - */ - Gui.serialise = function(serializer, object, indent) { - indent = indent || 0; - return serializer.stringify(object, function(key, value) { - // skip non-required values - if (key === 'path') - return undefined; - - // limit precision of floats - if (!/^#/.exec(key) && typeof value === 'number') { - var fixed = parseFloat(value.toFixed(3)); - - // do not limit if limiting will cause value to zero - // TODO: this should ideally dynamically find the SF precision required - if (fixed === 0 && value !== 0) - return value; - - return fixed; - } - - return value; - }, indent); - }; - - var _initDatGui = function(gui) { - var engine = gui.engine, - datGui = gui.datGui; - - var funcs = { - addBody: function() { _addBody(gui); }, - clear: function() { _clear(gui); }, - save: function() { Gui.saveState(gui.serializer, engine, 'guiState'); Events.trigger(gui, 'save'); }, - load: function() { Gui.loadState(gui.serializer, engine, 'guiState'); Events.trigger(gui, 'load'); }, - inspect: function() { - if (!Inspector.instance) - gui.inspector = Inspector.create(gui.engine); - } - }; - - var metrics = datGui.addFolder('Metrics'); - metrics.add(engine.timing, 'fps').listen(); - - if (engine.metrics.extended) { - 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.add(engine.metrics, 'narrowReuse').listen(); - metrics.close(); - } else { - metrics.open(); - } - - 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, 'inspect'); - 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']) - .onFinishChange(function(value) { - Composite.setModified(engine.world, true, false, false); - }); - - physics.add(engine.timing, 'timeScale', 0, 1.2).step(0.05).listen(); - 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(gui, 'renderer', ['canvas', 'webgl']) - .onFinishChange(function(value) { _setRenderer(gui, value); }); - - 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(); - }; - - var _setRenderer = function(gui, rendererName) { - var engine = gui.engine, - controller; - - if (rendererName === 'canvas') - controller = Render; - - if (rendererName === 'webgl') - controller = RenderPixi; - - // remove old canvas - engine.render.element.removeChild(engine.render.canvas); - - // create new renderer using the same options object - var options = engine.render.options; - - engine.render = controller.create({ - element: engine.render.element, - options: options - }); - - engine.render.options = options; - - // bind the mouse to the new canvas - Mouse.setElement(engine.input.mouse, engine.render.canvas); - }; - - var _addBody = function(gui) { - var engine = gui.engine; - - var options = { - density: gui.density, - friction: gui.friction, - frictionAir: gui.frictionAir, - restitution: gui.restitution - }; - - for (var i = 0; i < gui.amount; i++) { - World.add(engine.world, Bodies.polygon(gui.offset.x + 120 + i * gui.size + i * 50, gui.offset.y + 200, gui.sides, gui.size, options)); - } - }; - - var _clear = function(gui) { - var engine = gui.engine; - - World.clear(engine.world, true); - Engine.clear(engine); - - // clear scene graph (if defined in controller) - var renderController = engine.render.controller; - if (renderController.clear) - renderController.clear(engine.render); - - Events.trigger(gui, 'clear'); - }; - - /* - * - * Events Documentation - * - */ - - /** - * Fired after the gui's clear button pressed - * - * @event clear - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - - /** - * Fired after the gui's save button pressed - * - * @event save - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - - /** - * Fired after the gui's load button pressed - * - * @event load - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - -})(); - -; // End src/tools/Gui.js - - -// Begin src/tools/Inspector.js - -/** -* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) -* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. -* -* @class Inspector -*/ - -var Inspector = {}; - -(function() { - - var _key, - _isWebkit = 'WebkitAppearance' in document.documentElement.style, - $body; - - /** - * Creates a new inspector tool and inserts it into the page. Requires keymaster, jQuery, jsTree libraries. - * @method create - * @param {engine} engine - * @param {object} options - * @return {inspector} An inspector - */ - Inspector.create = function(engine, options) { - if (!jQuery || !$.fn.jstree || !window.key) { - console.log('Could not create inspector. Check keymaster, jQuery, jsTree libraries are loaded first.'); - return; - } - - var inspector = { - engine: engine, - isPaused: false, - selected: [], - selectStart: null, - selectEnd: null, - selectBounds: Bounds.create(), - mousePrevPosition: { x: 0, y: 0 }, - offset: { x: 0, y: 0 }, - autoHide: true, - autoRewind: true, - hasTransitions: _isWebkit ? true : false, - bodyClass: '', - exportIndent: 0, - controls: { - container: null, - worldTree: null - }, - root: Composite.create({ - label: 'Root' - }) - }; - - inspector = Common.extend(inspector, options); - Inspector.instance = inspector; - - inspector.serializer = new Resurrect({ prefix: '$', cleanup: true }); - inspector.serializer.parse = inspector.serializer.resurrect; - localStorage.removeItem('pauseState'); - - $body = $('body'); - $body.toggleClass('ins-auto-hide gui-auto-hide', inspector.autoHide); - $body.toggleClass('ins-transitions gui-transitions', inspector.hasTransitions); - - Composite.add(inspector.root, engine.world); - engine.world.isModified = true; - engine.world.parent = null; - _key = window.key; - - _initControls(inspector); - _initEngineEvents(inspector); - _initTree(inspector); - _initKeybinds(inspector); - - return inspector; - }; - - var _initControls = function(inspector) { - var engine = inspector.engine, - controls = inspector.controls; - - var $inspectorContainer = $('
'), - $buttonGroup = $('
'), - $searchBox = $(''), - $importButton = $(''), - $exportButton = $(''), - $pauseButton = $(''), - $helpButton = $(''), - $addCompositeButton = $(''); - - $buttonGroup.append($pauseButton, $importButton, $exportButton, $helpButton); - $inspectorContainer.prepend($buttonGroup, $searchBox, $addCompositeButton); - $body.prepend($inspectorContainer); - - controls.pauseButton = $pauseButton; - controls.importButton = $importButton; - controls.exportButton = $exportButton; - controls.helpButton = $helpButton; - controls.searchBox = $searchBox; - controls.container = $inspectorContainer; - controls.addCompositeButton = $addCompositeButton; - - controls.pauseButton.click(function() { - _setPaused(inspector, !inspector.isPaused); - }); - - controls.exportButton.click(function() { - _exportFile(inspector); - }); - - controls.importButton.click(function() { - _importFile(inspector); - }); - - controls.helpButton.click(function() { - _showHelp(inspector); - }); - - controls.addCompositeButton.click(function() { - _addNewComposite(inspector); - }); - - var searchTimeout; - controls.searchBox.keyup(function () { - clearTimeout(searchTimeout); - searchTimeout = setTimeout(function () { - var value = controls.searchBox.val(), - worldTree = controls.worldTree.data('jstree'); - worldTree.search(value); - }, 250); - }); - }; - - var _showHelp = function(inspector) { - var help = "Matter Tools\n\n"; - - help += "Drag nodes in the tree to move them between composites.\n"; - help += "Use browser's developer console to inspect selected objects.\n"; - help += "Note: selections only render if renderer supports it.\n\n"; - - help += "[shift + space] pause or play simulation.\n"; - help += "[right click] and drag on empty space to select a region.\n"; - help += "[right click] and drag on an object to move it.\n"; - help += "[right click + shift] and drag to move whole selection.\n"; - help += "[del] or [backspace] delete selected objects.\n\n"; - - help += "[shift + s] scale-xy selected objects with mouse or arrows.\n"; - help += "[shift + s + d] scale-x selected objects with mouse or arrows.\n"; - help += "[shift + s + f] scale-y selected objects with mouse or arrows.\n"; - help += "[shift + r] rotate selected objects with mouse or arrows.\n\n"; - - help += "[shift + q] set selected objects as static (can't be undone).\n"; - help += "[shift + i] import objects.\n"; - help += "[shift + o] export selected objects.\n"; - help += "[shift + h] toggle Matter.Gui.\n"; - help += "[shift + y] toggle auto-hide.\n"; - help += "[shift + r] toggle auto-rewind on play/pause.\n\n"; - - help += "[shift + j] show this help message."; - - alert(help); - }; - - var _initKeybinds = function(inspector) { - var engine = inspector.engine, - controls = inspector.controls; - - _key('shift+space', function() { - _setPaused(inspector, !inspector.isPaused); - }); - - _key('shift+o', function() { - _exportFile(inspector); - }); - - _key('shift+i', function() { - _importFile(inspector); - }); - - _key('shift+j', function() { - _showHelp(inspector); - }); - - _key('shift+y', function() { - inspector.autoHide = !inspector.autoHide; - $body.toggleClass('ins-auto-hide gui-auto-hide', inspector.autoHide); - }); - - _key('shift+r', function() { - inspector.autoRewind = !inspector.autoRewind; - if (!inspector.autoRewind) - localStorage.removeItem('pauseState'); - }); - - _key('shift+q', function() { - var worldTree = inspector.controls.worldTree.data('jstree'); - for (var i = 0; i < inspector.selected.length; i++) { - var object = inspector.selected[i].data; - if (object.type === 'body' && !object.isStatic) - Body.setStatic(object, true); - } - }); - - _key('del', function() { - _deleteSelectedObjects(inspector); - }); - - _key('backspace', function() { - _deleteSelectedObjects(inspector); - }); - - // prevent the backspace key from navigating back - // http://stackoverflow.com/questions/1495219/how-can-i-prevent-the-backspace-key-from-navigating-back - $(document).unbind('keydown').bind('keydown', function (event) { - var doPrevent = false; - if (event.keyCode === 8) { - var d = event.srcElement || event.target; - if ((d.tagName.toUpperCase() === 'INPUT' && (d.type.toUpperCase() === 'TEXT' || d.type.toUpperCase() === 'PASSWORD' || d.type.toUpperCase() === 'FILE' || d.type.toUpperCase() === 'EMAIL' || d.type.toUpperCase() === 'SEARCH')) || d.tagName.toUpperCase() === 'TEXTAREA') { - doPrevent = d.readOnly || d.disabled; - } - else { - doPrevent = true; - } - } - - if (doPrevent) { - event.preventDefault(); - } - }); - }; - - var _initTree = function(inspector) { - var engine = inspector.engine, - controls = inspector.controls, - deferTimeout; - - var worldTreeOptions = { - 'core': { - 'check_callback': true - }, - 'dnd': { - 'copy': false - }, - 'search': { - 'show_only_matches': true, - 'fuzzy': false - }, - 'types': { - '#': { - 'valid_children': [] - }, - 'body': { - 'valid_children': [] - }, - 'constraint': { - 'valid_children': [] - }, - 'composite': { - 'valid_children': [] - }, - 'bodies': { - 'valid_children': ['body'] - }, - 'constraints': { - 'valid_children': ['constraint'] - }, - 'composites': { - 'valid_children': ['composite'] - } - }, - 'plugins' : ['dnd', 'types', 'unique', 'search'] - }; - - controls.worldTree = $('
').jstree(worldTreeOptions); - controls.container.prepend(controls.worldTree); - - controls.worldTree.on('changed.jstree', function(event, data) { - var selected = [], - worldTree = controls.worldTree.data('jstree'); - - if (data.action !== 'select_node') - return; - - // defer selection update until selection has finished propagating - clearTimeout(deferTimeout); - deferTimeout = setTimeout(function() { - data.selected = worldTree.get_selected(); - - for (var i = 0; i < data.selected.length; i++) { - var nodeId = data.selected[i], - objectType = nodeId.split('_')[0], - objectId = nodeId.split('_')[1], - worldObject = Composite.get(engine.world, objectId, objectType); - - switch (objectType) { - case 'body': - case 'constraint': - case 'composite': - selected.push(worldObject); - break; - } - } - - _setSelectedObjects(inspector, selected); - - }, 1); - }); - - $(document).on('dnd_stop.vakata', function(event, data) { - var worldTree = controls.worldTree.data('jstree'), - nodes = data.data.nodes; - - // handle drag and drop - // move items between composites - for (var i = 0; i < nodes.length; i++) { - var node = worldTree.get_node(nodes[i]), - parentNode = worldTree.get_node(worldTree.get_parent(nodes[i])), - prevCompositeId = node.data.compositeId, - newCompositeId = parentNode.data.compositeId; - - if (prevCompositeId === newCompositeId) - continue; - - var nodeId = nodes[i], - objectType = nodeId.split('_')[0], - objectId = nodeId.split('_')[1], - worldObject = Composite.get(inspector.root, objectId, objectType), - prevComposite = Composite.get(inspector.root, prevCompositeId, 'composite'), - newComposite = Composite.get(inspector.root, newCompositeId, 'composite'); - - Composite.move(prevComposite, worldObject, newComposite); - } - }); - - controls.worldTree.on('dblclick.jstree', function(event, data) { - var worldTree = controls.worldTree.data('jstree'), - selected = worldTree.get_selected(); - - // select all children of double clicked node - for (var i = 0; i < selected.length; i++) { - var nodeId = selected[i], - objectType = nodeId.split('_')[0], - objectId = nodeId.split('_')[1], - worldObject = Composite.get(engine.world, objectId, objectType); - - switch (objectType) { - case 'composite': - case 'composites': - case 'bodies': - case 'constraints': - var node = worldTree.get_node(nodeId), - children = worldTree.get_node(nodeId).children; - - for (var j = 0; j < children.length; j++) - worldTree.select_node(children[j], false); - - break; - } - } - }); - }; - - var _addBodyClass = function(inspector, classNames) { - // only apply changes to prevent DOM lag - if (inspector.bodyClass.indexOf(' ' + classNames) === -1) { - $body.addClass(classNames); - inspector.bodyClass = ' ' + $body.attr('class'); - } - }; - - var _removeBodyClass = function(inspector, classNames) { - // only apply changes to prevent DOM lag - var updateRequired = false, - classes = classNames.split(' '); - - for (var i = 0; i < classes.length; i++) { - updateRequired = inspector.bodyClass.indexOf(' ' + classes[i]) !== -1; - if (updateRequired) - break; - } - - if (updateRequired) { - $body.removeClass(classNames); - inspector.bodyClass = ' ' + $body.attr('class'); - } - }; - - var _getMousePosition = function(inspector) { - return Vector.add(inspector.engine.input.mouse.position, inspector.offset); - }; - - var _initEngineEvents = function(inspector) { - var engine = inspector.engine, - mouse = engine.input.mouse, - mousePosition = _getMousePosition(inspector), - controls = inspector.controls; - - Events.on(engine, 'tick', function() { - // update mouse position reference - mousePosition = _getMousePosition(inspector); - - var mouseDelta = mousePosition.x - inspector.mousePrevPosition.x, - keyDelta = _key.isPressed('up') + _key.isPressed('right') - _key.isPressed('down') - _key.isPressed('left'), - delta = mouseDelta + keyDelta; - - // update interface when world changes - if (engine.world.isModified) { - var data = _generateCompositeTreeNode(inspector.root, null, true); - _updateTree(controls.worldTree.data('jstree'), data); - _setSelectedObjects(inspector, []); - } - - // update region selection - if (inspector.selectStart !== null) { - inspector.selectEnd.x = mousePosition.x; - inspector.selectEnd.y = mousePosition.y; - Bounds.update(inspector.selectBounds, [inspector.selectStart, inspector.selectEnd]); - } - - // rotate mode - if (_key.shift && _key.isPressed('r')) { - var rotateSpeed = 0.03, - angle = Math.max(-2, Math.min(2, delta)) * rotateSpeed; - - _addBodyClass(inspector, 'ins-cursor-rotate'); - _rotateSelectedObjects(inspector, angle); - } else { - _removeBodyClass(inspector, 'ins-cursor-rotate'); - } - - // scale mode - if (_key.shift && _key.isPressed('s')) { - var scaleSpeed = 0.02, - scale = 1 + Math.max(-2, Math.min(2, delta)) * scaleSpeed; - - _addBodyClass(inspector, 'ins-cursor-scale'); - - if (_key.isPressed('d')) { - scaleX = scale; - scaleY = 1; - } else if (_key.isPressed('f')) { - scaleX = 1; - scaleY = scale; - } else { - scaleX = scaleY = scale; - } - - _scaleSelectedObjects(inspector, scaleX, scaleY); - } else { - _removeBodyClass(inspector, 'ins-cursor-scale'); - } - - // translate mode - if (mouse.button === 2 && !mouse.sourceEvents.mousedown && !mouse.sourceEvents.mouseup) { - _addBodyClass(inspector, 'ins-cursor-move'); - _moveSelectedObjects(inspector, mousePosition.x, mousePosition.y); - } else { - _removeBodyClass(inspector, 'ins-cursor-move'); - } - - inspector.mousePrevPosition = Common.clone(mousePosition); - }); - - Events.on(engine, 'mouseup', function(event) { - // select objects in region if making a region selection - if (inspector.selectStart !== null) { - var selected = Query.region(Composite.allBodies(engine.world), inspector.selectBounds); - _setSelectedObjects(inspector, selected); - } - - // clear selection region - inspector.selectStart = null; - inspector.selectEnd = null; - Events.trigger(inspector, 'selectEnd'); - }); - - Events.on(engine, 'mousedown', function(event) { - var engine = event.source, - bodies = Composite.allBodies(engine.world), - constraints = Composite.allConstraints(engine.world), - isUnionSelect = _key.shift || _key.control, - worldTree = inspector.controls.worldTree.data('jstree'), - i; - - if (mouse.button === 2) { - var hasSelected = false; - - for (i = 0; i < bodies.length; i++) { - var body = bodies[i]; - - if (Bounds.contains(body.bounds, mousePosition) && Vertices.contains(body.vertices, mousePosition)) { - - if (isUnionSelect) { - _addSelectedObject(inspector, body); - } else { - _setSelectedObjects(inspector, [body]); - } - - hasSelected = true; - break; - } - } - - if (!hasSelected) { - for (i = 0; i < constraints.length; i++) { - var constraint = constraints[i], - bodyA = constraint.bodyA, - bodyB = constraint.bodyB; - - if (constraint.label.indexOf('Mouse Constraint') !== -1) - continue; - - var pointAWorld = constraint.pointA, - pointBWorld = constraint.pointB; - - if (bodyA) pointAWorld = Vector.add(bodyA.position, constraint.pointA); - if (bodyB) pointBWorld = Vector.add(bodyB.position, constraint.pointB); - - if (!pointAWorld || !pointBWorld) - continue; - - var distA = Vector.magnitudeSquared(Vector.sub(mousePosition, pointAWorld)), - distB = Vector.magnitudeSquared(Vector.sub(mousePosition, pointBWorld)); - - if (distA < 100 || distB < 100) { - if (isUnionSelect) { - _addSelectedObject(inspector, constraint); - } else { - _setSelectedObjects(inspector, [constraint]); - } - - hasSelected = true; - break; - } - } - - if (!hasSelected) { - worldTree.deselect_all(true); - _setSelectedObjects(inspector, []); - - inspector.selectStart = Common.clone(mousePosition); - inspector.selectEnd = Common.clone(mousePosition); - Bounds.update(inspector.selectBounds, [inspector.selectStart, inspector.selectEnd]); - - Events.trigger(inspector, 'selectStart'); - } else { - inspector.selectStart = null; - inspector.selectEnd = null; - } - } - } - - if (mouse.button === 2 && inspector.selected.length > 0) { - _addBodyClass(inspector, 'ins-cursor-move'); - - _updateSelectedMouseDownOffset(inspector); - } - }); - - // render hook - Events.on(engine, 'afterRender', function() { - var renderController = engine.render.controller, - context = engine.render.context; - if (renderController.inspector) - renderController.inspector(inspector, context); - }); - }; - - var _deleteSelectedObjects = function(inspector) { - var objects = [], - object, - worldTree = inspector.controls.worldTree.data('jstree'), - i; - - // delete objects in world - for (i = 0; i < inspector.selected.length; i++) { - object = inspector.selected[i].data; - if (object !== inspector.engine.world) - objects.push(object); - } - - // also delete non-world composites (selected only in the UI tree) - var selectedNodes = worldTree.get_selected(); - for (i = 0; i < selectedNodes.length; i++) { - var node = worldTree.get_node(selectedNodes[i]); - if (node.type === 'composite') { - node = worldTree.get_node(node.children[0]); - if (node.data) { - var compositeId = node.data.compositeId; - object = Composite.get(inspector.root, compositeId, 'composite'); - if (object && object !== inspector.engine.world) { - objects.push(object); - worldTree.delete_node(selectedNodes[i]); - } - } - } - } - - Composite.remove(inspector.root, objects, true); - _setSelectedObjects(inspector, []); - }; - - var _updateSelectedMouseDownOffset = function(inspector) { - var selected = inspector.selected, - mouse = inspector.engine.input.mouse, - mousePosition = _getMousePosition(inspector), - item, - data; - - for (var i = 0; i < selected.length; i++) { - item = selected[i]; - data = item.data; - - if (data.position) { - item.mousedownOffset = { - x: mousePosition.x - data.position.x, - y: mousePosition.y - data.position.y - }; - } else if (data.pointA && !data.bodyA) { - item.mousedownOffset = { - x: mousePosition.x - data.pointA.x, - y: mousePosition.y - data.pointA.y - }; - } else if (data.pointB && !data.bodyB) { - item.mousedownOffset = { - x: mousePosition.x - data.pointB.x, - y: mousePosition.y - data.pointB.y - }; - } - } - }; - - var _moveSelectedObjects = function(inspector, x, y) { - var selected = inspector.selected, - mouse = inspector.engine.input.mouse, - mousePosition = _getMousePosition(inspector), - item, - data; - - for (var i = 0; i < selected.length; i++) { - item = selected[i]; - data = item.data; - - if (!item.mousedownOffset) - continue; - - switch (data.type) { - - case 'body': - var delta = { - x: x - data.position.x - item.mousedownOffset.x, - y: y - data.position.y - item.mousedownOffset.y - }; - - Body.translate(data, delta); - data.positionPrev.x = data.position.x; - data.positionPrev.y = data.position.y; - - break; - - case 'constraint': - var point = data.pointA; - if (data.bodyA) - point = data.pointB; - - point.x = x - item.mousedownOffset.x; - point.y = y - item.mousedownOffset.y; - - var initialPointA = data.bodyA ? Vector.add(data.bodyA.position, data.pointA) : data.pointA, - initialPointB = data.bodyB ? Vector.add(data.bodyB.position, data.pointB) : data.pointB; - - data.length = Vector.magnitude(Vector.sub(initialPointA, initialPointB)); - - break; - - } - } - }; - - var _scaleSelectedObjects = function(inspector, scaleX, scaleY) { - var selected = inspector.selected, - item, - data; - - for (var i = 0; i < selected.length; i++) { - item = selected[i]; - data = item.data; - - switch (data.type) { - case 'body': - Body.scale(data, scaleX, scaleY, data.position); - - if (data.circleRadius) - data.circleRadius *= scaleX; - - break; - } - } - }; - - var _rotateSelectedObjects = function(inspector, angle) { - var selected = inspector.selected, - item, - data; - - for (var i = 0; i < selected.length; i++) { - item = selected[i]; - data = item.data; - - switch (data.type) { - case 'body': - Body.rotate(data, angle); - break; - } - } - }; - - var _setPaused = function(inspector, isPaused) { - if (isPaused) { - if (inspector.autoRewind) { - _setSelectedObjects(inspector, []); - Gui.loadState(inspector.serializer, inspector.engine, 'pauseState'); - } - - inspector.engine.timing.timeScale = 0; - inspector.isPaused = true; - inspector.controls.pauseButton.text('Play'); - - Events.trigger(inspector, 'paused'); - } else { - if (inspector.autoRewind) { - Gui.saveState(inspector.serializer, inspector.engine, 'pauseState'); - } - - inspector.engine.timing.timeScale = 1; - inspector.isPaused = false; - inspector.controls.pauseButton.text('Pause'); - - Events.trigger(inspector, 'play'); - } - }; - - var _setSelectedObjects = function(inspector, objects) { - var worldTree = inspector.controls.worldTree.data('jstree'), - selectedItems = [], - data, - i; - - for (i = 0; i < inspector.selected.length; i++) { - data = inspector.selected[i].data; - worldTree.deselect_node(data.type + '_' + data.id, true); - } - - inspector.selected = []; - console.clear(); - - for (i = 0; i < objects.length; i++) { - data = objects[i]; - - if (data) { - // add the object to the selection - _addSelectedObject(inspector, data); - - // log selected objects to console for property inspection - if (i < 5) { - console.log(data.label + ' ' + data.id + ': %O', data); - } else if (i === 6) { - console.warn('Omitted inspecting ' + (objects.length - 5) + ' more objects'); - } - } - } - }; - - var _addSelectedObject = function(inspector, object) { - if (!object) - return; - - var worldTree = inspector.controls.worldTree.data('jstree'); - inspector.selected.push({ data: object }); - worldTree.select_node(object.type + '_' + object.id, true); - }; - - var _updateTree = function(tree, data) { - data[0].state = data[0].state || { opened: true }; - tree.settings.core.data = data; - tree.refresh(-1); - }; - - var _generateCompositeTreeNode = function(composite, compositeId, isRoot) { - var children = [], - node = { - id: 'composite_' + composite.id, - data: { - compositeId: compositeId, - }, - type: 'composite', - text: (composite.label ? composite.label : 'Composite') + ' ' + composite.id, - 'li_attr': { - 'class': 'jstree-node-type-composite' - } - }; - - var childNode = _generateCompositesTreeNode(composite.composites, composite.id); - childNode.id = 'composites_' + composite.id; - children.push(childNode); - - if (isRoot) - return childNode.children; - - childNode = _generateBodiesTreeNode(composite.bodies, composite.id); - childNode.id = 'bodies_' + composite.id; - children.push(childNode); - - childNode = _generateConstraintsTreeNode(composite.constraints, composite.id); - childNode.id = 'constraints_' + composite.id; - children.push(childNode); - - node.children = children; - - return node; - }; - - var _generateCompositesTreeNode = function(composites, compositeId) { - var node = { - type: 'composites', - text: 'Composites', - data: { - compositeId: compositeId, - }, - children: [], - 'li_attr': { - 'class': 'jstree-node-type-composites' - } - }; - - for (var i = 0; i < composites.length; i++) { - var composite = composites[i]; - node.children.push(_generateCompositeTreeNode(composite, compositeId)); - } - - return node; - }; - - var _generateBodiesTreeNode = function(bodies, compositeId) { - var node = { - type: 'bodies', - text: 'Bodies', - data: { - compositeId: compositeId, - }, - children: [], - 'li_attr': { - 'class': 'jstree-node-type-bodies' - } - }; - - for (var i = 0; i < bodies.length; i++) { - var body = bodies[i]; - node.children.push({ - type: 'body', - id: 'body_' + body.id, - data: { - compositeId: compositeId, - }, - text: (body.label ? body.label : 'Body') + ' ' + body.id, - 'li_attr': { - 'class': 'jstree-node-type-body' - } - }); - } - - return node; - }; - - var _generateConstraintsTreeNode = function(constraints, compositeId) { - var node = { - type: 'constraints', - text: 'Constraints', - data: { - compositeId: compositeId, - }, - children: [], - 'li_attr': { - 'class': 'jstree-node-type-constraints' - } - }; - - for (var i = 0; i < constraints.length; i++) { - var constraint = constraints[i]; - node.children.push({ - type: 'constraint', - id: 'constraint_' + constraint.id, - data: { - compositeId: compositeId, - }, - text: (constraint.label ? constraint.label : 'Constraint') + ' ' + constraint.id, - 'li_attr': { - 'class': 'jstree-node-type-constraint' - } - }); - } - - return node; - }; - - var _addNewComposite = function(inspector) { - var newComposite = Composite.create(); - - Composite.add(inspector.root, newComposite); - - // move new composite to the start so that it appears top of tree - inspector.root.composites.splice(inspector.root.composites.length - 1, 1); - inspector.root.composites.unshift(newComposite); - - Composite.setModified(inspector.engine.world, true, true, false); - }; - - var _exportFile = function(inspector) { - var engine = inspector.engine, - toExport = []; - - if (inspector.selected.length === 0) { - alert('No objects were selected, so export could not be created. Can only export objects that are in the World composite.'); - return; - } - - var fileName = 'export-objects', - exportComposite = Composite.create({ - label: 'Exported Objects' - }); - - // add everything else, must be in top-down order - for (var i = 0; i < inspector.selected.length; i++) { - var object = inspector.selected[i].data; - - // skip if it's already in the composite tree - // this means orphans will be added in the root - if (Composite.get(exportComposite, object.id, object.type)) - continue; - - Composite.add(exportComposite, object); - - // better filename for small exports - if (inspector.selected.length === 1) - fileName = 'export-' + object.label + '-' + object.id; - } - - // santise filename - fileName = fileName.toLowerCase().replace(/[^\w\-]/g, '') + '.json'; - - // serialise - var json = Gui.serialise(inspector.serializer, exportComposite, inspector.exportIndent); - - // launch export download - if (_isWebkit) { - var blob = new Blob([json], { type: 'application/json' }), - anchor = document.createElement('a'); - anchor.download = fileName; - anchor.href = (window.webkitURL || window.URL).createObjectURL(blob); - anchor.dataset.downloadurl = ['application/json', anchor.download, anchor.href].join(':'); - anchor.click(); - } else { - window.open('data:application/json;charset=utf-8,' + escape(json)); - } - - Events.trigger(inspector, 'export'); - }; - - var _importFile = function(inspector) { - var engine = inspector.engine, - element = document.createElement('div'), - fileInput; - - element.innerHTML = ''; - fileInput = element.firstChild; - - fileInput.addEventListener('change', function(e) { - var file = fileInput.files[0]; - - if (file.name.match(/\.(txt|json)$/)) { - var reader = new FileReader(); - - reader.onload = function(e) { - var importedComposite = inspector.serializer.parse(reader.result); - - if (importedComposite) { - importedComposite.label = 'Imported Objects'; - - Composite.rebase(importedComposite); - Composite.add(inspector.root, importedComposite); - - // move imported composite to the start so that it appears top of tree - inspector.root.composites.splice(inspector.root.composites.length - 1, 1); - inspector.root.composites.unshift(importedComposite); - - var worldTree = inspector.controls.worldTree.data('jstree'), - data = _generateCompositeTreeNode(inspector.root, null, true); - _updateTree(worldTree, data); - } - }; - - reader.readAsText(file); - } else { - alert('File not supported, .json or .txt JSON files only'); - } - }); - - fileInput.click(); - }; - - /* - * - * Events Documentation - * - */ - - /** - * Fired after the inspector's import button pressed - * - * @event export - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - - /** - * Fired after the inspector's export button pressed - * - * @event import - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - - /** - * Fired after the inspector user starts making a selection - * - * @event selectStart - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - - /** - * Fired after the inspector user ends making a selection - * - * @event selectEnd - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - - /** - * Fired after the inspector is paused - * - * @event pause - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - - /** - * Fired after the inspector is played - * - * @event play - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - -})(); - -; // End src/tools/Inspector.js - - // aliases World.add = Composite.add; @@ -7534,12 +6330,10 @@ Matter.Axes = Axes; Matter.Bounds = Bounds; Matter.Vector = Vector; Matter.Vertices = Vertices; -Matter.Gui = Gui; Matter.Render = Render; Matter.RenderPixi = RenderPixi; Matter.Events = Events; Matter.Query = Query; -Matter.Inspector = Inspector; // CommonJS module if (typeof exports !== 'undefined') { diff --git a/build/matter.min.js b/build/matter.min.js index b883592..fcc56b2 100644 --- a/build/matter.min.js +++ b/build/matter.min.js @@ -1,9 +1,8 @@ /** -* matter.min.js 0.7.0-edge 2014-05-01 +* matter.min.js 0.8.0-edge 2014-05-04 * http://brm.io/matter-js/ * License: MIT */ -!function(){var a={},b={};!function(){var a=1;b.create=function(a){var b={id:o.nextId(),type:"body",label:"Body",angle:0,position:{x:0,y:0},force:{x:0,y:0},torque:0,positionImpulse:{x:0,y:0},constraintImpulse:{x:0,y:0,angle:0},speed:0,angularSpeed:0,velocity:{x:0,y:0},angularVelocity:0,isStatic:!1,isSleeping:!1,motion:0,sleepThreshold:60,density:.001,restitution:0,friction:.1,frictionAir:.01,groupId:0,slop:.05,timeScale:1,render:{visible:!0,sprite:{xScale:1,yScale:1},path:"L 0 0 L 40 0 L 40 40 L 0 40",lineWidth:1.5}},d=o.extend(b,a);return c(d),d},b.nextGroupId=function(){return a++};var c=function(a){a.vertices=a.vertices||z.fromPath(a.render.path),a.axes=a.axes||w.fromVertices(a.vertices),a.area=z.area(a.vertices),a.bounds=x.create(a.vertices),a.mass=a.mass||a.density*a.area,a.inverseMass=1/a.mass,a.inertia=a.inertia||z.inertia(a.vertices,a.mass),a.inverseInertia=1/a.inertia,a.positionPrev=a.positionPrev||{x:a.position.x,y:a.position.y},a.anglePrev=a.anglePrev||a.angle,a.render.fillStyle=a.render.fillStyle||(a.isStatic?"#eeeeee":o.choose(["#556270","#4ECDC4","#C7F464","#FF6B6B","#C44D58"])),a.render.strokeStyle=a.render.strokeStyle||o.shadeColor(a.render.fillStyle,-20),z.create(a.vertices,a);var c=z.centre(a.vertices);z.translate(a.vertices,a.position),z.translate(a.vertices,c,-1),z.rotate(a.vertices,a.angle,a.position),w.rotate(a.axes,a.angle),x.update(a.bounds,a.vertices,a.velocity),b.setStatic(a,a.isStatic),t.set(a,a.isSleeping)};b.setStatic=function(a,b){a.isStatic=b,b&&(a.restitution=0,a.friction=1,a.mass=a.inertia=a.density=1/0,a.inverseMass=a.inverseInertia=0,a.render.lineWidth=1,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.anglePrev=a.angle,a.angularVelocity=0,a.speed=0,a.angularSpeed=0,a.motion=0)},b.resetForcesAll=function(a){for(var b=0;be.max.x||g.bounds.max.ye.max.y||b.update(g,c,d)}},b.update=function(a,b,c){var d=b*b*a.timeScale,e=1-a.frictionAir,f=a.position.x-a.positionPrev.x,g=a.position.y-a.positionPrev.y;a.velocity.x=f*e*c+a.force.x/a.mass*d,a.velocity.y=g*e*c+a.force.y/a.mass*d,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.position.x+=a.velocity.x,a.position.y+=a.velocity.y,a.angularVelocity=(a.angle-a.anglePrev)*e*c+a.torque/a.inertia*d,a.anglePrev=a.angle,a.angle+=a.angularVelocity,a.speed=y.magnitude(a.velocity),a.angularSpeed=Math.abs(a.angularVelocity),z.translate(a.vertices,a.velocity),0!==a.angularVelocity&&(z.rotate(a.vertices,a.angularVelocity,a.position),w.rotate(a.axes,a.angularVelocity)),x.update(a.bounds,a.vertices,a.velocity)},b.applyForce=function(a,b,c){a.force.x+=c.x,a.force.y+=c.y;var d={x:b.x-a.position.x,y:b.y-a.position.y};a.torque+=(d.x*c.y-d.y*c.x)*a.inverseInertia},b.translate=function(a,b){a.positionPrev.x+=b.x,a.positionPrev.y+=b.y,a.position.x+=b.x,a.position.y+=b.y,z.translate(a.vertices,b),x.update(a.bounds,a.vertices,a.velocity)},b.rotate=function(a,b){a.anglePrev+=b,a.angle+=b,z.rotate(a.vertices,b,a.position),w.rotate(a.axes,b),x.update(a.bounds,a.vertices,a.velocity)},b.scale=function(a,b,c,d){z.scale(a.vertices,b,c,d),a.axes=w.fromVertices(a.vertices),a.area=z.area(a.vertices),a.mass=a.density*a.area,a.inverseMass=1/a.mass,z.translate(a.vertices,{x:-a.position.x,y:-a.position.y}),a.inertia=z.inertia(a.vertices,a.mass),a.inverseInertia=1/a.inertia,z.translate(a.vertices,{x:a.position.x,y:a.position.y}),x.update(a.bounds,a.vertices,a.velocity)}}();var c={};!function(){c.create=function(a){return o.extend({id:o.nextId(),type:"composite",parent:null,isModified:!1,bodies:[],constraints:[],composites:[],label:"Composite"},a)},c.setModified=function(a,b,d,e){if(a.isModified=b,d&&a.parent&&c.setModified(a.parent,b,d,e),e)for(var f=0;fq.bounds.width||u.bounds.max.y<0||u.bounds.min.y>q.bounds.height)){var v=b(c,u);if(!u.region||v.id!==u.region.id||k){s.broadphaseTests+=1,(!u.region||k)&&(u.region=v);var w=a(v,u.region);for(m=w.startCol;m<=w.endCol;m++)for(n=w.startRow;n<=w.endRow;n++){p=d(m,n),o=r[p];var x=m>=v.startCol&&m<=v.endCol&&n>=v.startRow&&n<=v.endRow,y=m>=u.region.startCol&&m<=u.region.endCol&&n>=u.region.startRow&&n<=u.region.endRow;!x&&y&&y&&o&&i(c,o,u),(u.region===v||x&&!y||k)&&(o||(o=e(r,p)),f(c,o,u))}u.region=v,t=!0}}}t&&(c.pairsList=j(c))},g.clear=function(a){a.buckets={},a.pairs={},a.pairsList=[]};var a=function(a,b){var d=Math.min(a.startCol,b.startCol),e=Math.max(a.endCol,b.endCol),f=Math.min(a.startRow,b.startRow),g=Math.max(a.endRow,b.endRow);return c(d,e,f,g)},b=function(a,b){var d=b.bounds,e=Math.floor(d.min.x/a.bucketWidth),f=Math.floor(d.max.x/a.bucketWidth),g=Math.floor(d.min.y/a.bucketHeight),h=Math.floor(d.max.y/a.bucketHeight);return c(e,f,g,h)},c=function(a,b,c,d){return{id:a+","+b+","+c+","+d,startCol:a,endCol:b,startRow:c,endRow:d}},d=function(a,b){return a+","+b},e=function(a,b){var c=a[b]=[];return c},f=function(a,b,c){for(var d=0;d0?d.push(c):delete a.pairs[b[e]];return d}}();var h={};!function(){h.create=function(a,b){var c=a.bodyA,d=a.bodyB,e={id:h.id(c,d),bodyA:c,bodyB:d,contacts:{},activeContacts:[],separation:0,isActive:!0,timeCreated:b,timeUpdated:b,inverseMass:c.inverseMass+d.inverseMass,friction:Math.min(c.friction,d.friction),restitution:Math.max(c.restitution,d.restitution),slop:Math.max(c.slop,d.slop)};return h.update(e,a,b),e},h.update=function(a,b,c){var d=a.contacts,f=b.supports,g=a.activeContacts;if(a.collision=b,g.length=0,b.collided){for(var i=0;ia&&j.push(g);for(g=0;gB*e.friction&&(C=B*e.friction*z);var D=y.cross(q,i),E=y.cross(r,i),F=l/(e.inverseMass+g.inverseInertia*D*D+h.inverseInertia*E*E);if(A*=F,C*=F,0>v&&v*v>a)n.normalImpulse=0,n.tangentImpulse=0;else{var G=n.normalImpulse;n.normalImpulse=Math.min(n.normalImpulse+A,0),A=n.normalImpulse-G;var H=n.tangentImpulse;n.tangentImpulse=o.clamp(n.tangentImpulse+C,-x,x),C=n.tangentImpulse-H}c.x=i.x*A+j.x*C,c.y=i.y*A+j.y*C,g.isStatic||g.isSleeping||(g.positionPrev.x+=c.x*g.inverseMass,g.positionPrev.y+=c.y*g.inverseMass,g.anglePrev+=y.cross(q,c)*g.inverseInertia),h.isStatic||h.isSleeping||(h.positionPrev.x-=c.x*h.inverseMass,h.positionPrev.y-=c.y*h.inverseMass,h.anglePrev-=y.cross(r,c)*h.inverseInertia)}}}}}();var l={};!function(){l.collides=function(b,d,e){var f,g,h,i,j=e,k=!1;if(j){var l=b.speed*b.speed+b.angularSpeed*b.angularSpeed+d.speed*d.speed+d.angularSpeed*d.angularSpeed;k=j&&j.collided&&.2>l,i=j}else i={collided:!1,bodyA:b,bodyB:d};if(j&&k){var m=[j.bodyA.axes[j.axisNumber]];if(h=a(j.bodyA.vertices,j.bodyB.vertices,m),i.reused=!0,h.overlap<=0)return i.collided=!1,i}else{if(f=a(b.vertices,d.vertices,b.axes),f.overlap<=0)return i.collided=!1,i;if(g=a(d.vertices,b.vertices,d.axes),g.overlap<=0)return i.collided=!1,i;f.overlap0&&(i.normal=y.neg(i.normal)),i.tangent=y.perp(i.normal),i.penetration={x:i.normal.x*i.depth,y:i.normal.y*i.depth};var n=c(b,d,i.normal),o=[n[0]];if(z.contains(b.vertices,n[1]))o.push(n[1]);else{var p=c(d,b,y.neg(i.normal));z.contains(d.vertices,p[0])&&o.push(p[0]),o.length<2&&z.contains(d.vertices,p[1])&&o.push(p[1])}return i.supports=o,i.supportCorrected=y.sub(n[0],i.penetration),i};var a=function(a,c,d){for(var e,f,g={},h={},i={overlap:Number.MAX_VALUE},j=0;j=e)return i.overlap=e,i;ee?e=g:d>g&&(d=g)}a.min=d,a.max=e},c=function(a,b,c){for(var d,e,f=Number.MAX_VALUE,g={x:0,y:0},h=b.vertices,i=a.position,j=h[0],k=h[1],l=0;ld&&(f=d,j=e);var m=j.index-1>=0?j.index-1:h.length-1;e=h[m],g.x=e.x-i.x,g.y=e.y-i.y,f=-y.dot(c,g),k=e;var n=(j.index+1)%h.length;return e=h[n],g.x=e.x-i.x,g.y=e.y-i.y,d=-y.dot(c,g),f>d&&(f=d,k=e),[j,k]}}();var m={};!function(){var a=1e-6,b=.001;m.create=function(b){var c=b;c.bodyA&&!c.pointA&&(c.pointA={x:0,y:0}),c.bodyB&&!c.pointB&&(c.pointB={x:0,y:0});var d=c.bodyA?y.add(c.bodyA.position,c.pointA):c.pointA,e=c.bodyB?y.add(c.bodyB.position,c.pointB):c.pointB,f=y.magnitude(y.sub(d,e));c.length=c.length||f||a;var g={visible:!0,lineWidth:2,strokeStyle:"#666"};return c.render=o.extend(g,c.render),c.id=c.id||o.nextId(),c.label=c.label||"Constraint",c.type="constraint",c.stiffness=c.stiffness||1,c.angularStiffness=c.angularStiffness||0,c.angleA=c.bodyA?c.bodyA.angle:c.angleA,c.angleB=c.bodyB?c.bodyB.angle:c.angleB,c},m.solveAll=function(a,b){for(var c=0;c0&&(B=0);var C,D={x:n.x*B,y:n.y*B};e&&!e.isStatic&&(C=y.cross(s,D)*e.inverseInertia*(1-c.angularStiffness),t.set(e,!1),C=o.clamp(C,-.01,.01),e.constraintImpulse.x-=p.x,e.constraintImpulse.y-=p.y,e.constraintImpulse.angle+=C,e.position.x-=p.x,e.position.y-=p.y,e.angle+=C),f&&!f.isStatic&&(C=y.cross(u,D)*f.inverseInertia*(1-c.angularStiffness),t.set(f,!1),C=o.clamp(C,-.01,.01),f.constraintImpulse.x+=p.x,f.constraintImpulse.y+=p.y,f.constraintImpulse.angle-=C,f.position.x+=p.x,f.position.y+=p.y,f.angle-=C)}}},m.postSolveAll=function(a){for(var b=0;b>16)+d,f=(c>>8&255)+d,g=(255&c)+d;return"#"+(16777216+65536*(255>e?1>e?0:e:255)+256*(255>f?1>f?0:f:255)+(255>g?1>g?0:g:255)).toString(16).slice(1)},o.shuffle=function(a){for(var b=a.length-1;b>0;b--){var c=Math.floor(Math.random()*(b+1)),d=a[b];a[b]=a[c],a[c]=d}return a},o.choose=function(a){return a[Math.floor(Math.random()*a.length)]},o.isElement=function(a){try{return a instanceof HTMLElement}catch(b){return"object"==typeof a&&1===a.nodeType&&"object"==typeof a.style&&"object"==typeof a.ownerDocument}},o.clamp=function(a,b,c){return b>a?b:a>c?c:a},o.sign=function(a){return 0>a?-1:1},o.now=function(){var a=window.performance;return a?(a.now=a.now||a.webkitNow||a.msNow||a.oNow||a.mozNow,+a.now()):+new Date},o.random=function(a,b){return a+Math.random()*(b-a)},o.colorToNumber=function(a){return a=a.replace("#",""),3==a.length&&(a=a.charAt(0)+a.charAt(0)+a.charAt(1)+a.charAt(1)+a.charAt(2)+a.charAt(2)),parseInt(a,16)},o.log=function(a,b){if(console&&console.log){var c;switch(b){case"warn":c="color: coral";break;case"error":c="color: red"}console.log("%c [Matter] "+b+": "+a,c)}},o.nextId=function(){return o._nextId++}}();var p={};!function(){var a=60,e=a,h=1e3/a,j=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(function(){a(o.now())},h)};p.create=function(b,c){c=o.isElement(b)?c:b,b=o.isElement(b)?b:null;var e={enabled:!0,positionIterations:6,velocityIterations:4,constraintIterations:2,enableSleeping:!1,timeScale:1,input:{},events:[],timing:{fps:a,timestamp:0,delta:h,correction:1,deltaMin:1e3/a,deltaMax:1e3/(.5*a),timeScale:1},render:{element:b,controller:A}},j=o.extend(e,c);return j.render=j.render.controller.create(j.render),j.world=d.create(j.world),j.pairs=i.create(),j.metrics=j.metrics||r.create(),j.input.mouse=j.input.mouse||s.create(j.render.canvas),j.broadphase=j.broadphase||{current:"grid",grid:{controller:g,instance:g.create(),detector:f.collisions},bruteForce:{detector:f.bruteForce}},j},p.run=function(a){var b,c,d=a.timing,f=0,g=0,i=[],k=1;!function m(o){if(j(m),a.enabled){o=o||0;var r={timestamp:o};q.trigger(a,"beforeTick",r),b=o-d.timestamp||h,i.push(b),i=i.slice(-e),b=Math.min.apply(null,i),b=bd.deltaMax?d.deltaMax:b,c=b/d.delta,0!==k&&(c*=d.timeScale/k),0===d.timeScale&&(c=0),k=d.timeScale,d.timestamp=o,d.correction=c,d.delta=b,g+=1,o-f>=1e3&&(d.fps=g*((o-f)/1e3),f=o,g=0),q.trigger(a,"tick beforeUpdate",r),a.world.isModified&&a.render.controller.clear(a.render),p.update(a,b,c),n(a),l(a),q.trigger(a,"afterUpdate beforeRender",r),a.render.options.enabled&&a.render.controller.world(a),q.trigger(a,"afterTick afterRender",r)}}()},p.update=function(a,d,e){var f,g=a.world,h=a.timing,j=a.broadphase[a.broadphase.current],l=[],n=c.allBodies(g),o=c.allConstraints(g);for(r.reset(a.metrics),a.enableSleeping&&t.update(n),b.applyGravityAll(n,g.gravity),b.updateAll(n,d*h.timeScale,e,g.bounds),f=0;f0&&q.trigger(a,"collisionStart",{pairs:b.collisionStart}),b.collisionActive.length>0&&q.trigger(a,"collisionActive",{pairs:b.collisionActive}),b.collisionEnd.length>0&&q.trigger(a,"collisionEnd",{pairs:b.collisionEnd})}}();var q={};!function(){q.on=function(a,b,c){for(var d,e=b.split(" "),f=0;f0||e.force.y>0)t.set(e,!1);else{var g=Math.min(e.motion,f),h=Math.max(e.motion,f);e.motion=c*g+(1-c)*h,e.sleepThreshold>0&&e.motion=e.sleepThreshold&&t.set(e,!0)):e.sleepCounter>0&&(e.sleepCounter-=1)}}},t.afterCollisions=function(b){for(var c=0;ca&&t.set(h,!1)}}}},t.set=function(a,b){b?(a.isSleeping=!0,a.sleepCounter=a.sleepThreshold,a.positionImpulse.x=0,a.positionImpulse.y=0,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.anglePrev=a.angle,a.speed=0,a.angularSpeed=0,a.motion=0):(a.isSleeping=!1,a.sleepCounter=0)}}();var u={};!function(){u.rectangle=function(a,c,d,e,f){f=f||{};var g={label:"Rectangle Body",position:{x:a,y:c},render:{path:"L 0 0 L "+d+" 0 L "+d+" "+e+" L 0 "+e}};return b.create(o.extend({},g,f))},u.trapezoid=function(a,c,d,e,f,g){g=g||{},f*=.5;var h=(1-2*f)*d,i=d*f,j=i+h,k=j+i,l={label:"Trapezoid Body",position:{x:a,y:c},render:{path:"L 0 0 L "+i+" "+-e+" L "+j+" "+-e+" L "+k+" 0"}};return b.create(o.extend({},l,g))},u.circle=function(a,b,c,d,e){d=d||{},d.label="Circle Body",e=e||25;var f=Math.ceil(Math.max(10,Math.min(e,c)));return f%2===1&&(f+=1),d.circleRadius=c,u.polygon(a,b,f,c,d)},u.polygon=function(a,c,d,e,f){if(f=f||{},3>d)return u.circle(a,c,e,f);for(var g=2*Math.PI/d,h="",i=.5*g,j=0;d>j;j+=1){var k=i+j*g,l=Math.cos(k)*e,m=Math.sin(k)*e;h+="L "+l.toFixed(3)+" "+m.toFixed(3)+" "}var n={label:"Polygon Body",position:{x:a,y:c},render:{path:h}};return b.create(o.extend({},n,f))}}();var v={};!function(){v.stack=function(a,d,e,f,g,h,i){for(var j,k=c.create({label:"Stack"}),l=a,m=d,n=0,o=0;f>o;o++){for(var p=0,q=0;e>q;q++){var r=i(l,m,q,o,j,n);if(r){var s=r.bounds.max.y-r.bounds.min.y,t=r.bounds.max.x-r.bounds.min.x;s>p&&(p=s),b.translate(r,{x:.5*t,y:.5*s}),l=r.bounds.max.x+g,c.addBody(k,r),j=r,n+=1}}m+=p+h,l=a}return k},v.chain=function(a,b,d,e,f,g){for(var h=a.bodies,i=1;ig;g++){for(h=0;b>h;h++)h>0&&(i=l[h-1+g*b],j=l[h+g*b],c.addConstraint(a,m.create(o.extend({bodyA:i,bodyB:j},f))));for(h=0;b>h;h++)g>0&&(i=l[h+(g-1)*b],j=l[h+g*b],c.addConstraint(a,m.create(o.extend({bodyA:i,bodyB:j},f))),e&&h>0&&(k=l[h-1+(g-1)*b],c.addConstraint(a,m.create(o.extend({bodyA:k,bodyB:j},f)))),e&&b-1>h&&(k=l[h+1+(g-1)*b],c.addConstraint(a,m.create(o.extend({bodyA:k,bodyB:j},f)))))}return a.label+=" Mesh",a},v.pyramid=function(a,c,d,e,f,g,h){return v.stack(a,c,d,e,f,g,function(c,g,i,j,k,l){var m=Math.min(e,Math.ceil(d/2)),n=k?k.bounds.max.x-k.bounds.min.x:0;if(!(j>m)){j=m-j;var o=j,p=d-1-j;if(!(o>i||i>p)){1===l&&b.translate(k,{x:(i+(d%2===1?1:-1))*n,y:0});var q=k?i*n:0;return h(a+q+i*f,g,i,j,k,l)}}})},v.newtonsCradle=function(a,b,d,e,f){for(var g=c.create({label:"Newtons Cradle"}),h=0;d>h;h++){var i=1.9,j=u.circle(a+h*e*i,b+f,e,{inertia:99999,restitution:1,friction:0,frictionAir:1e-4,slop:.01}),k=m.create({pointA:{x:a+h*e*i,y:b},bodyB:j});c.addBody(g,j),c.addConstraint(g,k)}return g},v.car=function(a,d,e,f,g){var h=b.nextGroupId(),i=-20,j=.5*-e+i,k=.5*e-i,l=0,n=c.create({label:"Car"}),o=u.trapezoid(a,d,e,f,.3,{groupId:h,friction:.01}),p=u.circle(a+j,d+l,g,{groupId:h,restitution:.5,friction:.9,density:.01}),q=u.circle(a+k,d+l,g,{groupId:h,restitution:.5,friction:.9,density:.01}),r=m.create({bodyA:o,pointA:{x:j,y:l},bodyB:p,stiffness:.5}),s=m.create({bodyA:o,pointA:{x:k,y:l},bodyB:q,stiffness:.5});return c.addBody(n,o),c.addBody(n,p),c.addBody(n,q),c.addConstraint(n,r),c.addConstraint(n,s),n},v.softBody=function(a,b,c,d,e,f,g,h,i,j){i=o.extend({inertia:1/0},i),j=o.extend({stiffness:.4},j);var k=v.stack(a,b,c,d,e,f,function(a,b){return u.circle(a,b,h,i)});return v.mesh(k,c,d,g,j),k.label="Soft Body",k}}();var w={};!function(){w.fromVertices=function(a){for(var b={},c=0;ca.max.x&&(a.max.x=e.x),e.xa.max.y&&(a.max.y=e.y),e.y0?a.max.x+=c.x:a.min.x+=c.x,c.y>0?a.max.y+=c.y:a.min.y+=c.y)},x.contains=function(a,b){return b.x>=a.min.x&&b.x<=a.max.x&&b.y>=a.min.y&&b.y<=a.max.y},x.overlaps=function(a,b){return a.min.x<=b.max.x&&a.max.x>=b.min.x&&a.max.y>=b.min.y&&a.min.y<=b.max.y},x.translate=function(a,b){a.min.x+=b.x,a.max.x+=b.x,a.min.y+=b.y,a.max.y+=b.y},x.shift=function(a,b){var c=a.max.x-a.min.x,d=a.max.y-a.min.y;a.min.x=b.x,a.max.x=b.x+c,a.min.y=b.y,a.max.y=b.y+d}}();var y={};!function(){y.magnitude=function(a){return Math.sqrt(a.x*a.x+a.y*a.y)},y.magnitudeSquared=function(a){return a.x*a.x+a.y*a.y},y.rotate=function(a,b){var c=Math.cos(b),d=Math.sin(b);return{x:a.x*c-a.y*d,y:a.x*d+a.y*c}},y.rotateAbout=function(a,b,c){var d=Math.cos(b),e=Math.sin(b);return{x:c.x+((a.x-c.x)*d-(a.y-c.y)*e),y:c.y+((a.x-c.x)*e+(a.y-c.y)*d)}},y.normalise=function(a){var b=y.magnitude(a);return 0===b?{x:0,y:0}:{x:a.x/b,y:a.y/b}},y.dot=function(a,b){return a.x*b.x+a.y*b.y},y.cross=function(a,b){return a.x*b.y-a.y*b.x},y.add=function(a,b){return{x:a.x+b.x,y:a.y+b.y}},y.sub=function(a,b){return{x:a.x-b.x,y:a.y-b.y}},y.mult=function(a,b){return{x:a.x*b,y:a.y*b}},y.div=function(a,b){return{x:a.x/b,y:a.y/b}},y.perp=function(a,b){return b=b===!0?-1:1,{x:b*-a.y,y:b*a.x}},y.neg=function(a){return{x:-a.x,y:-a.y}},y.angle=function(a,b){return Math.atan2(b.y-a.y,b.x-a.x)}}();var z={};!function(){z.create=function(a,b){for(var c=0;c0)return!1}return!0},z.scale=function(a,b,c,d){if(1===b&&1===c)return a;d=d||z.centre(a);for(var e,f,g=0;g=500){var k="";k+="fps: "+Math.round(a.timing.fps)+j,a.metrics.extended&&(k+="delta: "+a.timing.delta.toFixed(3)+j,k+="correction: "+a.timing.correction.toFixed(3)+j,k+="bodies: "+i.length+j,a.broadphase.controller===g&&(k+="buckets: "+a.metrics.buckets+j),k+="\n",k+="collisions: "+a.metrics.collisions+j,k+="pairs: "+a.pairs.list.length+j,k+="broad: "+a.metrics.broadEff+j,k+="mid: "+a.metrics.midEff+j,k+="narrow: "+a.metrics.narrowEff+j),f.debugString=k,f.debugTimestamp=a.timing.timestamp}if(f.debugString){d.font="12px Arial",d.fillStyle=h.wireframes?"rgba(255,255,255,0.5)":"rgba(0,0,0,0.5)";for(var l=f.debugString.split("\n"),m=0;m0){var l=d.activeContacts[0].vertex.x,m=d.activeContacts[0].vertex.y;2===d.activeContacts.length&&(l=(d.activeContacts[0].vertex.x+d.activeContacts[1].vertex.x)/2,m=(d.activeContacts[0].vertex.y+d.activeContacts[1].vertex.y)/2),h.moveTo(l-8*e.normal.x,m-8*e.normal.y),h.lineTo(l,m)}h.strokeStyle=i.wireframes?"rgba(255,165,0,0.7)":"orange",h.lineWidth=1,h.stroke()},A.grid=function(a,b,c){var d=c,e=a.render.options;d.strokeStyle=e.wireframes?"rgba(255,180,0,0.1)":"rgba(255,180,0,0.5)",d.beginPath();for(var f=o.keys(b.buckets),g=0;g'),e=$('
'),f=$(''),h=$(''),i=$(''),j=$(''),k=$(''),l=$('');e.append(j,h,i,k),c.prepend(e,f,l),d.prepend(c),b.pauseButton=j,b.importButton=h,b.exportButton=i,b.helpButton=k,b.searchBox=f,b.container=c,b.addCompositeButton=l,b.pauseButton.click(function(){v(a,!a.isPaused)}),b.exportButton.click(function(){J(a)}),b.importButton.click(function(){K(a)}),b.helpButton.click(function(){g(a)}),b.addCompositeButton.click(function(){I(a)});var m;b.searchBox.keyup(function(){clearTimeout(m),m=setTimeout(function(){var a=b.searchBox.val(),c=b.worldTree.data("jstree");c.search(a)},250)})},g=function(){var a="Matter Tools\n\n";a+="Drag nodes in the tree to move them between composites.\n",a+="Use browser's developer console to inspect selected objects.\n",a+="Note: selections only render if renderer supports it.\n\n",a+="[shift + space] pause or play simulation.\n",a+="[right click] and drag on empty space to select a region.\n",a+="[right click] and drag on an object to move it.\n",a+="[right click + shift] and drag to move whole selection.\n",a+="[del] or [backspace] delete selected objects.\n\n",a+="[shift + s] scale-xy selected objects with mouse or arrows.\n",a+="[shift + s + d] scale-x selected objects with mouse or arrows.\n",a+="[shift + s + f] scale-y selected objects with mouse or arrows.\n",a+="[shift + r] rotate selected objects with mouse or arrows.\n\n",a+="[shift + q] set selected objects as static (can't be undone).\n",a+="[shift + i] import objects.\n",a+="[shift + o] export selected objects.\n",a+="[shift + h] toggle Matter.Gui.\n",a+="[shift + y] toggle auto-hide.\n",a+="[shift + r] toggle auto-rewind on play/pause.\n\n",a+="[shift + j] show this help message.",alert(a)},h=function(c){c.engine,c.controls;a("shift+space",function(){v(c,!c.isPaused)}),a("shift+o",function(){J(c)}),a("shift+i",function(){K(c)}),a("shift+j",function(){g(c)}),a("shift+y",function(){c.autoHide=!c.autoHide,d.toggleClass("ins-auto-hide gui-auto-hide",c.autoHide)}),a("shift+r",function(){c.autoRewind=!c.autoRewind,c.autoRewind||localStorage.removeItem("pauseState")}),a("shift+q",function(){for(var a=(c.controls.worldTree.data("jstree"),0);a').jstree(f),e.container.prepend(e.worldTree),e.worldTree.on("changed.jstree",function(f,g){var h=[],i=e.worldTree.data("jstree");"select_node"===g.action&&(clearTimeout(b),b=setTimeout(function(){g.selected=i.get_selected();for(var b=0;bC||100>D){l?A(b,s):w(b,[s]),n=!0;break}}}}n?(b.selectStart=null,b.selectEnd=null):(m.deselect_all(!0),w(b,[]),b.selectStart=o.clone(f),b.selectEnd=o.clone(f),x.update(b.selectBounds,[b.selectStart,b.selectEnd]),q.trigger(b,"selectStart"))}}2===e.button&&b.selected.length>0&&(k(b,"ins-cursor-move"),r(b))}),q.on(d,"afterRender",function(){var a=d.render.controller,c=d.render.context;a.inspector&&a.inspector(b,c)})},p=function(a){var b,d,e=[],f=a.controls.worldTree.data("jstree");for(d=0;dd?console.log(c.label+" "+c.id+": %O",c):6===d&&console.warn("Omitted inspecting "+(b.length-5)+" more objects"))},A=function(a,b){if(b){var c=a.controls.worldTree.data("jstree");a.selected.push({data:b}),c.select_node(b.type+"_"+b.id,!0)}},B=function(a,b){b[0].state=b[0].state||{opened:!0},a.settings.core.data=b,a.refresh(-1)},E=function(a,b,c){var d=[],e={id:"composite_"+a.id,data:{compositeId:b},type:"composite",text:(a.label?a.label:"Composite")+" "+a.id,li_attr:{"class":"jstree-node-type-composite"}},f=F(a.composites,a.id);return f.id="composites_"+a.id,d.push(f),c?f.children:(f=G(a.bodies,a.id),f.id="bodies_"+a.id,d.push(f),f=H(a.constraints,a.id),f.id="constraints_"+a.id,d.push(f),e.children=d,e)},F=function(a,b){for(var c={type:"composites",text:"Composites",data:{compositeId:b},children:[],li_attr:{"class":"jstree-node-type-composites"}},d=0;d',b=d.firstChild,b.addEventListener("change",function(){var d=b.files[0];if(d.name.match(/\.(txt|json)$/)){var e=new FileReader;e.onload=function(){var b=a.serializer.parse(e.result);if(b){b.label="Imported Objects",c.rebase(b),c.add(a.root,b),a.root.composites.splice(a.root.composites.length-1,1),a.root.composites.unshift(b);var d=a.controls.worldTree.data("jstree"),f=E(a.root,null,!0);B(d,f)}},e.readAsText(d)}else alert("File not supported, .json or .txt JSON files only")}),b.click()}}(),d.add=c.add,d.remove=c.remove,d.addComposite=c.addComposite,d.addBody=c.addBody,d.addConstraint=c.addConstraint,d.clear=c.clear,a.Body=b,a.Composite=c,a.World=d,a.Contact=e,a.Detector=f,a.Grid=g,a.Pairs=i,a.Pair=h,a.Resolver=k,a.SAT=l,a.Constraint=m,a.MouseConstraint=n,a.Common=o,a.Engine=p,a.Metrics=r,a.Mouse=s,a.Sleeping=t,a.Bodies=u,a.Composites=v,a.Axes=w,a.Bounds=x,a.Vector=y,a.Vertices=z,a.Gui=C,a.Render=A,a.RenderPixi=B,a.Events=q,a.Query=j,a.Inspector=D,"undefined"!=typeof exports&&("undefined"!=typeof module&&module.exports&&(exports=module.exports=a),exports.Matter=a),"function"==typeof define&&define.amd&&define("Matter",[],function(){return a}),"object"==typeof window&&"object"==typeof window.document&&(window.Matter=a)}(); \ No newline at end of file +!function(){var a={},b={};!function(){var a=1;b.create=function(a){var b={id:o.nextId(),type:"body",label:"Body",angle:0,vertices:z.fromPath("L 0 0 L 40 0 L 40 40 L 0 40"),position:{x:0,y:0},force:{x:0,y:0},torque:0,positionImpulse:{x:0,y:0},constraintImpulse:{x:0,y:0,angle:0},speed:0,angularSpeed:0,velocity:{x:0,y:0},angularVelocity:0,isStatic:!1,isSleeping:!1,motion:0,sleepThreshold:60,density:.001,restitution:0,friction:.1,frictionAir:.01,groupId:0,slop:.05,timeScale:1,render:{visible:!0,sprite:{xScale:1,yScale:1},lineWidth:1.5}},d=o.extend(b,a);return c(d),d},b.nextGroupId=function(){return a++};var c=function(a){a.axes=a.axes||w.fromVertices(a.vertices),a.area=z.area(a.vertices),a.bounds=x.create(a.vertices),a.mass=a.mass||a.density*a.area,a.inverseMass=1/a.mass,a.inertia=a.inertia||z.inertia(a.vertices,a.mass),a.inverseInertia=1/a.inertia,a.positionPrev=a.positionPrev||{x:a.position.x,y:a.position.y},a.anglePrev=a.anglePrev||a.angle,a.render.fillStyle=a.render.fillStyle||(a.isStatic?"#eeeeee":o.choose(["#556270","#4ECDC4","#C7F464","#FF6B6B","#C44D58"])),a.render.strokeStyle=a.render.strokeStyle||o.shadeColor(a.render.fillStyle,-20),z.create(a.vertices,a);var c=z.centre(a.vertices);z.translate(a.vertices,a.position),z.translate(a.vertices,c,-1),z.rotate(a.vertices,a.angle,a.position),w.rotate(a.axes,a.angle),x.update(a.bounds,a.vertices,a.velocity),b.setStatic(a,a.isStatic),t.set(a,a.isSleeping)};b.setStatic=function(a,b){a.isStatic=b,b&&(a.restitution=0,a.friction=1,a.mass=a.inertia=a.density=1/0,a.inverseMass=a.inverseInertia=0,a.render.lineWidth=1,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.anglePrev=a.angle,a.angularVelocity=0,a.speed=0,a.angularSpeed=0,a.motion=0)},b.resetForcesAll=function(a){for(var b=0;bf.max.x||h.bounds.max.yf.max.y||b.update(h,c,d,e)}},b.update=function(a,b,c,d){var e=Math.pow(b*c*a.timeScale,2),f=1-a.frictionAir*c*a.timeScale,g=a.position.x-a.positionPrev.x,h=a.position.y-a.positionPrev.y;a.velocity.x=g*f*d+a.force.x/a.mass*e,a.velocity.y=h*f*d+a.force.y/a.mass*e,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.position.x+=a.velocity.x,a.position.y+=a.velocity.y,a.angularVelocity=(a.angle-a.anglePrev)*f*d+a.torque/a.inertia*e,a.anglePrev=a.angle,a.angle+=a.angularVelocity,a.speed=y.magnitude(a.velocity),a.angularSpeed=Math.abs(a.angularVelocity),z.translate(a.vertices,a.velocity),0!==a.angularVelocity&&(z.rotate(a.vertices,a.angularVelocity,a.position),w.rotate(a.axes,a.angularVelocity)),x.update(a.bounds,a.vertices,a.velocity)},b.applyForce=function(a,b,c){a.force.x+=c.x,a.force.y+=c.y;var d={x:b.x-a.position.x,y:b.y-a.position.y};a.torque+=(d.x*c.y-d.y*c.x)*a.inverseInertia},b.translate=function(a,b){a.positionPrev.x+=b.x,a.positionPrev.y+=b.y,a.position.x+=b.x,a.position.y+=b.y,z.translate(a.vertices,b),x.update(a.bounds,a.vertices,a.velocity)},b.rotate=function(a,b){a.anglePrev+=b,a.angle+=b,z.rotate(a.vertices,b,a.position),w.rotate(a.axes,b),x.update(a.bounds,a.vertices,a.velocity)},b.scale=function(a,b,c,d){z.scale(a.vertices,b,c,d),a.axes=w.fromVertices(a.vertices),a.area=z.area(a.vertices),a.mass=a.density*a.area,a.inverseMass=1/a.mass,z.translate(a.vertices,{x:-a.position.x,y:-a.position.y}),a.inertia=z.inertia(a.vertices,a.mass),a.inverseInertia=1/a.inertia,z.translate(a.vertices,{x:a.position.x,y:a.position.y}),x.update(a.bounds,a.vertices,a.velocity)}}();var c={};!function(){c.create=function(a){return o.extend({id:o.nextId(),type:"composite",parent:null,isModified:!1,bodies:[],constraints:[],composites:[],label:"Composite"},a)},c.setModified=function(a,b,d,e){if(a.isModified=b,d&&a.parent&&c.setModified(a.parent,b,d,e),e)for(var f=0;fq.bounds.width||u.bounds.max.y<0||u.bounds.min.y>q.bounds.height)){var v=b(c,u);if(!u.region||v.id!==u.region.id||k){s.broadphaseTests+=1,(!u.region||k)&&(u.region=v);var w=a(v,u.region);for(m=w.startCol;m<=w.endCol;m++)for(n=w.startRow;n<=w.endRow;n++){p=d(m,n),o=r[p];var x=m>=v.startCol&&m<=v.endCol&&n>=v.startRow&&n<=v.endRow,y=m>=u.region.startCol&&m<=u.region.endCol&&n>=u.region.startRow&&n<=u.region.endRow;!x&&y&&y&&o&&i(c,o,u),(u.region===v||x&&!y||k)&&(o||(o=e(r,p)),f(c,o,u))}u.region=v,t=!0}}}t&&(c.pairsList=j(c))},g.clear=function(a){a.buckets={},a.pairs={},a.pairsList=[]};var a=function(a,b){var d=Math.min(a.startCol,b.startCol),e=Math.max(a.endCol,b.endCol),f=Math.min(a.startRow,b.startRow),g=Math.max(a.endRow,b.endRow);return c(d,e,f,g)},b=function(a,b){var d=b.bounds,e=Math.floor(d.min.x/a.bucketWidth),f=Math.floor(d.max.x/a.bucketWidth),g=Math.floor(d.min.y/a.bucketHeight),h=Math.floor(d.max.y/a.bucketHeight);return c(e,f,g,h)},c=function(a,b,c,d){return{id:a+","+b+","+c+","+d,startCol:a,endCol:b,startRow:c,endRow:d}},d=function(a,b){return a+","+b},e=function(a,b){var c=a[b]=[];return c},f=function(a,b,c){for(var d=0;d0?d.push(c):delete a.pairs[b[e]];return d}}();var h={};!function(){h.create=function(a,b){var c=a.bodyA,d=a.bodyB,e={id:h.id(c,d),bodyA:c,bodyB:d,contacts:{},activeContacts:[],separation:0,isActive:!0,timeCreated:b,timeUpdated:b,inverseMass:c.inverseMass+d.inverseMass,friction:Math.min(c.friction,d.friction),restitution:Math.max(c.restitution,d.restitution),slop:Math.max(c.slop,d.slop)};return h.update(e,a,b),e},h.update=function(a,b,c){var d=a.contacts,f=b.supports,g=a.activeContacts;if(a.collision=b,g.length=0,b.collided){for(var i=0;ia&&j.push(g);for(g=0;gD*g.friction*e&&(E=D*g.friction*e*B);var F=y.cross(s,k),G=y.cross(t,k),H=n/(g.inverseMass+i.inverseInertia*F*F+j.inverseInertia*G*G);if(C*=H,E*=H,0>x&&x*x>a*e)q.normalImpulse=0,q.tangentImpulse=0;else{var I=q.normalImpulse;q.normalImpulse=Math.min(q.normalImpulse+C,0),C=q.normalImpulse-I;var J=q.tangentImpulse;q.tangentImpulse=o.clamp(q.tangentImpulse+E,-A,A),E=q.tangentImpulse-J}d.x=k.x*C+l.x*E,d.y=k.y*C+l.y*E,i.isStatic||i.isSleeping||(i.positionPrev.x+=d.x*i.inverseMass,i.positionPrev.y+=d.y*i.inverseMass,i.anglePrev+=y.cross(s,d)*i.inverseInertia),j.isStatic||j.isSleeping||(j.positionPrev.x-=d.x*j.inverseMass,j.positionPrev.y-=d.y*j.inverseMass,j.anglePrev-=y.cross(t,d)*j.inverseInertia)}}}}}();var l={};!function(){l.collides=function(b,d,e){var f,g,h,i,j=e,k=!1;if(j){var l=b.speed*b.speed+b.angularSpeed*b.angularSpeed+d.speed*d.speed+d.angularSpeed*d.angularSpeed;k=j&&j.collided&&.2>l,i=j}else i={collided:!1,bodyA:b,bodyB:d};if(j&&k){var m=[j.bodyA.axes[j.axisNumber]];if(h=a(j.bodyA.vertices,j.bodyB.vertices,m),i.reused=!0,h.overlap<=0)return i.collided=!1,i}else{if(f=a(b.vertices,d.vertices,b.axes),f.overlap<=0)return i.collided=!1,i;if(g=a(d.vertices,b.vertices,d.axes),g.overlap<=0)return i.collided=!1,i;f.overlap0&&(i.normal=y.neg(i.normal)),i.tangent=y.perp(i.normal),i.penetration={x:i.normal.x*i.depth,y:i.normal.y*i.depth};var n=c(b,d,i.normal),o=[n[0]];if(z.contains(b.vertices,n[1]))o.push(n[1]);else{var p=c(d,b,y.neg(i.normal));z.contains(d.vertices,p[0])&&o.push(p[0]),o.length<2&&z.contains(d.vertices,p[1])&&o.push(p[1])}return i.supports=o,i.supportCorrected=y.sub(n[0],i.penetration),i};var a=function(a,c,d){for(var e,f,g={},h={},i={overlap:Number.MAX_VALUE},j=0;j=e)return i.overlap=e,i;ee?e=g:d>g&&(d=g)}a.min=d,a.max=e},c=function(a,b,c){for(var d,e,f=Number.MAX_VALUE,g={x:0,y:0},h=b.vertices,i=a.position,j=h[0],k=h[1],l=0;ld&&(f=d,j=e);var m=j.index-1>=0?j.index-1:h.length-1;e=h[m],g.x=e.x-i.x,g.y=e.y-i.y,f=-y.dot(c,g),k=e;var n=(j.index+1)%h.length;return e=h[n],g.x=e.x-i.x,g.y=e.y-i.y,d=-y.dot(c,g),f>d&&(f=d,k=e),[j,k]}}();var m={};!function(){var a=1e-6,b=.001;m.create=function(b){var c=b;c.bodyA&&!c.pointA&&(c.pointA={x:0,y:0}),c.bodyB&&!c.pointB&&(c.pointB={x:0,y:0});var d=c.bodyA?y.add(c.bodyA.position,c.pointA):c.pointA,e=c.bodyB?y.add(c.bodyB.position,c.pointB):c.pointB,f=y.magnitude(y.sub(d,e));c.length=c.length||f||a;var g={visible:!0,lineWidth:2,strokeStyle:"#666"};return c.render=o.extend(g,c.render),c.id=c.id||o.nextId(),c.label=c.label||"Constraint",c.type="constraint",c.stiffness=c.stiffness||1,c.angularStiffness=c.angularStiffness||0,c.angleA=c.bodyA?c.bodyA.angle:c.angleA,c.angleB=c.bodyB?c.bodyB.angle:c.angleB,c},m.solveAll=function(a,b){for(var c=0;c0&&(B=0);var C,D={x:n.x*B,y:n.y*B};e&&!e.isStatic&&(C=y.cross(s,D)*e.inverseInertia*(1-c.angularStiffness),t.set(e,!1),C=o.clamp(C,-.01,.01),e.constraintImpulse.x-=p.x,e.constraintImpulse.y-=p.y,e.constraintImpulse.angle+=C,e.position.x-=p.x,e.position.y-=p.y,e.angle+=C),f&&!f.isStatic&&(C=y.cross(u,D)*f.inverseInertia*(1-c.angularStiffness),t.set(f,!1),C=o.clamp(C,-.01,.01),f.constraintImpulse.x+=p.x,f.constraintImpulse.y+=p.y,f.constraintImpulse.angle-=C,f.position.x+=p.x,f.position.y+=p.y,f.angle-=C)}}},m.postSolveAll=function(a){for(var b=0;b>16)+d,f=(c>>8&255)+d,g=(255&c)+d;return"#"+(16777216+65536*(255>e?1>e?0:e:255)+256*(255>f?1>f?0:f:255)+(255>g?1>g?0:g:255)).toString(16).slice(1)},o.shuffle=function(a){for(var b=a.length-1;b>0;b--){var c=Math.floor(Math.random()*(b+1)),d=a[b];a[b]=a[c],a[c]=d}return a},o.choose=function(a){return a[Math.floor(Math.random()*a.length)]},o.isElement=function(a){try{return a instanceof HTMLElement}catch(b){return"object"==typeof a&&1===a.nodeType&&"object"==typeof a.style&&"object"==typeof a.ownerDocument}},o.clamp=function(a,b,c){return b>a?b:a>c?c:a},o.sign=function(a){return 0>a?-1:1},o.now=function(){var a=window.performance;return a?(a.now=a.now||a.webkitNow||a.msNow||a.oNow||a.mozNow,+a.now()):+new Date},o.random=function(a,b){return a+Math.random()*(b-a)},o.colorToNumber=function(a){return a=a.replace("#",""),3==a.length&&(a=a.charAt(0)+a.charAt(0)+a.charAt(1)+a.charAt(1)+a.charAt(2)+a.charAt(2)),parseInt(a,16)},o.log=function(a,b){if(console&&console.log){var c;switch(b){case"warn":c="color: coral";break;case"error":c="color: red"}console.log("%c [Matter] "+b+": "+a,c)}},o.nextId=function(){return o._nextId++}}();var p={};!function(){var a=60,e=a,h=1e3/a,j=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(function(){a(o.now())},h)};p.create=function(b,c){c=o.isElement(b)?c:b,b=o.isElement(b)?b:null;var e={enabled:!0,positionIterations:6,velocityIterations:4,constraintIterations:2,enableSleeping:!1,timeScale:1,input:{},events:[],timing:{fps:a,timestamp:0,delta:h,correction:1,deltaMin:1e3/a,deltaMax:1e3/(.5*a),timeScale:1,isFixed:!1},render:{element:b,controller:A}},j=o.extend(e,c);return j.render=j.render.controller.create(j.render),j.world=d.create(j.world),j.pairs=i.create(),j.metrics=j.metrics||r.create(),j.input.mouse=j.input.mouse||s.create(j.render.canvas),j.broadphase=j.broadphase||{current:"grid",grid:{controller:g,instance:g.create(),detector:f.collisions},bruteForce:{detector:f.bruteForce}},j},p.run=function(a){var b,c=0,d=0,f=[],g=1;!function h(i){if(j(h),a.enabled){var k,m,o=a.timing,r={timestamp:i};q.trigger(a,"beforeTick",r),o.isFixed?k=o.delta:(k=i-b||o.delta,b=i,f.push(k),f=f.slice(-e),k=Math.min.apply(null,f),k=ko.deltaMax?o.deltaMax:k,m=k/o.delta,o.delta=k),0!==g&&(m*=o.timeScale/g),0===o.timeScale&&(m=0),g=o.timeScale,d+=1,i-c>=1e3&&(o.fps=d*((i-c)/1e3),c=i,d=0),q.trigger(a,"tick",r),a.world.isModified&&a.render.controller.clear(a.render),p.update(a,k,m),n(a),l(a),p.render(a),q.trigger(a,"afterTick",r)}}()},p.update=function(a,d,e){e="undefined"!=typeof e?e:1;var f,g=a.world,h=a.timing,j=a.broadphase[a.broadphase.current],l=[];h.timestamp+=d*h.timeScale,h.correction=e;var n={timestamp:a.timing.timestamp};q.trigger(a,"beforeUpdate",n);var o=c.allBodies(g),p=c.allConstraints(g);for(r.reset(a.metrics),a.enableSleeping&&t.update(o),b.applyGravityAll(o,g.gravity),b.updateAll(o,d,h.timeScale,e,g.bounds),f=0;f0&&q.trigger(a,"collisionStart",{pairs:b.collisionStart}),b.collisionActive.length>0&&q.trigger(a,"collisionActive",{pairs:b.collisionActive}),b.collisionEnd.length>0&&q.trigger(a,"collisionEnd",{pairs:b.collisionEnd})}}();var q={};!function(){q.on=function(a,b,c){for(var d,e=b.split(" "),f=0;f0||e.force.y>0)t.set(e,!1);else{var g=Math.min(e.motion,f),h=Math.max(e.motion,f);e.motion=c*g+(1-c)*h,e.sleepThreshold>0&&e.motion=e.sleepThreshold&&t.set(e,!0)):e.sleepCounter>0&&(e.sleepCounter-=1)}}},t.afterCollisions=function(b){for(var c=0;ca&&t.set(h,!1)}}}},t.set=function(a,b){b?(a.isSleeping=!0,a.sleepCounter=a.sleepThreshold,a.positionImpulse.x=0,a.positionImpulse.y=0,a.positionPrev.x=a.position.x,a.positionPrev.y=a.position.y,a.anglePrev=a.angle,a.speed=0,a.angularSpeed=0,a.motion=0):(a.isSleeping=!1,a.sleepCounter=0)}}();var u={};!function(){u.rectangle=function(a,c,d,e,f){f=f||{};var g={label:"Rectangle Body",position:{x:a,y:c},vertices:z.fromPath("L 0 0 L "+d+" 0 L "+d+" "+e+" L 0 "+e)};if(f.chamfer){var h=f.chamfer;g.vertices=z.chamfer(g.vertices,h.radius,h.quality,h.qualityMin,h.qualityMax),delete f.chamfer}return b.create(o.extend({},g,f))},u.trapezoid=function(a,c,d,e,f,g){g=g||{},f*=.5;var h=(1-2*f)*d,i=d*f,j=i+h,k=j+i,l={label:"Trapezoid Body",position:{x:a,y:c},vertices:z.fromPath("L 0 0 L "+i+" "+-e+" L "+j+" "+-e+" L "+k+" 0")}; +if(g.chamfer){var m=g.chamfer;l.vertices=z.chamfer(l.vertices,m.radius,m.quality,m.qualityMin,m.qualityMax),delete g.chamfer}return b.create(o.extend({},l,g))},u.circle=function(a,b,c,d,e){d=d||{},d.label="Circle Body",e=e||25;var f=Math.ceil(Math.max(10,Math.min(e,c)));return f%2===1&&(f+=1),d.circleRadius=c,u.polygon(a,b,f,c,d)},u.polygon=function(a,c,d,e,f){if(f=f||{},3>d)return u.circle(a,c,e,f);for(var g=2*Math.PI/d,h="",i=.5*g,j=0;d>j;j+=1){var k=i+j*g,l=Math.cos(k)*e,m=Math.sin(k)*e;h+="L "+l.toFixed(3)+" "+m.toFixed(3)+" "}var n={label:"Polygon Body",position:{x:a,y:c},vertices:z.fromPath(h)};if(f.chamfer){var p=f.chamfer;n.vertices=z.chamfer(n.vertices,p.radius,p.quality,p.qualityMin,p.qualityMax),delete f.chamfer}return b.create(o.extend({},n,f))}}();var v={};!function(){v.stack=function(a,d,e,f,g,h,i){for(var j,k=c.create({label:"Stack"}),l=a,m=d,n=0,o=0;f>o;o++){for(var p=0,q=0;e>q;q++){var r=i(l,m,q,o,j,n);if(r){var s=r.bounds.max.y-r.bounds.min.y,t=r.bounds.max.x-r.bounds.min.x;s>p&&(p=s),b.translate(r,{x:.5*t,y:.5*s}),l=r.bounds.max.x+g,c.addBody(k,r),j=r,n+=1}}m+=p+h,l=a}return k},v.chain=function(a,b,d,e,f,g){for(var h=a.bodies,i=1;ig;g++){for(h=0;b>h;h++)h>0&&(i=l[h-1+g*b],j=l[h+g*b],c.addConstraint(a,m.create(o.extend({bodyA:i,bodyB:j},f))));for(h=0;b>h;h++)g>0&&(i=l[h+(g-1)*b],j=l[h+g*b],c.addConstraint(a,m.create(o.extend({bodyA:i,bodyB:j},f))),e&&h>0&&(k=l[h-1+(g-1)*b],c.addConstraint(a,m.create(o.extend({bodyA:k,bodyB:j},f)))),e&&b-1>h&&(k=l[h+1+(g-1)*b],c.addConstraint(a,m.create(o.extend({bodyA:k,bodyB:j},f)))))}return a.label+=" Mesh",a},v.pyramid=function(a,c,d,e,f,g,h){return v.stack(a,c,d,e,f,g,function(c,g,i,j,k,l){var m=Math.min(e,Math.ceil(d/2)),n=k?k.bounds.max.x-k.bounds.min.x:0;if(!(j>m)){j=m-j;var o=j,p=d-1-j;if(!(o>i||i>p)){1===l&&b.translate(k,{x:(i+(d%2===1?1:-1))*n,y:0});var q=k?i*n:0;return h(a+q+i*f,g,i,j,k,l)}}})},v.newtonsCradle=function(a,b,d,e,f){for(var g=c.create({label:"Newtons Cradle"}),h=0;d>h;h++){var i=1.9,j=u.circle(a+h*e*i,b+f,e,{inertia:99999,restitution:1,friction:0,frictionAir:1e-4,slop:.01}),k=m.create({pointA:{x:a+h*e*i,y:b},bodyB:j});c.addBody(g,j),c.addConstraint(g,k)}return g},v.car=function(a,d,e,f,g){var h=b.nextGroupId(),i=-20,j=.5*-e+i,k=.5*e-i,l=0,n=c.create({label:"Car"}),o=u.trapezoid(a,d,e,f,.3,{groupId:h,friction:.01,chamfer:{radius:10}}),p=u.circle(a+j,d+l,g,{groupId:h,restitution:.5,friction:.9,density:.01}),q=u.circle(a+k,d+l,g,{groupId:h,restitution:.5,friction:.9,density:.01}),r=m.create({bodyA:o,pointA:{x:j,y:l},bodyB:p,stiffness:.5}),s=m.create({bodyA:o,pointA:{x:k,y:l},bodyB:q,stiffness:.5});return c.addBody(n,o),c.addBody(n,p),c.addBody(n,q),c.addConstraint(n,r),c.addConstraint(n,s),n},v.softBody=function(a,b,c,d,e,f,g,h,i,j){i=o.extend({inertia:1/0},i),j=o.extend({stiffness:.4},j);var k=v.stack(a,b,c,d,e,f,function(a,b){return u.circle(a,b,h,i)});return v.mesh(k,c,d,g,j),k.label="Soft Body",k}}();var w={};!function(){w.fromVertices=function(a){for(var b={},c=0;ca.max.x&&(a.max.x=e.x),e.xa.max.y&&(a.max.y=e.y),e.y0?a.max.x+=c.x:a.min.x+=c.x,c.y>0?a.max.y+=c.y:a.min.y+=c.y)},x.contains=function(a,b){return b.x>=a.min.x&&b.x<=a.max.x&&b.y>=a.min.y&&b.y<=a.max.y},x.overlaps=function(a,b){return a.min.x<=b.max.x&&a.max.x>=b.min.x&&a.max.y>=b.min.y&&a.min.y<=b.max.y},x.translate=function(a,b){a.min.x+=b.x,a.max.x+=b.x,a.min.y+=b.y,a.max.y+=b.y},x.shift=function(a,b){var c=a.max.x-a.min.x,d=a.max.y-a.min.y;a.min.x=b.x,a.max.x=b.x+c,a.min.y=b.y,a.max.y=b.y+d}}();var y={};!function(){y.magnitude=function(a){return Math.sqrt(a.x*a.x+a.y*a.y)},y.magnitudeSquared=function(a){return a.x*a.x+a.y*a.y},y.rotate=function(a,b){var c=Math.cos(b),d=Math.sin(b);return{x:a.x*c-a.y*d,y:a.x*d+a.y*c}},y.rotateAbout=function(a,b,c){var d=Math.cos(b),e=Math.sin(b);return{x:c.x+((a.x-c.x)*d-(a.y-c.y)*e),y:c.y+((a.x-c.x)*e+(a.y-c.y)*d)}},y.normalise=function(a){var b=y.magnitude(a);return 0===b?{x:0,y:0}:{x:a.x/b,y:a.y/b}},y.dot=function(a,b){return a.x*b.x+a.y*b.y},y.cross=function(a,b){return a.x*b.y-a.y*b.x},y.add=function(a,b){return{x:a.x+b.x,y:a.y+b.y}},y.sub=function(a,b){return{x:a.x-b.x,y:a.y-b.y}},y.mult=function(a,b){return{x:a.x*b,y:a.y*b}},y.div=function(a,b){return{x:a.x/b,y:a.y/b}},y.perp=function(a,b){return b=b===!0?-1:1,{x:b*-a.y,y:b*a.x}},y.neg=function(a){return{x:-a.x,y:-a.y}},y.angle=function(a,b){return Math.atan2(b.y-a.y,b.x-a.x)}}();var z={};!function(){z.create=function(a,b){for(var c=0;c0)return!1}return!0},z.scale=function(a,b,c,d){if(1===b&&1===c)return a;d=d||z.centre(a);for(var e,f,g=0;g=0?g-1:a.length-1],i=a[g],j=a[(g+1)%a.length],k=b[gv;v++)f.push(y.add(y.rotate(p,u*v),r))}else f.push(i)}return f}}();var A={};!function(){A.create=function(b){var c={controller:A,element:null,canvas:null,options:{width:800,height:600,background:"#fafafa",wireframeBackground:"#222",hasBounds:!1,enabled:!0,wireframes:!0,showSleeping:!0,showDebug:!1,showBroadphase:!1,showBounds:!1,showVelocity:!1,showCollisions:!1,showAxes:!1,showPositions:!1,showAngleIndicator:!1,showIds:!1,showShadows:!1}},d=o.extend(c,b);return d.canvas=d.canvas||a(d.options.width,d.options.height),d.context=d.canvas.getContext("2d"),d.textures={},d.bounds=d.bounds||{min:{x:0,y:0},max:{x:d.options.width,y:d.options.height}},A.setBackground(d,d.options.background),o.isElement(d.element)?d.element.appendChild(d.canvas):o.log('No "render.element" passed, "render.canvas" was not inserted into document.',"warn"),d},A.clear=function(){},A.setBackground=function(a,b){if(a.currentBackground!==b){var c=b;/(jpg|gif|png)$/.test(b)&&(c="url("+b+")"),a.canvas.style.background=c,a.canvas.style.backgroundSize="contain",a.currentBackground=b}},A.world=function(a){var b,d=a.render,e=a.world,f=d.canvas,g=d.context,h=d.options,i=c.allBodies(e),j=c.allConstraints(e),k=[],l=[];h.wireframes?A.setBackground(d,h.wireframeBackground):A.setBackground(d,h.background),g.globalCompositeOperation="source-in",g.fillStyle="transparent",g.fillRect(0,0,f.width,f.height),g.globalCompositeOperation="source-over";var m=d.bounds.max.x-d.bounds.min.x,n=d.bounds.max.y-d.bounds.min.y,o=m/d.options.width,p=n/d.options.height;if(h.hasBounds){for(b=0;b=500){var k="";k+="fps: "+Math.round(a.timing.fps)+j,a.metrics.extended&&(k+="delta: "+a.timing.delta.toFixed(3)+j,k+="correction: "+a.timing.correction.toFixed(3)+j,k+="bodies: "+i.length+j,a.broadphase.controller===g&&(k+="buckets: "+a.metrics.buckets+j),k+="\n",k+="collisions: "+a.metrics.collisions+j,k+="pairs: "+a.pairs.list.length+j,k+="broad: "+a.metrics.broadEff+j,k+="mid: "+a.metrics.midEff+j,k+="narrow: "+a.metrics.narrowEff+j),f.debugString=k,f.debugTimestamp=a.timing.timestamp}if(f.debugString){d.font="12px Arial",d.fillStyle=h.wireframes?"rgba(255,255,255,0.5)":"rgba(0,0,0,0.5)";for(var l=f.debugString.split("\n"),m=0;m0){var l=d.activeContacts[0].vertex.x,m=d.activeContacts[0].vertex.y;2===d.activeContacts.length&&(l=(d.activeContacts[0].vertex.x+d.activeContacts[1].vertex.x)/2,m=(d.activeContacts[0].vertex.y+d.activeContacts[1].vertex.y)/2),h.moveTo(l-8*e.normal.x,m-8*e.normal.y),h.lineTo(l,m)}h.strokeStyle=i.wireframes?"rgba(255,165,0,0.7)":"orange",h.lineWidth=1,h.stroke()},A.grid=function(a,b,c){var d=c,e=a.render.options;d.strokeStyle=e.wireframes?"rgba(255,180,0,0.1)":"rgba(255,180,0,0.5)",d.beginPath();for(var f=o.keys(b.buckets),g=0;g - - + + @@ -33,8 +33,12 @@ + + + + diff --git a/demo/js/lib/matter-0.7.0.js b/demo/js/lib/matter-0.8.0.js similarity index 83% rename from demo/js/lib/matter-0.7.0.js rename to demo/js/lib/matter-0.8.0.js index ff36224..3184f5c 100644 --- a/demo/js/lib/matter-0.7.0.js +++ b/demo/js/lib/matter-0.8.0.js @@ -1,5 +1,5 @@ /** -* matter-0.7.0.js 0.7.0-alpha 2014-04-01 +* matter-0.8.0.js 0.8.0-alpha 2014-05-04 * http://brm.io/matter-js/ * License: MIT */ @@ -51,8 +51,7 @@ var Body = {}; (function() { - var _nextId = 0, - _nextGroupId = 1; + var _nextGroupId = 1; /** * Description to be written. @@ -62,9 +61,11 @@ var Body = {}; */ Body.create = function(options) { var defaults = { - id: Body.nextId(), + id: Common.nextId(), type: 'body', + label: 'Body', angle: 0, + vertices: Vertices.fromPath('L 0 0 L 40 0 L 40 40 L 0 40'), position: { x: 0, y: 0 }, force: { x: 0, y: 0 }, torque: 0, @@ -84,33 +85,24 @@ var Body = {}; frictionAir: 0.01, groupId: 0, slop: 0.05, + timeScale: 1, render: { visible: true, sprite: { xScale: 1, yScale: 1 }, - path: 'L 0 0 L 40 0 L 40 40 L 0 40', lineWidth: 1.5 } }; var body = Common.extend(defaults, options); - Body.updateProperties(body); + _initProperties(body); return body; }; - /** - * Description - * @method nextId - * @return {Number} Unique bodyID - */ - Body.nextId = function() { - return _nextId++; - }; - /** * Description * @method nextGroupId @@ -121,13 +113,13 @@ var Body = {}; }; /** - * Description - * @method updateProperties + * Initialises body properties + * @method _initProperties + * @private * @param {body} body */ - Body.updateProperties = function(body) { + var _initProperties = function(body) { // calculated properties - body.vertices = body.vertices || Vertices.fromPath(body.render.path); body.axes = body.axes || Axes.fromVertices(body.vertices); body.area = Vertices.area(body.vertices); body.bounds = Bounds.create(body.vertices); @@ -149,15 +141,33 @@ var Body = {}; Axes.rotate(body.axes, body.angle); Bounds.update(body.bounds, body.vertices, body.velocity); - if (body.isStatic) { + Body.setStatic(body, body.isStatic); + Sleeping.set(body, body.isSleeping); + }; + + /** + * Sets the body as static, including isStatic flag and setting mass and inertia to Infinity + * @method setStatic + * @param {bool} isStatic + */ + Body.setStatic = function(body, isStatic) { + body.isStatic = isStatic; + + if (isStatic) { body.restitution = 0; body.friction = 1; body.mass = body.inertia = body.density = Infinity; body.inverseMass = body.inverseInertia = 0; body.render.lineWidth = 1; - } - Sleeping.set(body, body.isSleeping); + body.positionPrev.x = body.position.x; + body.positionPrev.y = body.position.y; + body.anglePrev = body.angle; + body.angularVelocity = 0; + body.speed = 0; + body.angularSpeed = 0; + body.motion = 0; + } }; /** @@ -200,10 +210,11 @@ var Body = {}; * @method updateAll * @param {body[]} bodies * @param {number} deltaTime + * @param {number} timeScale * @param {number} correction * @param {bounds} worldBounds */ - Body.updateAll = function(bodies, deltaTime, correction, worldBounds) { + Body.updateAll = function(bodies, deltaTime, timeScale, correction, worldBounds) { for (var i = 0; i < bodies.length; i++) { var body = bodies[i]; @@ -216,7 +227,7 @@ var Body = {}; || body.bounds.max.y < worldBounds.min.y || body.bounds.min.y > worldBounds.max.y) continue; - Body.update(body, deltaTime, correction); + Body.update(body, deltaTime, timeScale, correction); } }; @@ -225,13 +236,14 @@ var Body = {}; * @method update * @param {body} body * @param {number} deltaTime + * @param {number} timeScale * @param {number} correction */ - Body.update = function(body, deltaTime, correction) { - var deltaTimeSquared = deltaTime * deltaTime; + Body.update = function(body, deltaTime, timeScale, correction) { + var deltaTimeSquared = Math.pow(deltaTime * timeScale * body.timeScale, 2); // from the previous step - var frictionAir = 1 - body.frictionAir, + var frictionAir = 1 - body.frictionAir * timeScale * body.timeScale, velocityPrevX = body.position.x - body.positionPrev.x, velocityPrevY = body.position.y - body.positionPrev.y; @@ -305,6 +317,34 @@ var Body = {}; Bounds.update(body.bounds, body.vertices, body.velocity); }; + /** + * Scales the body, including updating physical properties (mass, area, axes, inertia), from a point (default is centre) + * @method translate + * @param {body} body + * @param {number} scaleX + * @param {number} scaleY + * @param {vector} point + */ + Body.scale = function(body, scaleX, scaleY, point) { + // scale vertices + Vertices.scale(body.vertices, scaleX, scaleY, point); + + // update properties + body.axes = Axes.fromVertices(body.vertices); + body.area = Vertices.area(body.vertices); + body.mass = body.density * body.area; + body.inverseMass = 1 / body.mass; + + // update inertia (requires vertices to be at origin) + Vertices.translate(body.vertices, { x: -body.position.x, y: -body.position.y }); + body.inertia = Vertices.inertia(body.vertices, body.mass); + body.inverseInertia = 1 / body.inertia; + Vertices.translate(body.vertices, { x: body.position.x, y: body.position.y }); + + // update bounds + Bounds.update(body.bounds, body.vertices, body.velocity); + }; + })(); ; // End src/body/Body.js @@ -325,8 +365,6 @@ var Composite = {}; (function() { - var _nextId = 0; - /** * Description * @method create @@ -335,25 +373,17 @@ var Composite = {}; */ Composite.create = function(options) { return Common.extend({ - id: Composite.nextId(), + id: Common.nextId(), type: 'composite', parent: null, isModified: false, bodies: [], constraints: [], - composites: [] + composites: [], + label: 'Composite' }, options); }; - /** - * Returns the next unique compositeID - * @method nextId - * @return {Number} Unique compositeID - */ - Composite.nextId = function() { - return _nextId++; - }; - /** * Sets the composite's `isModified` flag. * If `updateParents` is true, all parents will be set (default: false). @@ -672,6 +702,74 @@ var Composite = {}; return composites; }; + /** + * Searches the composite recursively for an object matching the type and id supplied, null if not found + * @method get + * @param {composite} composite + * @param {number} id + * @param {string} type + * @return {object} The requested object, if found + */ + Composite.get = function(composite, id, type) { + var objects, + object; + + switch (type) { + case 'body': + objects = Composite.allBodies(composite); + break; + case 'constraint': + objects = Composite.allConstraints(composite); + break; + case 'composite': + objects = Composite.allComposites(composite).concat(composite); + break; + } + + if (!objects) + return null; + + object = objects.filter(function(object) { + return object.id.toString() === id.toString(); + }); + + return object.length === 0 ? null : object[0]; + }; + + /** + * Moves the given object(s) from compositeA to compositeB (equal to a remove followed by an add) + * @method move + * @param {compositeA} compositeA + * @param {object[]} objects + * @param {compositeB} compositeB + * @return {composite} Returns compositeA + */ + Composite.move = function(compositeA, objects, compositeB) { + Composite.remove(compositeA, objects); + Composite.add(compositeB, objects); + return compositeA; + }; + + /** + * Assigns new ids for all objects in the composite, recursively + * @method rebase + * @param {composite} composite + * @return {composite} Returns composite + */ + Composite.rebase = function(composite) { + var objects = Composite.allBodies(composite) + .concat(Composite.allConstraints(composite)) + .concat(Composite.allComposites(composite)); + + for (var i = 0; i < objects.length; i++) { + objects[i].id = Common.nextId(); + } + + Composite.setModified(composite, true, true, false); + + return composite; + }; + })(); ; // End src/body/Composite.js @@ -701,6 +799,7 @@ var World = {}; var composite = Composite.create(); var defaults = { + label: 'World', gravity: { x: 0, y: 1 }, bounds: { min: { x: 0, y: 0 }, @@ -1497,6 +1596,77 @@ var Pairs = {}; ; // End src/collision/Pairs.js +// Begin src/collision/Query.js + +/** +* Functions for performing collision queries +* +* @class Query +*/ + +var Query = {}; + +(function() { + + /** + * Casts a ray segment against a set of bodies and returns all collisions, ray width is optional. Intersection points are not provided. + * @method ray + * @param {body[]} bodies + * @param {vector} startPoint + * @param {vector} endPoint + * @return {object[]} Collisions + */ + Query.ray = function(bodies, startPoint, endPoint, rayWidth) { + rayWidth = rayWidth || Number.MIN_VALUE; + + var rayAngle = Vector.angle(startPoint, endPoint), + rayLength = Vector.magnitude(Vector.sub(startPoint, endPoint)), + rayX = (endPoint.x + startPoint.x) * 0.5, + rayY = (endPoint.y + startPoint.y) * 0.5, + ray = Bodies.rectangle(rayX, rayY, rayLength, rayWidth, { angle: rayAngle }), + collisions = []; + + for (var i = 0; i < bodies.length; i++) { + var bodyA = bodies[i]; + + if (Bounds.overlaps(bodyA.bounds, ray.bounds)) { + var collision = SAT.collides(bodyA, ray); + if (collision.collided) { + collision.body = collision.bodyA = collision.bodyB = bodyA; + collisions.push(collision); + } + } + } + + return collisions; + }; + + /** + * Returns all bodies whose bounds are inside (or outside if set) the given set of bounds, from the given set of bodies + * @method region + * @param {body[]} bodies + * @param {bounds} bounds + * @param {bool} outside + * @return {body[]} The bodies matching the query + */ + Query.region = function(bodies, bounds, outside) { + var result = []; + + for (var i = 0; i < bodies.length; i++) { + var body = bodies[i], + overlaps = Bounds.overlaps(body.bounds, bounds); + if ((overlaps && !outside) || (!overlaps && outside)) + result.push(body); + } + + return result; + }; + +})(); + +; // End src/collision/Query.js + + // Begin src/collision/Resolver.js /** @@ -1517,8 +1687,9 @@ var Resolver = {}; * Description * @method solvePosition * @param {pair[]} pairs + * @param {number} timeScale */ - Resolver.solvePosition = function(pairs) { + Resolver.solvePosition = function(pairs, timeScale) { var i, pair, collision, @@ -1560,7 +1731,7 @@ var Resolver = {}; bodyA = collision.bodyA; bodyB = collision.bodyB; normal = collision.normal; - positionImpulse = (pair.separation * _positionDampen) - pair.slop; + positionImpulse = ((pair.separation * _positionDampen) - pair.slop) * timeScale; if (bodyA.isStatic || bodyB.isStatic) positionImpulse *= 2; @@ -1673,8 +1844,9 @@ var Resolver = {}; * @method solveVelocity * @param {pair[]} pairs */ - Resolver.solveVelocity = function(pairs) { - var impulse = {}; + Resolver.solveVelocity = function(pairs, timeScale) { + var impulse = {}, + timeScaleSquared = timeScale * timeScale; for (var i = 0; i < pairs.length; i++) { var pair = pairs[i]; @@ -1719,8 +1891,8 @@ var Resolver = {}; // coulomb friction var tangentImpulse = tangentVelocity; - if (tangentSpeed > normalForce * pair.friction) - tangentImpulse = normalForce * pair.friction * tangentVelocityDirection; + if (tangentSpeed > normalForce * pair.friction * timeScaleSquared) + tangentImpulse = normalForce * pair.friction * timeScaleSquared * tangentVelocityDirection; // modify impulses accounting for mass, inertia and offset var oAcN = Vector.cross(offsetA, normal), @@ -1730,7 +1902,7 @@ var Resolver = {}; tangentImpulse *= share; // handle high velocity and resting collisions separately - if (normalVelocity < 0 && normalVelocity * normalVelocity > _restingThresh) { + if (normalVelocity < 0 && normalVelocity * normalVelocity > _restingThresh * timeScaleSquared) { // high velocity so clear cached contact impulse contact.normalImpulse = 0; contact.tangentImpulse = 0; @@ -2050,8 +2222,7 @@ var Constraint = {}; (function() { var _minLength = 0.000001, - _minDifference = 0.001, - _nextId = 0; + _minDifference = 0.001; /** * Description @@ -2085,7 +2256,8 @@ var Constraint = {}; constraint.render = Common.extend(render, constraint.render); // option defaults - constraint.id = constraint.id || Constraint.nextId(); + constraint.id = constraint.id || Common.nextId(); + constraint.label = constraint.label || 'Constraint'; constraint.type = 'constraint'; constraint.stiffness = constraint.stiffness || 1; constraint.angularStiffness = constraint.angularStiffness || 0; @@ -2099,10 +2271,11 @@ var Constraint = {}; * Description * @method solveAll * @param {constraint[]} constraints + * @param {number} timeScale */ - Constraint.solveAll = function(constraints) { + Constraint.solveAll = function(constraints, timeScale) { for (var i = 0; i < constraints.length; i++) { - Constraint.solve(constraints[i]); + Constraint.solve(constraints[i], timeScale); } }; @@ -2110,8 +2283,9 @@ var Constraint = {}; * Description * @method solve * @param {constraint} constraint + * @param {number} timeScale */ - Constraint.solve = function(constraint) { + Constraint.solve = function(constraint, timeScale) { var bodyA = constraint.bodyA, bodyB = constraint.bodyB, pointA = constraint.pointA, @@ -2148,10 +2322,10 @@ var Constraint = {}; // 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); + force = Vector.mult(delta, difference * 0.5 * constraint.stiffness * timeScale * timeScale); // if difference is very small, we can skip - if (Math.abs(1 - (currentLength / constraint.length)) < _minDifference) + if (Math.abs(1 - (currentLength / constraint.length)) < _minDifference * timeScale) return; var velocityPointA, @@ -2285,15 +2459,6 @@ var Constraint = {}; } }; - /** - * Returns the next unique constraintId - * @method nextId - * @return {Number} Unique constraintId - */ - Constraint.nextId = function() { - return _nextId++; - }; - })(); ; // End src/constraint/Constraint.js @@ -2323,6 +2488,7 @@ var MouseConstraint = {}; var mouse = engine.input.mouse; var constraint = Constraint.create({ + label: 'Mouse Constraint', pointA: mouse.position, pointB: { x: 0, y: 0 }, length: 0.01, @@ -2404,6 +2570,8 @@ var Common = {}; (function() { + Common._nextId = 0; + /** * Description * @method extend @@ -2663,6 +2831,15 @@ var Common = {}; console.log('%c [Matter] ' + type + ': ' + message, style); }; + /** + * Returns the next unique sequential ID + * @method nextId + * @return {Number} Unique sequential ID + */ + Common.nextId = function() { + return Common._nextId++; + }; + })(); ; // End src/core/Common.js @@ -2677,9 +2854,7 @@ var Common = {}; * @class Engine */ -// TODO: multiple event handlers, before & after handlers // TODO: viewports -// TODO: frameskipping var Engine = {}; @@ -2721,7 +2896,9 @@ var Engine = {}; delta: _delta, correction: 1, deltaMin: 1000 / _fps, - deltaMax: 1000 / (_fps * 0.5) + deltaMax: 1000 / (_fps * 0.5), + timeScale: 1, + isFixed: false }, render: { element: element, @@ -2753,89 +2930,79 @@ var Engine = {}; }; /** - * Description + * An optional utility function that provides a game loop, that handles updating the engine for you. + * Calls `Engine.update` and `Engine.render` on the `requestAnimationFrame` event automatically. + * Handles time correction and non-fixed dynamic timing (if enabled). + * Triggers `beforeTick`, `tick` and `afterTick` events. * @method run * @param {engine} engine */ Engine.run = function(engine) { - var timing = engine.timing, - delta, - correction, - counterTimestamp = 0, + var counterTimestamp = 0, frameCounter = 0, - deltaHistory = []; + deltaHistory = [], + timePrev, + timeScalePrev = 1; - (function render(timestamp){ + (function render(time){ _requestAnimationFrame(render); if (!engine.enabled) return; - // timestamp is undefined on the first update - timestamp = timestamp || 0; + var timing = engine.timing, + delta, + correction; // create an event object var event = { - timestamp: timestamp + timestamp: time }; - /** - * Fired at the start of a tick, before any updates to the engine or timing - * - * @event beforeTick - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ Events.trigger(engine, 'beforeTick', event); - delta = (timestamp - timing.timestamp) || _delta; + if (timing.isFixed) { + // fixed timestep + delta = timing.delta; + } else { + // dynamic timestep based on wall clock between calls + delta = (time - timePrev) || timing.delta; + timePrev = time; - // optimistically filter delta over a few frames, to improve stability - deltaHistory.push(delta); - deltaHistory = deltaHistory.slice(-_deltaSampleSize); - delta = Math.min.apply(null, deltaHistory); - - // limit delta - delta = delta < engine.timing.deltaMin ? engine.timing.deltaMin : delta; - delta = delta > engine.timing.deltaMax ? engine.timing.deltaMax : 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 < timing.deltaMin ? timing.deltaMin : delta; + delta = delta > timing.deltaMax ? timing.deltaMax : delta; - // verlet time correction - correction = delta / timing.delta; + // time correction for delta + correction = delta / timing.delta; - // update engine timing object - timing.timestamp = timestamp; - timing.correction = correction; - timing.delta = delta; + // update engine timing object + timing.delta = delta; + } + + // time correction for time scaling + if (timeScalePrev !== 0) + correction *= timing.timeScale / timeScalePrev; + + if (timing.timeScale === 0) + correction = 0; + + timeScalePrev = timing.timeScale; // fps counter frameCounter += 1; - if (timestamp - counterTimestamp >= 1000) { - timing.fps = frameCounter * ((timestamp - counterTimestamp) / 1000); - counterTimestamp = timestamp; + if (time - counterTimestamp >= 1000) { + timing.fps = frameCounter * ((time - counterTimestamp) / 1000); + counterTimestamp = time; frameCounter = 0; } - /** - * Fired after engine timing updated, but just before engine state updated - * - * @event tick - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - /** - * Fired just before an update - * - * @event beforeUpdate - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(engine, 'tick beforeUpdate', event); + Events.trigger(engine, 'tick', event); // if world has been modified, clear the render scene graph if (engine.world.isModified) @@ -2848,54 +3015,15 @@ var Engine = {}; _triggerCollisionEvents(engine); _triggerMouseEvents(engine); - /** - * Fired after engine update and all collision events - * - * @event afterUpdate - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - /** - * Fired just before rendering - * - * @event beforeRender - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(engine, 'afterUpdate beforeRender', event); - // render - if (engine.render.options.enabled) - engine.render.controller.world(engine); + Engine.render(engine); - /** - * Fired after rendering - * - * @event afterRender - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - /** - * Fired after engine update and after rendering - * - * @event afterTick - * @param {} event An event object - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(engine, 'afterTick afterRender', event); + Events.trigger(engine, 'afterTick', event); })(); }; /** - * Description + * Moves the simulation forward in time by `delta` ms. Triggers `beforeUpdate` and `afterUpdate` events. * @method update * @param {engine} engine * @param {number} delta @@ -2903,11 +3031,25 @@ var Engine = {}; * @return engine */ Engine.update = function(engine, delta, correction) { + correction = (typeof correction !== 'undefined') ? correction : 1; + var world = engine.world, + timing = engine.timing, broadphase = engine.broadphase[engine.broadphase.current], broadphasePairs = [], i; + // increment timestamp + timing.timestamp += delta * timing.timeScale; + timing.correction = correction; + + // create an event object + var event = { + timestamp: engine.timing.timestamp + }; + + Events.trigger(engine, 'beforeUpdate', event); + // get lists of all bodies and constraints, no matter what composites they are in var allBodies = Composite.allBodies(world), allConstraints = Composite.allConstraints(world); @@ -2923,11 +3065,11 @@ var Engine = {}; Body.applyGravityAll(allBodies, world.gravity); // update all body position and rotation by integration - Body.updateAll(allBodies, delta * engine.timeScale, correction, world.bounds); + Body.updateAll(allBodies, delta, timing.timeScale, correction, world.bounds); // update all constraints for (i = 0; i < engine.constraintIterations; i++) { - Constraint.solveAll(allConstraints); + Constraint.solveAll(allConstraints, timing.timeScale); } Constraint.postSolveAll(allBodies); @@ -2952,7 +3094,7 @@ var Engine = {}; // update collision pairs var pairs = engine.pairs, - timestamp = engine.timing.timestamp; + timestamp = timing.timestamp; Pairs.update(pairs, collisions, timestamp); Pairs.removeOld(pairs, timestamp); @@ -2963,12 +3105,12 @@ var Engine = {}; // iteratively resolve velocity between collisions Resolver.preSolveVelocity(pairs.list); for (i = 0; i < engine.velocityIterations; i++) { - Resolver.solveVelocity(pairs.list); + Resolver.solveVelocity(pairs.list, timing.timeScale); } // iteratively resolve position between collisions for (i = 0; i < engine.positionIterations; i++) { - Resolver.solvePosition(pairs.list); + Resolver.solvePosition(pairs.list, timing.timeScale); } Resolver.postSolvePosition(allBodies); @@ -2982,8 +3124,27 @@ var Engine = {}; if (world.isModified) Composite.setModified(world, false, false, true); + Events.trigger(engine, 'afterUpdate', event); + return engine; }; + + /** + * Renders the world by calling its defined renderer `engine.render.controller`. Triggers `beforeRender` and `afterRender` events. + * @method render + * @param {engine} engineA + * @param {engine} engineB + */ + Engine.render = function(engine) { + // create an event object + var event = { + timestamp: engine.timing.timestamp + }; + + Events.trigger(engine, 'beforeRender', event); + engine.render.controller.world(engine); + Events.trigger(engine, 'afterRender', event); + }; /** * Description @@ -3004,7 +3165,7 @@ var Engine = {}; for (var i = 0; i < bodies.length; i++) { var body = bodies[i]; Sleeping.set(body, false); - body.id = Body.nextId(); + body.id = Common.nextId(); } } }; @@ -3037,45 +3198,18 @@ var Engine = {}; var mouse = engine.input.mouse, mouseEvents = mouse.sourceEvents; - /** - * Fired when the mouse has moved (or a touch moves) during the last step - * - * @event mousemove - * @param {} event An event object - * @param {mouse} event.mouse The engine's mouse instance - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (mouseEvents.mousemove) { Events.trigger(engine, 'mousemove', { mouse: mouse }); } - /** - * Fired when the mouse is down (or a touch has started) during the last step - * - * @event mousedown - * @param {} event An event object - * @param {mouse} event.mouse The engine's mouse instance - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (mouseEvents.mousedown) { Events.trigger(engine, 'mousedown', { mouse: mouse }); } - /** - * Fired when the mouse is up (or a touch has ended) during the last step - * - * @event mouseup - * @param {} event An event object - * @param {mouse} event.mouse The engine's mouse instance - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (mouseEvents.mouseup) { Events.trigger(engine, 'mouseup', { mouse: mouse @@ -3095,48 +3229,18 @@ var Engine = {}; var _triggerCollisionEvents = function(engine) { var pairs = engine.pairs; - /** - * Fired after engine update, provides a list of all pairs that have started to collide in the current tick (if any) - * - * @event collisionStart - * @param {} event An event object - * @param {} event.pairs List of affected pairs - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (pairs.collisionStart.length > 0) { Events.trigger(engine, 'collisionStart', { pairs: pairs.collisionStart }); } - /** - * Fired after engine update, provides a list of all pairs that are colliding in the current tick (if any) - * - * @event collisionActive - * @param {} event An event object - * @param {} event.pairs List of affected pairs - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (pairs.collisionActive.length > 0) { Events.trigger(engine, 'collisionActive', { pairs: pairs.collisionActive }); } - /** - * Fired after engine update, provides a list of all pairs that have ended collision in the current tick (if any) - * - * @event collisionEnd - * @param {} event An event object - * @param {} event.pairs List of affected pairs - * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ if (pairs.collisionEnd.length > 0) { Events.trigger(engine, 'collisionEnd', { pairs: pairs.collisionEnd @@ -3144,6 +3248,145 @@ var Engine = {}; } }; + /* + * + * Events Documentation + * + */ + + /** + * Fired at the start of a tick, before any updates to the engine or timing + * + * @event beforeTick + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine timing updated, but just before engine state updated + * + * @event tick + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired just before an update + * + * @event beforeUpdate + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine update and all collision events + * + * @event afterUpdate + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired just before rendering + * + * @event beforeRender + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after rendering + * + * @event afterRender + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine update and after rendering + * + * @event afterTick + * @param {} event An event object + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired when the mouse has moved (or a touch moves) during the last step + * + * @event mousemove + * @param {} event An event object + * @param {mouse} event.mouse The engine's mouse instance + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired when the mouse is down (or a touch has started) during the last step + * + * @event mousedown + * @param {} event An event object + * @param {mouse} event.mouse The engine's mouse instance + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired when the mouse is up (or a touch has ended) during the last step + * + * @event mouseup + * @param {} event An event object + * @param {mouse} event.mouse The engine's mouse instance + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine update, provides a list of all pairs that have started to collide in the current tick (if any) + * + * @event collisionStart + * @param {} event An event object + * @param {} event.pairs List of affected pairs + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine update, provides a list of all pairs that are colliding in the current tick (if any) + * + * @event collisionActive + * @param {} event An event object + * @param {} event.pairs List of affected pairs + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + + /** + * Fired after engine update, provides a list of all pairs that have ended collision in the current tick (if any) + * + * @event collisionEnd + * @param {} event An event object + * @param {} event.pairs List of affected pairs + * @param {DOMHighResTimeStamp} event.timestamp The timestamp of the current tick + * @param {} event.source The source object of the event + * @param {} event.name The name of the event + */ + })(); ; // End src/core/Engine.js @@ -3179,23 +3422,43 @@ var Events = {}; object.events[name] = object.events[name] || []; object.events[name].push(callback); } + + return callback; }; /** - * Clears all callbacks for the given event names if supplied, otherwise all events + * Removes the given event callback. If no callback, clears all callbacks in eventNames. If no eventNames, clears all events. * @method off * @param {} object * @param {string} eventNames + * @param {function} callback */ - Events.off = function(object, eventNames) { + Events.off = function(object, eventNames, callback) { if (!eventNames) { object.events = {}; return; } + // handle Events.off(object, callback) + if (typeof eventNames === 'function') { + callback = eventNames; + eventNames = Common.keys(object.events).join(' '); + } + var names = eventNames.split(' '); + for (var i = 0; i < names.length; i++) { - object.events[names[i]] = []; + var callbacks = object.events[names[i]], + newCallbacks = []; + + if (callback) { + for (var j = 0; j < callbacks.length; j++) { + if (callbacks[j] !== callback) + newCallbacks.push(callbacks[j]); + } + } + + object.events[names[i]] = newCallbacks; } }; @@ -3348,15 +3611,20 @@ var Mouse; var mouse = this; this.element = element || document.body; + this.absolute = { x: 0, y: 0 }; this.position = { x: 0, y: 0 }; this.mousedownPosition = { x: 0, y: 0 }; this.mouseupPosition = { x: 0, y: 0 }; + this.offset = { x: 0, y: 0 }; + this.scale = { x: 1, y: 1 }; + this.wheelDelta = 0; this.button = -1; this.sourceEvents = { mousemove: null, mousedown: null, - mouseup: null + mouseup: null, + mousewheel: null }; this.mousemove = function(event) { @@ -3368,7 +3636,10 @@ var Mouse; event.preventDefault(); } - mouse.position = position; + mouse.absolute.x = position.x; + mouse.absolute.y = position.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; mouse.sourceEvents.mousemove = event; }; @@ -3383,7 +3654,12 @@ var Mouse; mouse.button = event.button; } - mouse.position = mouse.mousedownPosition = position; + mouse.absolute.x = position.x; + mouse.absolute.y = position.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; + mouse.mousedownPosition.x = mouse.position.x; + mouse.mousedownPosition.y = mouse.position.y; mouse.sourceEvents.mousedown = event; }; @@ -3396,10 +3672,20 @@ var Mouse; } mouse.button = -1; - mouse.position = mouse.mouseupPosition = position; + mouse.absolute.x = position.x; + mouse.absolute.y = position.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; + mouse.mouseupPosition.x = mouse.position.x; + mouse.mouseupPosition.y = mouse.position.y; mouse.sourceEvents.mouseup = event; }; + this.mousewheel = function(event) { + mouse.wheelDelta = Math.max(-1, Math.min(1, event.wheelDelta || -event.detail)); + event.preventDefault(); + }; + Mouse.setElement(mouse, mouse.element); }; @@ -3426,6 +3712,9 @@ var Mouse; element.addEventListener('mousedown', mouse.mousedown); element.addEventListener('mouseup', mouse.mouseup); + element.addEventListener("mousewheel", mouse.mousewheel); + element.addEventListener("DOMMouseScroll", mouse.mousewheel); + element.addEventListener('touchmove', mouse.mousemove); element.addEventListener('touchstart', mouse.mousedown); element.addEventListener('touchend', mouse.mouseup); @@ -3440,6 +3729,32 @@ var Mouse; mouse.sourceEvents.mousemove = null; mouse.sourceEvents.mousedown = null; mouse.sourceEvents.mouseup = null; + mouse.sourceEvents.mousewheel = null; + mouse.wheelDelta = 0; + }; + + /** + * Sets the offset + * @method setOffset + * @param {mouse} mouse + */ + Mouse.setOffset = function(mouse, offset) { + mouse.offset.x = offset.x; + mouse.offset.y = offset.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; + }; + + /** + * Sets the scale + * @method setScale + * @param {mouse} mouse + */ + Mouse.setScale = function(mouse, scale) { + mouse.scale.x = scale.x; + mouse.scale.y = scale.y; + mouse.position.x = mouse.absolute.x * mouse.scale.x + mouse.offset.x; + mouse.position.y = mouse.absolute.y * mouse.scale.y + mouse.offset.y; }; /** @@ -3452,8 +3767,9 @@ var Mouse; */ 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, + rootNode = (document.documentElement || document.body.parentNode || document.body), + scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset : rootNode.scrollLeft, + scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset : rootNode.scrollTop, touches = event.changedTouches, x, y; @@ -3620,12 +3936,18 @@ var Bodies = {}; options = options || {}; var rectangle = { + label: 'Rectangle Body', position: { x: x, y: y }, - render: { - path: 'L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height - } + vertices: Vertices.fromPath('L 0 0 L ' + width + ' 0 L ' + width + ' ' + height + ' L 0 ' + height) }; + if (options.chamfer) { + var chamfer = options.chamfer; + rectangle.vertices = Vertices.chamfer(rectangle.vertices, chamfer.radius, + chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); + delete options.chamfer; + } + return Body.create(Common.extend({}, rectangle, options)); }; @@ -3651,12 +3973,18 @@ var Bodies = {}; x3 = x2 + x1; var trapezoid = { + label: 'Trapezoid Body', position: { x: x, y: y }, - render: { - path: 'L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0' - } + vertices: Vertices.fromPath('L 0 0 L ' + x1 + ' ' + (-height) + ' L ' + x2 + ' ' + (-height) + ' L ' + x3 + ' 0') }; + if (options.chamfer) { + var chamfer = options.chamfer; + trapezoid.vertices = Vertices.chamfer(trapezoid.vertices, chamfer.radius, + chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); + delete options.chamfer; + } + return Body.create(Common.extend({}, trapezoid, options)); }; @@ -3672,6 +4000,7 @@ var Bodies = {}; */ Bodies.circle = function(x, y, radius, options, maxSides) { options = options || {}; + options.label = 'Circle Body'; // approximate circles with polygons until true circles implemented in SAT @@ -3717,12 +4046,18 @@ var Bodies = {}; } var polygon = { + label: 'Polygon Body', position: { x: x, y: y }, - render: { - path: path - } + vertices: Vertices.fromPath(path) }; + if (options.chamfer) { + var chamfer = options.chamfer; + polygon.vertices = Vertices.chamfer(polygon.vertices, chamfer.radius, + chamfer.quality, chamfer.qualityMin, chamfer.qualityMax); + delete options.chamfer; + } + return Body.create(Common.extend({}, polygon, options)); }; @@ -3757,7 +4092,7 @@ var Composites = {}; * @return {composite} A new composite containing objects created in the callback */ Composites.stack = function(xx, yy, columns, rows, columnGap, rowGap, callback) { - var stack = Composite.create(), + var stack = Composite.create({ label: 'Stack' }), x = xx, y = yy, lastBody, @@ -3827,6 +4162,8 @@ var Composites = {}; Composite.addConstraint(composite, Constraint.create(constraint)); } + + composite.label += ' Chain'; return composite; }; @@ -3876,6 +4213,8 @@ var Composites = {}; } } } + + composite.label += ' Mesh'; return composite; }; @@ -3931,7 +4270,7 @@ var Composites = {}; * @return {composite} A new composite newtonsCradle body */ Composites.newtonsCradle = function(xx, yy, number, size, length) { - var newtonsCradle = Composite.create(); + var newtonsCradle = Composite.create({ label: 'Newtons Cradle' }); for (var i = 0; i < number; i++) { var separation = 1.9, @@ -3963,8 +4302,14 @@ var Composites = {}; 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 car = Composite.create({ label: 'Car' }), + body = Bodies.trapezoid(xx, yy, width, height, 0.3, { + groupId: groupId, + friction: 0.01, + chamfer: { + radius: 10 + } + }); var wheelA = Bodies.circle(xx + wheelAOffset, yy + wheelYOffset, wheelSize, { groupId: groupId, @@ -4028,6 +4373,8 @@ var Composites = {}; Composites.mesh(softBody, columns, rows, crossBrace, constraintOptions); + softBody.label = 'Soft Body'; + return softBody; }; @@ -4125,7 +4472,10 @@ var Bounds = {}; min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }; - Bounds.update(bounds, vertices); + + if (vertices) + Bounds.update(bounds, vertices); + return bounds; }; @@ -4188,6 +4538,35 @@ var Bounds = {}; return (boundsA.min.x <= boundsB.max.x && boundsA.max.x >= boundsB.min.x && boundsA.max.y >= boundsB.min.y && boundsA.min.y <= boundsB.max.y); }; + + /** + * Translates the bounds by the given vector + * @method translate + * @param {bounds} bounds + * @param {vector} vector + */ + Bounds.translate = function(bounds, vector) { + bounds.min.x += vector.x; + bounds.max.x += vector.x; + bounds.min.y += vector.y; + bounds.max.y += vector.y; + }; + + /** + * Shifts the bounds to the given position + * @method shift + * @param {bounds} bounds + * @param {vector} position + */ + Bounds.shift = function(bounds, position) { + var deltaX = bounds.max.x - bounds.min.x, + deltaY = bounds.max.y - bounds.min.y; + + bounds.min.x = position.x; + bounds.max.x = position.x + deltaX; + bounds.min.y = position.y; + bounds.max.y = position.y + deltaY; + }; })(); @@ -4361,6 +4740,17 @@ var Vector = {}; return { x: -vector.x, y: -vector.y }; }; + /** + * Returns the angle in radians between the two vectors relative to the x-axis + * @method angle + * @param {vector} vectorA + * @param {vector} vectorB + * @return {number} The angle in radians + */ + Vector.angle = function(vectorA, vectorB) { + return Math.atan2(vectorB.y - vectorA.y, vectorB.x - vectorA.x); + }; + })(); ; // End src/geometry/Vector.js @@ -4418,23 +4808,30 @@ var Vertices = {}; * @return {vector} The centre point */ Vertices.centre = function(vertices) { - var cx = 0, cy = 0; + var area = Vertices.area(vertices, true), + centre = { x: 0, y: 0 }, + cross, + temp, + j; for (var i = 0; i < vertices.length; i++) { - cx += vertices[i].x; - cy += vertices[i].y; + j = (i + 1) % vertices.length; + cross = Vector.cross(vertices[i], vertices[j]); + temp = Vector.mult(Vector.add(vertices[i], vertices[j]), cross); + centre = Vector.add(centre, temp); } - return { x: cx / vertices.length, y: cy / vertices.length }; + return Vector.div(centre, 6 * area); }; /** * Description * @method area * @param {vertices} vertices + * @param {bool} signed * @return {number} The area */ - Vertices.area = function(vertices) { + Vertices.area = function(vertices, signed) { var area = 0, j = vertices.length - 1; @@ -4443,6 +4840,9 @@ var Vertices = {}; j = i; } + if (signed) + return area / 2; + return Math.abs(area) / 2; }; @@ -4537,277 +4937,112 @@ var Vertices = {}; return true; }; + /** + * Scales the vertices from a point (default is centre) + * @method scale + * @param {vertices} vertices + * @param {number} scaleX + * @param {number} scaleY + * @param {vector} point + */ + Vertices.scale = function(vertices, scaleX, scaleY, point) { + if (scaleX === 1 && scaleY === 1) + return vertices; + + point = point || Vertices.centre(vertices); + + var vertex, + delta; + + for (var i = 0; i < vertices.length; i++) { + vertex = vertices[i]; + delta = Vector.sub(vertex, point); + vertices[i].x = point.x + delta.x * scaleX; + vertices[i].y = point.y + delta.y * scaleY; + } + + return vertices; + }; + + /** + * Chamfers a set of vertices by giving them rounded corners, returns a new set of vertices. + * The radius parameter is a single number or an array to specify the radius for each vertex. + * @method chamfer + * @param {vertices} vertices + * @param {number[]} radius + * @param {number} quality + * @param {number} qualityMin + * @param {number} qualityMax + */ + Vertices.chamfer = function(vertices, radius, quality, qualityMin, qualityMax) { + radius = radius || [8]; + + if (!radius.length) + radius = [radius]; + + // quality defaults to -1, which is auto + quality = (typeof quality !== 'undefined') ? quality : -1; + qualityMin = qualityMin || 2; + qualityMax = qualityMax || 14; + + var centre = Vertices.centre(vertices), + newVertices = []; + + for (var i = 0; i < vertices.length; i++) { + var prevVertex = vertices[i - 1 >= 0 ? i - 1 : vertices.length - 1], + vertex = vertices[i], + nextVertex = vertices[(i + 1) % vertices.length], + currentRadius = radius[i < radius.length ? i : radius.length - 1]; + + if (currentRadius === 0) { + newVertices.push(vertex); + continue; + } + + var prevNormal = Vector.normalise({ + x: vertex.y - prevVertex.y, + y: prevVertex.x - vertex.x + }); + + var nextNormal = Vector.normalise({ + x: nextVertex.y - vertex.y, + y: vertex.x - nextVertex.x + }); + + var diagonalRadius = Math.sqrt(2 * Math.pow(currentRadius, 2)), + radiusVector = Vector.mult(Common.clone(prevNormal), currentRadius), + midNormal = Vector.normalise(Vector.mult(Vector.add(prevNormal, nextNormal), 0.5)), + scaledVertex = Vector.sub(vertex, Vector.mult(midNormal, diagonalRadius)); + + var precision = quality; + + if (quality === -1) { + // automatically decide precision + precision = Math.pow(currentRadius, 0.32) * 1.75; + } + + precision = Common.clamp(precision, qualityMin, qualityMax); + + // use an even value for precision, more likely to reduce axes by using symmetry + if (precision % 2 === 1) + precision += 1; + + var alpha = Math.acos(Vector.dot(prevNormal, nextNormal)), + theta = alpha / precision; + + for (var j = 0; j < precision; j++) { + newVertices.push(Vector.add(Vector.rotate(radiusVector, theta * j), scaledVertex)); + } + } + + return newVertices; + }; + })(); ; // End src/geometry/Vertices.js -// Begin src/render/Gui.js - -/** -* See [Demo.js](https://github.com/liabru/matter-js/blob/master/demo/js/Demo.js) -* and [DemoMobile.js](https://github.com/liabru/matter-js/blob/master/demo/js/DemoMobile.js) for usage examples. -* -* @class Gui -*/ - -var Gui = {}; - -(function() { - - /** - * Description - * @method create - * @param {engine} engine - * @param {object} options - * @return {gui} A container for a configured dat.gui - */ - Gui.create = function(engine, options) { - var _datGuiSupported = window.dat && window.localStorage, - _serializer; - - if (!_datGuiSupported) { - console.log("Could not create GUI. Check dat.gui library is loaded first."); - return; - } - - var datGui = new dat.GUI(options); - - if (Resurrect) { - _serializer = new Resurrect({ prefix: '$' }); - _serializer.parse = _serializer.resurrect; - } else { - _serializer = JSON; - } - - var gui = { - datGui: datGui, - amount: 1, - size: 40, - sides: 4, - density: 0.001, - restitution: 0, - friction: 0.1, - frictionAir: 0.01, - renderer: 'canvas' - }; - - 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.add(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); - - // clear scene graph (if defined in controller) - var renderController = engine.render.controller; - if (renderController.clear) - renderController.clear(engine.render); - - /** - * Fired after the gui's clear button pressed - * - * @event clear - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(gui, 'clear'); - }, - - save: function() { - if (localStorage && _serializer) { - localStorage.setItem('world', _serializer.stringify(engine.world)); - } - - /** - * Fired after the gui's save button pressed - * - * @event save - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(gui, 'save'); - }, - - load: function() { - var loadedWorld; - - if (localStorage && _serializer) { - loadedWorld = _serializer.parse(localStorage.getItem('world')); - } - - if (loadedWorld) { - Engine.merge(engine, { world: loadedWorld }); - } - - /** - * Fired after the gui's load button pressed - * - * @event load - * @param {} event An event object - * @param {} event.source The source object of the event - * @param {} event.name The name of the event - */ - Events.trigger(gui, 'load'); - } - }; - - var metrics = datGui.addFolder('Metrics'); - metrics.add(engine.timing, 'fps').listen(); - - if (engine.metrics.extended) { - 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.add(engine.metrics, 'narrowReuse').listen(); - metrics.close(); - } else { - metrics.open(); - } - - 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']) - .onFinishChange(function(value) { - Composite.setModified(engine.world, true, false, false); - }); - - 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(gui, 'renderer', ['canvas', 'webgl']) - .onFinishChange(function(value) { - var controller; - - if (value === 'canvas') - controller = Render; - - if (value === 'webgl') - controller = RenderPixi; - - // remove old canvas - engine.render.element.removeChild(engine.render.canvas); - - // create new renderer using the same options object - var options = engine.render.options; - - engine.render = controller.create({ - element: engine.render.element, - options: options - }); - - engine.render.options = options; - - // bind the mouse to the new canvas - Mouse.setElement(engine.input.mouse, engine.render.canvas); - }); - - render.add(engine.render.options, 'wireframes'); - render.add(engine.render.options, 'showDebug'); - render.add(engine.render.options, 'showPositions'); - render.add(engine.render.options, 'showBroadphase'); - render.add(engine.render.options, 'showBounds'); - render.add(engine.render.options, 'showVelocity'); - render.add(engine.render.options, 'showCollisions'); - render.add(engine.render.options, 'showAxes'); - render.add(engine.render.options, 'showAngleIndicator'); - render.add(engine.render.options, 'showSleeping'); - render.add(engine.render.options, 'showIds'); - render.add(engine.render.options, 'showShadows'); - render.add(engine.render.options, 'enabled'); - render.open(); - - //datGui.remember(world) - - return gui; - }; - - /** - * Description - * @method update - * @param {gui} gui - * @param {datGui} datGui - */ - Gui.update = function(gui, datGui) { - var i; - datGui = datGui || gui.datGui; - - for (i in datGui.__folders) { - Gui.update(gui, datGui.__folders[i]); - } - - for (i in datGui.__controllers) { - var controller = datGui.__controllers[i]; - if (controller.updateDisplay) - controller.updateDisplay(); - } - }; - - /** - * Description - * @method closeAll - * @param {gui} gui - */ - Gui.closeAll = function(gui) { - var datGui = gui.datGui; - - for (var i in datGui.__folders) { - datGui.__folders[i].close(); - } - }; - -})(); - -; // End src/render/Gui.js - - // Begin src/render/Render.js /** @@ -4840,6 +5075,7 @@ var Render = {}; height: 600, background: '#fafafa', wireframeBackground: '#222', + hasBounds: false, enabled: true, wireframes: true, showSleeping: true, @@ -4862,6 +5098,17 @@ var Render = {}; render.context = render.canvas.getContext('2d'); render.textures = {}; + render.bounds = render.bounds || { + min: { + x: 0, + y: 0 + }, + max: { + x: render.options.width, + y: render.options.height + } + }; + Render.setBackground(render, render.options.background); if (Common.isElement(render.element)) { @@ -4913,8 +5160,10 @@ var Render = {}; canvas = render.canvas, context = render.context, options = render.options, - bodies = Composite.allBodies(world), - constraints = Composite.allConstraints(world), + allBodies = Composite.allBodies(world), + allConstraints = Composite.allConstraints(world), + bodies = [], + constraints = [], i; if (options.wireframes) { @@ -4929,8 +5178,45 @@ var Render = {}; context.fillRect(0, 0, canvas.width, canvas.height); context.globalCompositeOperation = 'source-over'; - /*if (options.showShadows && !options.wireframes) - Render.bodyShadows(engine, bodies, context);*/ + // handle bounds + var boundsWidth = render.bounds.max.x - render.bounds.min.x, + boundsHeight = render.bounds.max.y - render.bounds.min.y, + boundsScaleX = boundsWidth / render.options.width, + boundsScaleY = boundsHeight / render.options.height; + + if (options.hasBounds) { + // filter out bodies that are not in view + for (i = 0; i < allBodies.length; i++) { + var body = allBodies[i]; + if (Bounds.overlaps(body.bounds, render.bounds)) + bodies.push(body); + } + + // filter out constraints that are not in view + for (i = 0; i < allConstraints.length; i++) { + var constraint = allConstraints[i], + bodyA = constraint.bodyA, + bodyB = constraint.bodyB, + pointAWorld = constraint.pointA, + pointBWorld = constraint.pointB; + + if (bodyA) pointAWorld = Vector.add(bodyA.position, constraint.pointA); + if (bodyB) pointBWorld = Vector.add(bodyB.position, constraint.pointB); + + if (!pointAWorld || !pointBWorld) + continue; + + if (Bounds.contains(render.bounds, pointAWorld) || Bounds.contains(render.bounds, pointBWorld)) + constraints.push(constraint); + } + + // transform the view + context.scale(1 / boundsScaleX, 1 / boundsScaleY); + context.translate(-render.bounds.min.x, -render.bounds.min.y); + } else { + constraints = allConstraints; + bodies = allBodies; + } if (!options.wireframes || (engine.enableSleeping && options.showSleeping)) { // fully featured rendering of bodies @@ -4965,6 +5251,11 @@ var Render = {}; if (options.showDebug) Render.debug(engine, context); + + if (options.hasBounds) { + // revert view transforms + context.setTransform(1, 0, 0, 1, 0, 0); + } }; /** @@ -5500,6 +5791,92 @@ var Render = {}; c.stroke(); }; + /** + * Description + * @method inspector + * @param {inspector} inspector + * @param {RenderingContext} context + */ + Render.inspector = function(inspector, context) { + var engine = inspector.engine, + mouse = engine.input.mouse, + selected = inspector.selected, + c = context, + render = engine.render, + options = render.options, + bounds; + + if (options.hasBounds) { + var boundsWidth = render.bounds.max.x - render.bounds.min.x, + boundsHeight = render.bounds.max.y - render.bounds.min.y, + boundsScaleX = boundsWidth / render.options.width, + boundsScaleY = boundsHeight / render.options.height; + + context.scale(1 / boundsScaleX, 1 / boundsScaleY); + context.translate(-render.bounds.min.x, -render.bounds.min.y); + } + + for (var i = 0; i < selected.length; i++) { + var item = selected[i].data; + + context.translate(0.5, 0.5); + context.lineWidth = 1; + context.strokeStyle = 'rgba(255,165,0,0.9)'; + context.setLineDash([1,2]); + + switch (item.type) { + + case 'body': + + // render body selections + bounds = item.bounds; + context.beginPath(); + context.rect(Math.floor(bounds.min.x - 3), Math.floor(bounds.min.y - 3), + Math.floor(bounds.max.x - bounds.min.x + 6), Math.floor(bounds.max.y - bounds.min.y + 6)); + context.closePath(); + context.stroke(); + + break; + + case 'constraint': + + // render constraint selections + var point = item.pointA; + if (item.bodyA) + point = item.pointB; + context.beginPath(); + context.arc(point.x, point.y, 10, 0, 2 * Math.PI); + context.closePath(); + context.stroke(); + + break; + + } + + context.setLineDash([0]); + context.translate(-0.5, -0.5); + } + + // render selection region + if (inspector.selectStart !== null) { + context.translate(0.5, 0.5); + context.lineWidth = 1; + context.strokeStyle = 'rgba(255,165,0,0.6)'; + context.fillStyle = 'rgba(255,165,0,0.1)'; + bounds = inspector.selectBounds; + context.beginPath(); + context.rect(Math.floor(bounds.min.x), Math.floor(bounds.min.y), + Math.floor(bounds.max.x - bounds.min.x), Math.floor(bounds.max.y - bounds.min.y)); + context.closePath(); + context.stroke(); + context.fill(); + context.translate(-0.5, -0.5); + } + + if (options.hasBounds) + context.setTransform(1, 0, 0, 1, 0, 0); + }; + /** * Description * @method _createCanvas @@ -5953,10 +6330,10 @@ Matter.Axes = Axes; Matter.Bounds = Bounds; Matter.Vector = Vector; Matter.Vertices = Vertices; -Matter.Gui = Gui; Matter.Render = Render; Matter.RenderPixi = RenderPixi; Matter.Events = Events; +Matter.Query = Query; // CommonJS module if (typeof exports !== 'undefined') { diff --git a/demo/js/lib/matter-tools/matter-tools-dev.js b/demo/js/lib/matter-tools/matter-tools-dev.js index 27e0dcb..65438a3 100644 --- a/demo/js/lib/matter-tools/matter-tools-dev.js +++ b/demo/js/lib/matter-tools/matter-tools-dev.js @@ -1,12 +1,12 @@ /** -* matter-tools-dev.min.js 0.5.0-dev 2014-05-01 +* matter-tools-dev.min.js 0.5.0-dev 2014-05-04 * https://github.com/liabru/matter-tools * License: MIT */ (function() { var MatterTools = {}; - var Engine = Matter.Engine, World = Matter.World, Bodies = Matter.Bodies, Body = Matter.Body, Composite = Matter.Composite, Composites = Matter.Composites, Common = Matter.Common, Constraint = Matter.Constraint, Events = Matter.Events, Bounds = Matter.Bounds, Vector = Matter.Vector, Vertices = Matter.Vertices, MouseConstraint = Matter.MouseConstraint, Query = Matter.Query; + var Engine = Matter.Engine, World = Matter.World, Bodies = Matter.Bodies, Body = Matter.Body, Composite = Matter.Composite, Composites = Matter.Composites, Common = Matter.Common, Constraint = Matter.Constraint, Events = Matter.Events, Bounds = Matter.Bounds, Vector = Matter.Vector, Vertices = Matter.Vertices, MouseConstraint = Matter.MouseConstraint, Render = Matter.Render, RenderPixi = Matter.RenderPixi, Mouse = Matter.Mouse, Query = Matter.Query; var Gui = {}; (function() { Gui.create = function(engine, options) { @@ -30,7 +30,8 @@ x:0, y:0 }, - renderer:"canvas" + renderer:"canvas", + chamfer:0 }; if (Resurrect) { gui.serializer = new Resurrect({ @@ -72,7 +73,6 @@ Gui.serialise = function(serializer, object, indent) { indent = indent || 0; return serializer.stringify(object, function(key, value) { - if (key === "path") return undefined; if (!/^#/.exec(key) && typeof value === "number") { var fixed = parseFloat(value.toFixed(3)); if (fixed === 0 && value !== 0) return value; @@ -81,6 +81,11 @@ return value; }, indent); }; + Gui.clone = function(serializer, object) { + var clone = serializer.parse(Gui.serialise(serializer, object)); + clone.id = Common.nextId(); + return clone; + }; var _initDatGui = function(gui) { var engine = gui.engine, datGui = gui.datGui; var funcs = { @@ -126,6 +131,7 @@ controls.add(gui, "friction", 0, 1).step(.05); controls.add(gui, "frictionAir", 0, gui.frictionAir * 10).step(gui.frictionAir / 10); controls.add(gui, "restitution", 0, 1).step(.1); + controls.add(gui, "chamfer", 0, 30).step(2); controls.add(funcs, "addBody"); controls.open(); var worldGui = datGui.addFolder("World"); @@ -188,6 +194,11 @@ frictionAir:gui.frictionAir, restitution:gui.restitution }; + if (gui.chamfer && gui.sides > 2) { + options.chamfer = { + radius:gui.chamfer + }; + } for (var i = 0; i < gui.amount; i++) { World.add(engine.world, Bodies.polygon(gui.offset.x + 120 + i * gui.size + i * 50, gui.offset.y + 200, gui.sides, gui.size, options)); } @@ -229,6 +240,7 @@ hasTransitions:_isWebkit ? true :false, bodyClass:"", exportIndent:0, + clipboard:[], controls:{ container:null, worldTree:null @@ -303,7 +315,9 @@ help += "[shift + space] pause or play simulation.\n"; help += "[right click] and drag on empty space to select a region.\n"; help += "[right click] and drag on an object to move it.\n"; - help += "[right click + shift] and drag to move whole selection.\n"; + help += "[right click + shift] and drag to move whole selection.\n\n"; + help += "[ctrl-c] to copy selected world objects.\n"; + help += "[ctrl-v] to paste copied world objects to mouse position.\n"; help += "[del] or [backspace] delete selected objects.\n\n"; help += "[shift + s] scale-xy selected objects with mouse or arrows.\n"; help += "[shift + s + d] scale-x selected objects with mouse or arrows.\n"; @@ -353,6 +367,12 @@ _key("backspace", function() { _deleteSelectedObjects(inspector); }); + _key("ctrl+c", function() { + _copySelectedObjects(inspector); + }); + _key("ctrl+v", function() { + _pasteSelectedObjects(inspector); + }); $(document).unbind("keydown").bind("keydown", function(event) { var doPrevent = false; if (event.keyCode === 8) { @@ -609,6 +629,30 @@ Composite.remove(inspector.root, objects, true); _setSelectedObjects(inspector, []); }; + var _copySelectedObjects = function(inspector) { + inspector.clipboard.length = 0; + for (var i = 0; i < inspector.selected.length; i++) { + var object = inspector.selected[i].data; + if (object.type !== "body") continue; + inspector.clipboard.push(object); + } + }; + var _pasteSelectedObjects = function(inspector) { + var objects = [], worldTree = inspector.controls.worldTree.data("jstree"); + for (var i = 0; i < inspector.clipboard.length; i++) { + var object = inspector.clipboard[i], clone = Gui.clone(inspector.serializer, object); + Body.translate(clone, { + x:50, + y:50 + }); + var node = worldTree.get_node(object.type + "_" + object.id, false), compositeId = node.data.compositeId, composite = Composite.get(inspector.engine.world, compositeId, "composite"); + Composite.add(composite, clone); + objects.push(clone); + } + setTimeout(function() { + _setSelectedObjects(inspector, objects); + }, 200); + }; var _updateSelectedMouseDownOffset = function(inspector) { var selected = inspector.selected, mouse = inspector.engine.input.mouse, mousePosition = _getMousePosition(inspector), item, data; for (var i = 0; i < selected.length; i++) { diff --git a/demo/mobile.html b/demo/mobile.html index 35e3314..3615820 100644 --- a/demo/mobile.html +++ b/demo/mobile.html @@ -7,7 +7,7 @@ - + Matter.js Physics Engine Mobile Demo diff --git a/package.json b/package.json index 54346cc..44c7cd2 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "Matter", - "version": "0.7.0", + "version": "0.8.0", "license": "MIT", "homepage": "http://brm.io/matter-js/", "author": "Liam Brummitt (http://brm.io/)", "description": "a 2D rigid body physics engine for the web", - "main": "build/matter-0.7.0.min.js", + "main": "build/matter-0.8.0.min.js", "repository":{ "type" : "git", "url" : "https://github.com/liabru/matter-js.git"