Car Demo flashrod Follow 2011-02-22 10:32:13 License: see code comments Fork0 Fav10 View1512 Play Stop Reload Fullscreen Smart Phone Readme JavaScript 1592 lines HTML 6 lines CSS 1 lines Car Demo /* Copyright (c) 2006, 2007 Alec Cove Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* ported by flashrod 2008 ActionScript ported by flashrod 2011 JavaScript */ var Util = { concat: function(d, s) { for (var key in s) { if (s.hasOwnProperty(key)) { var getter = s.__lookupGetter__(key); if (getter) { d.__defineGetter__(key, getter); } var setter = s.__lookupSetter__(key); if (setter) { d.__defineSetter__(key, setter); } if (!(getter || setter)) { d[key] = s[key]; } } } return d; } }; var Vector = function(x, y) { this.x = x || 0; this.y = y || 0; }; Vector.prototype = { setTo: function(x, y) { this.x = x; this.y = y; }, copy: function(v) { this.x = v.x; this.y = v.y; }, dot: function(v) { return this.x * v.x + this.y * v.y; }, cross: function(v) { return this.x * v.y - this.y * v.x; }, plus: function(v) { return new Vector(this.x + v.x, this.y + v.y); }, plusEquals: function(v) { this.x += v.x; this.y += v.y; return this; }, minus: function(v) { return new Vector(this.x - v.x, this.y - v.y); }, minusEquals: function(v) { this.x -= v.x; this.y -= v.y; return this; }, mult: function(s) { return new Vector(this.x * s, this.y * s); }, multEquals: function(s) { this.x *= s; this.y *= s; return this; }, times: function(v) { return new Vector(this.x * v.x, this.y * v.y); }, divEquals: function(s) { if (s == 0) { s = 0.0001; } this.x /= s; this.y /= s; return this; }, magnitude: function() { return Math.sqrt(this.x * this.x + this.y * this.y); }, distance: function(v) { var delta = this.minus(v); return delta.magnitude(); }, normalize: function() { var m = this.magnitude(); if (m == 0) { m = 0.0001; } return this.mult(1 / m); }, toString: function() { return "(" + this.x + ", " + this.y + ")"; } }; var Particle = function(x, y, fixed, mass, elasticity, friction) { this.samp = new Vector(0, 0); this.interval = { min:0, max:0 }; this.forces = new Vector(0, 0); this.collision = {vn: new Vector(0, 0), vt: new Vector(0, 0)}; this._collidable = true; this._multisample = 0; this.curr = new Vector(x, y); this.prev = new Vector(x, y); this._fixed = fixed; this._mass = mass; this._invMass = fixed ? 0 : 1 / mass; this._elasticity = elasticity; this._friction = friction; }; Particle.prototype = { get mass() { return this._mass; }, set mass(m) { if (m <= 0) { throw new ArgumentError("mass may not be set <= 0"); } this._mass = m; this._invMass = this._fixed ? 0 : 1 / this._mass; }, get invMass() { return this._invMass; }, get elasticity() { return this._elasticity; }, set elasticity(k) { this._elasticity = k; }, get multisample() { return this._multisample; }, set multisample(m) { this._multisample = m; }, get friction() { return this._friction; }, set friction(f) { if (f < 0 || f > 1) throw new ArgumentError("Legal friction must be >= 0 and <=1"); this._friction = f; }, get fixed() { return this._fixed; }, set fixed(f) { this._fixed = f; }, get position() { return new Vector(this.curr.x, this.curr.y); }, set position(p) { this.curr.copy(p); this.prev.copy(p); }, get x() { return this.curr.x; }, set x(x) { this.prev.x = this.curr.x = x; }, get y() { return this.curr.y; }, set y(y) { this.prev.y = this.curr.y = y; }, get velocity() { return this.curr.minus(this.prev); }, set velocity(v) { this.prev = this.curr.minus(v); }, get collidable() { return this._collidable; }, set collidable(b) { this._collidable = b; }, addForce: function(f) { this.forces.plusEquals(f.mult(this.invMass)); }, addMasslessForce: function(f) { this.forces.plusEquals(f); }, update: function(dt2, engine) { if (this.fixed) { return; } this.addForce(engine.force); this.addMasslessForce(engine.gravity); var temp = new Vector(this.curr.x, this.curr.y); var nv = this.velocity.plus(this.forces.multEquals(dt2)); this.curr.plusEquals(nv.multEquals(engine.damping)); this.prev.copy(temp); this.forces.setTo(0, 0); }, getComponents: function(collisionNormal) { var vel = this.velocity; var vdotn = collisionNormal.dot(vel); this.collision.vn = collisionNormal.mult(vdotn); this.collision.vt = vel.minus(this.collision.vn); return this.collision; }, resolveCollision: function(mtd, vel, n, d, o, p) { this.curr.plusEquals(mtd); this.velocity = vel; }, updatePosition: function() {}, paint: function() {}, toString: function() { return "Particle()"; } }; var Circle = function(x, y, radius, fixed, mass, elasticity, friction) { fixed = fixed || false; mass = mass || 1; elasticity = elasticity || 0.3; friction = friction || 0; Particle.call(this, x, y, fixed, mass, elasticity, friction); this.radius = radius; }; Circle.prototype = Util.concat(new Particle(), { getProjection: function(axis) { var c = this.samp.dot(axis); this.interval.min = c - this.radius; this.interval.max = c + this.radius; return this.interval; }, getIntervalX: function() { this.interval.min = this.curr.x - this.radius; this.interval.max = this.curr.x + this.radius; return this.interval; }, getIntervalY: function() { this.interval.min = this.curr.y - this.radius; this.interval.max = this.curr.y + this.radius; return this.interval; }, toString: function() { return "Circle("+this.x+", "+this.y+", "+this.radius+")"; } }); var Rect = function(x, y, width, height, rotation, fixed, mass, elasticity, friction) { rotation = rotation || 0; fixed = fixed || false; mass = mass || 1; elasticity = elasticity || 0.3; friction = friction || 0; Particle.call(this, x, y, fixed, mass, elasticity, friction); this._axes = [new Vector(0, 0), new Vector(0, 0)]; this._extents = [width / 2, height / 2]; this.radian = rotation; }; Rect.prototype = Util.concat(new Particle(), { get radian() { return this._radian; }, set radian(t) { this._radian = t; this.setAxes(t); }, get angle() { return this.radian * (180 / Math.PI); }, set angle(a) { this.radian = a * (Math.PI / 180); }, get width() { return this._extents[0] * 2; }, set width(w) { this._extents[0] = w / 2; }, get height() { return this._extents[1] * 2; }, set height(h) { this._extents[1] = h / 2; }, get axes() { return this._axes; }, get extents() { return this._extents; }, getProjection: function(axis) { var radius = this._extents[0] * Math.abs(axis.dot(this._axes[0]))+ this._extents[1] * Math.abs(axis.dot(this._axes[1])); var c = this.samp.dot(axis); this.interval.min = c - radius; this.interval.max = c + radius; return this.interval; }, setAxes: function(t) { var s = Math.sin(t); var c = Math.cos(t); this._axes[0].x = c; this._axes[0].y = s; this._axes[1].x = -s; this._axes[1].y = c; }, toString: function() { return "Rect("+this.x+","+this.y+","+this.width+","+this.height+")"; } }); var Constraint = function(stiffness) { this.stiffness = stiffness; }; Constraint.prototype = { get collidableParticle() { return null; }, set collidableParticle(p) {}, isConnectedTo: function(p) { return false; }, resolve: function() {}, paint: function() {}, toString: function() { return "Constraint("+this.stiffness+")"; } }; var Spring = function(p1, p2, stiffness) { stiffness = stiffness || 0.5; Constraint.call(this, stiffness); if (p1 && p2) { this.p1 = p1; this.p2 = p2; this.checkParticlesLocation(); this._restLength = this.currLength; } this._scp = null; }; Spring.prototype = Util.concat(new Constraint(), { get radian() { var d = this.delta; return Math.atan2(d.y, d.x); }, get angle() { return this.radian * (180 / Math.PI); }, get center() { return (this.p1.curr.plus(this.p2.curr)).divEquals(2); }, set rectScale(s) { if (this._scp != null) { this._scp.rectScale = s; } }, get rectScale() { if (this._scp != null) { return this._scp.rectScale; } return NaN; }, get currLength() { return this.p1.curr.distance(this.p2.curr); }, get rectHeight() { if (this._scp != null) { return this._scp.rectHeight; } return NaN; }, set rectHeight(h) { if (this._scp != null) { this._scp.rectHeight = h; } }, get restLength() { return this._restLength; }, set restLength(r) { if (r <= 0) { throw new ArgumentError("restLength must be greater than 0"); } this._restLength = r; }, get fixedEndLimit() { if (this._scp != null) { return this._scp.fixedEndLimit; } return NaN; }, set fixedEndLimit(f) { if (this._scp != null) { this._scp.fixedEndLimit = f; } }, get collidableParticle() { return this._scp; }, set collidableParticle(p) { this._scp = p; }, isConnectedTo: function(p) { return (p == this.p1 || p == this.p2); }, get fixed() { return (this.p1.fixed && this.p2.fixed); }, get delta() { return this.p1.curr.minus(this.p2.curr); }, resolve: function() { if (this.p1.fixed && this.p2.fixed) { return; } var deltaLength = this.currLength; var diff = (deltaLength - this.restLength) / (deltaLength * (this.p1.invMass + this.p2.invMass)); var dmds = this.delta.mult(diff * this.stiffness); this.p1.curr.minusEquals(dmds.mult(this.p1.invMass)); this.p2.curr.plusEquals (dmds.mult(this.p2.invMass)); }, checkParticlesLocation: function() { if (this.p1.curr.x == this.p2.curr.x && this.p1.curr.y == this.p2.curr.y) { this.p2.curr.x += 0.0001; } }, paint: function() { if (this._scp != null) { this._scp.paint(); } }, toString: function() { return "Spring()"; } }); var Collection = function() { this.particles = []; this.constraints = []; }; Collection.prototype = { integrate: function(dt2, engine) { var a = this.particles; var l = a.length; for (var i = 0; i < l; ++i) { var p = a[i]; p.update(dt2, engine); } }, satisfyConstraints: function() { var a = this.constraints; var l = a.length; for (var i = 0; i < l; ++i) { var c = a[i]; c.resolve(); } }, checkInternalCollisions: function() { var a1 = this.particles; var l1 = a1.length; var a2 = this.constraints; var l2 = a2.length; for (var j = 0; j < l1; j++) { var pj = a1[j]; if (! pj.collidable) { continue; } for (var i = j + 1; i < l1; i++) { var pi = a1[i]; if (pi.collidable) { collisionTest(pj, pi); } } for (var k = 0; k < l2; ++k) { var c = a2[k]; var pk = c.collidableParticle; if (pk != null && ! c.isConnectedTo(pj)) { pk.updatePosition(); collisionTest(pj, pk); } } } }, checkCollisionsVsCollection: function(that) { var a1 = this.particles; var l1 = a1.length; var a2 = that.particles; var l2 = a2.length; var a3 = this.constraints; var l3 = a3.length; var a4 = that.constraints; var l4 = a4.length; for (var i1 = 0; i1 < l1; ++i1) { var pga = a1[i1]; if (! pga.collidable) { continue; } for (var i2 = 0; i2 < l2; ++i2) { var pgb = a2[i2]; if (pgb.collidable) { collisionTest(pga, pgb); } } for (var i4 = 0; i4 < l4; ++i4) { var cgb = a4[i4]; var pk = cgb.collidableParticle; if (pk != null && ! cgb.isConnectedTo(pga)) { pk.updatePosition(); collisionTest(pga, pk); } } } for (var i3 = 0; i3 < l3; ++i3) { var cga = a3[i3]; var pl = cga.collidableParticle; if (pl == null) { continue; } for (i2 = 0; i2 < l2; ++i2) { pgb = a2[i2]; if (pgb.collidable && !cga.isConnectedTo(pgb)) { pl.updatePosition(); collisionTest(pgb, pl); } } } }, paint: function() { var a1 = this.particles; var l1 = a1.length; for (var i1 = 0; i1 < l1; ++i1) { var p = a1[i1]; p.paint(); } var a2 = this.constraints; var l2 = a2.length; for (var i2 = 0; i2 < l2; ++i2) { var c = a2[i2]; c.paint(); } }, toString: function() { return "Collection()"; } }; var Composite = function() { Collection.call(this); }; Composite.prototype = Util.concat(new Collection(), { rotate: function(angleRadians, center) { var a = this.particles; var l = a.length; for (var i = 0; i < l; ++i) { var p = a[i]; var other = p.position; var radius = other.distance(center); var angle = Math.atan2(other.y - center.y, other.x - center.x) + angleRadians; p.x = (Math.cos(angle) * radius) + center.x; p.y = (Math.sin(angle) * radius) + center.y; } }, toString: function() { return "Composite()"; } }); var Group = function(collideInternal) { Collection.call(this); this.collideInternal = collideInternal || false; this.composites = []; this.collisionList = []; }; Group.prototype = Util.concat(new Collection(), { integrate: function(dt2, engine) { Collection.prototype.integrate.call(this, dt2, engine); var a = this.composites; var l = a.length; for (var i = 0; i < l; ++i) { var cmp = a[i]; cmp.integrate(dt2, engine); } }, satisfyConstraints: function() { Collection.prototype.satisfyConstraints.call(this); var a = this.composites; var l = a.length; for (var i = 0; i < l; ++i) { var cmp = a[i]; cmp.satisfyConstraints(); } }, checkCollisions: function() { if (this.collideInternal) { this.checkCollisionGroupInternal(); } var a = this.collisionList; var l = a.length; for (var i = 0; i < l; ++i) { var g = a[i]; this.checkCollisionVsGroup(g); } }, checkCollisionGroupInternal: function() { this.checkInternalCollisions(); var clen = this.composites.length; for (var j = 0; j < clen; j++) { var cj = this.composites[j]; cj.checkCollisionsVsCollection(this); for (var i = j + 1; i < clen; i++) { var ci = this.composites[i]; cj.checkCollisionsVsCollection(ci); } } }, checkCollisionVsGroup: function(that) { this.checkCollisionsVsCollection(that); var a1 = this.composites; var l1 = a1.length; var a2 = that.composites; var l2 = a2.length; for (var i = 0; i < l1; ++i) { var c = a1[i]; c.checkCollisionsVsCollection(that); for (var j = 0; j < l2; ++j) { var gc = a2[j]; c.checkCollisionsVsCollection(gc); } } for (j = 0; j < l2; ++j) { gc = a2[j]; this.checkCollisionsVsCollection(gc); } }, paint: function() { Collection.prototype.paint.call(this); var a = this.composites; var l = a.length; for (var i = 0; i < l; ++i) { var c = a[i]; c.paint(); } }, toString: function() { return "Group()"; } }); function collisionTest(objA, objB) { if (objA.fixed && objB.fixed) { return; } if (objA.multisample == 0 && objB.multisample == 0) { normVsNorm(objA, objB); } else if (objA.multisample > 0 && objB.multisample == 0) { sampVsNorm(objA, objB); } else if (objB.multisample > 0 && objA.multisample == 0) { sampVsNorm(objB, objA); } else if (objA.multisample == objB.multisample) { sampVsSamp(objA, objB); } else { normVsNorm(objA, objB); } } function normVsNorm(objA, objB) { objA.samp.copy(objA.curr); objB.samp.copy(objB.curr); testTypes(objA, objB); } function sampVsNorm(objA, objB) { var s = 1 / (objA.multisample + 1); var t = s; objB.samp.copy(objB.curr); for (var i = 0; i <= objA.multisample; i++) { objA.samp.setTo(objA.prev.x + t * (objA.curr.x - objA.prev.x), objA.prev.y + t * (objA.curr.y - objA.prev.y)); if (testTypes(objA, objB)) { return; } t += s; } } function sampVsSamp(objA, objB) { var s = 1 / (objA.multisample + 1); var t = s; for (var i = 0; i <= objA.multisample; i++) { objA.samp.setTo(objA.prev.x + t * (objA.curr.x - objA.prev.x), objA.prev.y + t * (objA.curr.y - objA.prev.y)); objB.samp.setTo(objB.prev.x + t * (objB.curr.x - objB.prev.x), objB.prev.y + t * (objB.curr.y - objB.prev.y)); if (testTypes(objA, objB)) { return; } t += s; } } function testTypes(objA, objB) { if ((objA instanceof Circle) && (objB instanceof Circle)) { testCirclevsCircle(objA, objB); } else if ((objA instanceof Circle) && (objB instanceof Rect)) { testCirclevsOBB(objA, objB); } else if ((objA instanceof Rect) && (objB instanceof Circle)) { testOBBvsCircle(objA, objB); } else { testOBBvsOBB(objA, objB); } } function testOBBvsOBB(ra, rb) { var collisionNormal; var collisionDepth = Number.POSITIVE_INFINITY; for (var i = 0; i < 2; i++) { var axisA = ra.axes[i]; var depthA = testIntervals(ra.getProjection(axisA), rb.getProjection(axisA)); if (depthA == 0) { return false; } var axisB = rb.axes[i]; var depthB = testIntervals(ra.getProjection(axisB), rb.getProjection(axisB)); if (depthB == 0) { return false; } var absA = Math.abs(depthA); var absB = Math.abs(depthB); if (absA < Math.abs(collisionDepth) || absB < Math.abs(collisionDepth)) { var altb = absA < absB; collisionNormal = altb ? axisA : axisB; collisionDepth = altb ? depthA : depthB; } } resolveParticleParticle(ra, rb, collisionNormal, collisionDepth); return true; } function testCirclevsOBB(ca, ra) { return testOBBvsCircle(ra, ca); } function testOBBvsCircle(ra, ca) { var collisionNormal; var collisionDepth = Number.POSITIVE_INFINITY; var depths = new Array(2); // first go through the axes of the rectangle for (var i = 0; i < 2; i++) { var boxAxis = ra.axes[i]; var depth = testIntervals(ra.getProjection(boxAxis), ca.getProjection(boxAxis)); if (depth == 0) { return false; } if (Math.abs(depth) < Math.abs(collisionDepth)) { collisionNormal = boxAxis; collisionDepth = depth; } depths[i] = depth; } // determine if the circle's center is in a vertex region var r = ca.radius; if (Math.abs(depths[0]) < r && Math.abs(depths[1]) < r) { var vertex = closestVertexOnOBB(ca.samp, ra); // get the distance from the closest vertex on rect to circle center collisionNormal = vertex.minus(ca.samp); var mag = collisionNormal.magnitude(); collisionDepth = r - mag; if (collisionDepth > 0) { // there is a collision in one of the vertex regions collisionNormal.divEquals(mag); } else { // ra is in vertex region, but is not colliding return false; } } resolveParticleParticle(ra, ca, collisionNormal, collisionDepth); return true; } function testCirclevsCircle(ca, cb) { var depthX = testIntervals(ca.getIntervalX(), cb.getIntervalX()); if (depthX == 0) { return false; } var depthY = testIntervals(ca.getIntervalY(), cb.getIntervalY()); if (depthY == 0) { return false; } var collisionNormal = ca.samp.minus(cb.samp); var mag = collisionNormal.magnitude(); var collisionDepth = (ca.radius + cb.radius) - mag; if (collisionDepth > 0) { collisionNormal.divEquals(mag); resolveParticleParticle(ca, cb, collisionNormal, collisionDepth); return true; } return false; } function testIntervals(intervalA, intervalB) { if (intervalA.max < intervalB.min) { return 0; } if (intervalB.max < intervalA.min) { return 0; } var lenA = intervalB.max - intervalA.min; var lenB = intervalB.min - intervalA.max; return (Math.abs(lenA) < Math.abs(lenB)) ? lenA : lenB; } function closestVertexOnOBB(p, r) { var d = p.minus(r.samp); var q = new Vector(r.samp.x, r.samp.y); for (var i = 0; i < 2; i++) { var dist = d.dot(r.axes[i]); if (dist >= 0) { dist = r.extents[i]; } else if (dist < 0) { dist = -r.extents[i]; } q.plusEquals(r.axes[i].mult(dist)); } return q; } function clamp01(n) { if (n < 0) return 0; if (n > 1) return 1; return n; } // thanks to Jim Bonacci for changes using the inverse mass instead of mass function resolveParticleParticle(pa, pb, normal, depth) { // a collision has occured. set the current positions to sample locations pa.curr.copy(pa.samp); pb.curr.copy(pb.samp); var mtd = normal.mult(depth); var te = pa.elasticity + pb.elasticity; var sumInvMass = pa.invMass + pb.invMass; // the total friction in a collision is combined but clamped to [0,1] var tf = clamp01(1 - (pa.friction + pb.friction)); // get the collision, vn and vt var ca = pa.getComponents(normal); var cb = pb.getComponents(normal); // calculate the coefficient of restitution based on the mass, as the normal component var vnA = (cb.vn.mult((te + 1) * pa.invMass).plus(ca.vn.mult(pb.invMass - te * pa.invMass))).divEquals(sumInvMass); var vnB = (ca.vn.mult((te + 1) * pb.invMass).plus(cb.vn.mult(pa.invMass - te * pb.invMass))).divEquals(sumInvMass); // apply friction to the tangental component ca.vt.multEquals(tf); cb.vt.multEquals(tf); // scale the mtd by the ratio of the masses. heavier particles move less var mtdA = mtd.mult( pa.invMass / sumInvMass); var mtdB = mtd.mult(-pb.invMass / sumInvMass); // add the tangental component to the normal component for the new velocity vnA.plusEquals(ca.vt); vnB.plusEquals(cb.vt); if (! pa.fixed) { pa.resolveCollision(mtdA, vnA, normal, depth, -1, pb); } if (! pb.fixed) { pb.resolveCollision(mtdB, vnB, normal, depth, 1, pa); } } var Rim = function(r, mt, parent) { this.curr = new Vector(r, 0); this.prev = new Vector(0, 0); this.sp = 0; this.av = 0; this.maxTorque = mt; this.wr = r; this.parent = parent; }; Rim.prototype = { get speed() { return this.sp; }, set speed(s) { this.sp = s; }, get angularVelocity() { return this.av; }, set angularVelocity(s) { this.av = s; }, update: function(dt, engine) { //clamp torques to valid range this.sp = Math.max(-this.maxTorque, Math.min(this.maxTorque, this.sp + this.av)); //apply torque //this is the tangent vector at the rim particle var dx = -this.curr.y; var dy = this.curr.x; //normalize so we can scale by the rotational speed var len = Math.sqrt(dx * dx + dy * dy); dx /= len; dy /= len; this.curr.x += this.sp * dx; this.curr.y += this.sp * dy; var ox = this.prev.x; var oy = this.prev.y; var px = this.prev.x = this.curr.x; var py = this.prev.y = this.curr.y; this.curr.x += engine.damping * (px - ox); this.curr.y += engine.damping * (py - oy); // hold the rim particle in place var clen = Math.sqrt(this.curr.x * this.curr.x + this.curr.y * this.curr.y); var diff = (clen - this.wr) / clen; this.curr.x -= this.curr.x * diff; this.curr.y -= this.curr.y * diff; }, toString: function() { return "Rim()"; } }; var Wheel = function(x, y, radius, fixed, mass, elasticity, friction, traction) { fixed = fixed || false; mass = mass || 1; elasticity = elasticity || 0.3; friction = friction || 0; traction = traction || 1; Circle.call(this, x, y, radius, fixed, mass, elasticity, friction); this.tan = new Vector(); this.normSlip = new Vector(); this.orientation = new Vector(); this.rim = new Rim(radius, 2, this); this.traction = traction; }; Wheel.prototype = Util.concat(new Circle(), { get speed() { return this.rim.speed; }, set speed(s) { this.rim.speed = s; }, get angularVelocity() { return this.rim.angularVelocity; }, set angularVelocity(a) { this.rim.angularVelocity = a; }, get traction() { return 1 - this._traction; }, set traction(t) { this._traction = 1 - t; }, get radian() { this.orientation.setTo(this.rim.curr.x, this.rim.curr.y); return Math.atan2(this.orientation.y, this.orientation.x) + Math.PI; }, get angle() { return this.radian * (180 / Math.PI); }, update: function(dt, engine) { Circle.prototype.update.call(this, dt, engine); this.rim.update(dt, engine); }, resolveCollision: function(mtd, vel, n, d, o, p) { // review the o (order) need here - its a hack fix Circle.prototype.resolveCollision.call(this, mtd, vel, n, d, o, p); this.resolve(n.mult(Wheel.sign(d * o))); }, resolve: function(n) { // this is the tangent vector at the this.rim particle this.tan.setTo(-this.rim.curr.y, this.rim.curr.x); // normalize so we can scale by the rotational speed this.tan = this.tan.normalize(); // velocity of the wheel's surface var wheelSurfaceVelocity = this.tan.mult(this.rim.speed); // the velocity of the wheel's surface relative to the ground var combinedVelocity = this.velocity.plusEquals(wheelSurfaceVelocity); // the wheel's comb velocity projected onto the contact normal var cp = combinedVelocity.cross(n); // set the wheel's spinspeed to track the ground this.tan.multEquals(cp); this.rim.prev.copy(this.rim.curr.minus(this.tan)); // some of the wheel's torque is removed and converted into linear displacement var slipSpeed = (1 - this._traction) * this.rim.speed; this.normSlip.setTo(slipSpeed * n.y, slipSpeed * n.x); this.curr.plusEquals(this.normSlip); this.rim.speed = this.rim.speed * this._traction; }, toString: function() { return "Wheel()"; } }); Wheel.sign = function(val) { return (val < 0) ? -1 : 1; } var SpringRect = function(spring, rectHeight, rectScale, scaleToLength) { Rect.call(this, 0, 0, 0, 0, 0, false); if (spring) { this._spring = spring; this.p1 = spring.p1; this.p2 = spring.p2; } this.rectScale = rectScale || 1; this.rectHeight = rectHeight || 1; this.scaleToLength = scaleToLength || false; this.avgVelocity = new Vector(); this.lambda = new Vector(); this.rca = new Vector(); this.rcb = new Vector(); this.fixedEndLimit = 0; }; SpringRect.prototype = Util.concat(new Rect(), { get mass() { return (this.p1.mass + this.p2.mass) / 2; }, get elasticity() { return (this.p1.elasticity + this.p2.elasticity) / 2; }, get friction() { return (this.p1.friction + this.p2.friction) / 2; }, get velocity() { var p1v = this.p1.velocity; var p2v = this.p2.velocity; this.avgVelocity.setTo(((p1v.x + p2v.x) / 2), ((p1v.y + p2v.y) / 2)); return this.avgVelocity; }, get invMass() { if (this.p1.fixed && this.p2.fixed) { return 0; } return 1 / ((this.p1.mass + this.p2.mass) / 2); }, updatePosition: function() { var c = this._spring.center; this.curr.setTo(c.x, c.y); this.width = this.scaleToLength ? this._spring.currLength * this.rectScale : this._spring.restLength * this.rectScale; this.height = this.rectHeight; this.radian = this._spring.radian; }, resolveCollision: function(mtd, vel, n, d, o, p) { var t = this.getContactPointParam(p); var c1 = 1 - t; var c2 = t; // if one is fixed then move the other particle the entire way out of collision. // also, dispose of collisions at the sides of the scp. The higher the this.fixedEndLimit // value, the more of the scp not be effected by collision. if (this.p1.fixed) { if (c2 <= this.fixedEndLimit) { return; } this.lambda.setTo(mtd.x / c2, mtd.y / c2); this.p2.curr.plusEquals(this.lambda); this.p2.velocity = vel; } else if (this.p2.fixed) { if (c1 <= this.fixedEndLimit) { return; } this.lambda.setTo(mtd.x / c1, mtd.y / c1); this.p1.curr.plusEquals(this.lambda); this.p1.velocity = vel; // else both non fixed - move proportionally out of collision } else { var denom = (c1 * c1 + c2 * c2); if (denom == 0) { return; } this.lambda.setTo(mtd.x / denom, mtd.y / denom); this.p1.curr.plusEquals(this.lambda.mult(c1)); this.p2.curr.plusEquals(this.lambda.mult(c2)); // if collision is in the middle of SCP set the velocity of both end particles if (t == 0.5) { this.p1.velocity = vel; this.p2.velocity = vel; // otherwise change the velocity of the particle closest to contact } else { var corrParticle = (t < 0.5) ? this.p1 : this.p2; corrParticle.velocity = vel; } } }, closestParamPoint: function(c) { var ab = this.p2.curr.minus(this.p1.curr); var t = (ab.dot(c.minus(this.p1.curr))) / (ab.dot(ab)); return clamp01(t); }, getContactPointParam: function(p) { var t; if (p instanceof Circle) { t = this.closestParamPoint(p.curr); } else if (p instanceof Rect) { // go through the sides of the colliding rectangle as line segments var shortestIndex; var paramList = new Array(4); var shortestDistance = Number.POSITIVE_INFINITY; for (var i = 0; i < 4; i++) { this.setCorners(p, i); // check for closest points on SCP to side of rectangle var d = this.closestPtSegmentSegment(); if (d < shortestDistance) { shortestDistance = d; shortestIndex = i; paramList[i] = this.s; } } t = paramList[shortestIndex]; } return t; }, setCorners: function(r, i) { var rx = r.curr.x; var ry = r.curr.y; var axes = r.axes; var extents = r.extents; var ae0_x = axes[0].x * extents[0]; var ae0_y = axes[0].y * extents[0]; var ae1_x = axes[1].x * extents[1]; var ae1_y = axes[1].y * extents[1]; var emx = ae0_x - ae1_x; var emy = ae0_y - ae1_y; var epx = ae0_x + ae1_x; var epy = ae0_y + ae1_y; if (i == 0) { // 0 and 1 this.rca.x = rx - epx; this.rca.y = ry - epy; this.rcb.x = rx + emx; this.rcb.y = ry + emy; } else if (i == 1) { // 1 and 2 this.rca.x = rx + emx; this.rca.y = ry + emy; this.rcb.x = rx + epx; this.rcb.y = ry + epy; } else if (i == 2) { // 2 and 3 this.rca.x = rx + epx; this.rca.y = ry + epy; this.rcb.x = rx - emx; this.rcb.y = ry - emy; } else if (i == 3) { // 3 and 0 this.rca.x = rx - emx; this.rca.y = ry - emy; this.rcb.x = rx - epx; this.rcb.y = ry - epy; } }, closestPtSegmentSegment: function() { var pp1 = this.p1.curr; var pq1 = this.p2.curr; var pp2 = this.rca; var pq2 = this.rcb; var d1 = pq1.minus(pp1); var d2 = pq2.minus(pp2); var r = pp1.minus(pp2); var t; var a = d1.dot(d1); var e = d2.dot(d2); var f = d2.dot(r); var c = d1.dot(r); var b = d1.dot(d2); var denom = a * e - b * b; if (denom != 0.0) { this.s = clamp01((b * f - c * e) / denom); } else { this.s = 0.5; // give the midpoint for parallel lines } t = (b * this.s + f) / e; if (t < 0) { t = 0; this.s = clamp01(-c / a); } else if (t > 0) { t = 1; this.s = clamp01((b - c) / a); } var c1 = pp1.plus(d1.mult(this.s)); var c2 = pp2.plus(d2.mult(t)); var c1mc2 = c1.minus(c2); return c1mc2.dot(c1mc2); }, toString: function() { return "SpringRect()"; } }); /** * Initializes the engine. You must call this method prior to adding * any particles or constraints. * * @param dt The delta time value for the engine. This * parameter can be used -- in conjunction with speed at which * <code>Engine.step()</code> is called -- to change the speed * of the simulation. Typical values are 1/3 or 1/4. Lower * values result in slower, but more accurate simulations, and * higher ones result in faster, less accurate ones. Note * that this only applies to the forces added to particles. If * you do not add any forces, the <code>dt</code> value won't * matter. */ var Engine = function(dt) { dt = dt || 0.25; this.force = new Vector(0, 0); this.gravity = new Vector(0, 0); this.groups = []; this._timeStep = dt * dt; this.damping = 1; this.constraintCycles = 0; this.constraintCollisionCycles = 1; }; Engine.prototype = { step: function() { var a = this.groups; var l = a.length; for (var i = 0; i < l; ++i) { var g = a[i]; g.integrate(this._timeStep, this); } for (var j = 0; j < this.constraintCycles; j++) { for (i = 0; i < l; ++i) { g = a[i]; g.satisfyConstraints(); } } for (j = 0; j < this.constraintCollisionCycles; j++) { for (i = 0; i < l; ++i) { g = a[i]; g.satisfyConstraints(); } for (i = 0; i < l; ++i) { g = a[i]; g.checkCollisions(); } } }, paint: function() { var a = this.groups; var l = a.length; for (var i = 0; i < l; ++i) { var g = a[i]; g.paint(); } } }; ////////////////////////////////////////////////////////////////////// const SVG = "http://www.w3.org/2000/svg"; /** class Style */ var Style = function(lineThickness, lineColor, fillColor) { this.lineThickness = lineThickness || 0; this.lineColor = lineColor || '#000000'; this.fillColor = fillColor || '#ffffff'; }; /** class Bridge extends Group */ var Bridge = function(style, container) { Group.call(this); // just a cut and paste class here. everything should be parameterized // if you want to do it the right way, eg locations, section size, etc var bx = 170; var by = 40; var bsize = 51.5; var yslope = 2.4; var particleSize = 4; var bridgePAA = new SVGCircle(bx,by,particleSize,true, 1, 0.3, 0, style); container.appendChild(bridgePAA.shape); bx += bsize; by += yslope; var bridgePA = new SVGCircle(bx,by,particleSize, false, 1, 0.3, 0, style); container.appendChild(bridgePA.shape); bx += bsize; by += yslope; var bridgePB = new SVGCircle(bx,by,particleSize, false, 1, 0.3, 0, style); container.appendChild(bridgePB.shape); bx += bsize; by += yslope; var bridgePC = new SVGCircle(bx,by,particleSize, false, 1, 0.3, 0, style); container.appendChild(bridgePC.shape); bx += bsize; by += yslope; var bridgePD = new SVGCircle(bx,by,particleSize, false, 1, 0.3, 0, style); container.appendChild(bridgePD.shape); bx += bsize; by += yslope; var bridgePDD = new SVGCircle(bx,by,particleSize,true, 1, 0.3, 0, style); container.appendChild(bridgePDD.shape); var bridgeConnA = new Spring(bridgePAA, bridgePA, 0.9); var bridgeConnAp = new SVGRectSpring(bridgeConnA, 10, 0.8, true, style); bridgeConnA.collidableParticle = bridgeConnAp; container.appendChild(bridgeConnAp.shape); // collision response on the bridgeConnA will be ignored on // on the first 1/4 of the constraint. this avoids blow ups // particular to springcontraints that have 1 fixed particle. bridgeConnA.fixedEndLimit = 0.25; var bridgeConnB = new Spring(bridgePA, bridgePB, 0.9); var bridgeConnBp = new SVGRectSpring(bridgeConnB, 10, 0.8, true, style); bridgeConnB.collidableParticle = bridgeConnBp; container.appendChild(bridgeConnBp.shape); var bridgeConnC = new Spring(bridgePB, bridgePC, 0.9); var bridgeConnCp = new SVGRectSpring(bridgeConnC, 10, 0.8, true, style); bridgeConnC.collidableParticle = bridgeConnCp; container.appendChild(bridgeConnCp.shape); var bridgeConnD = new Spring(bridgePC, bridgePD, 0.9); var bridgeConnDp = new SVGRectSpring(bridgeConnD, 10, 0.8, true, style); bridgeConnD.collidableParticle = bridgeConnDp; container.appendChild(bridgeConnDp.shape); var bridgeConnE = new Spring(bridgePD, bridgePDD, 0.9); var bridgeConnEp = new SVGRectSpring(bridgeConnE, 10, 0.8, true, style); bridgeConnE.collidableParticle = bridgeConnEp; container.appendChild(bridgeConnEp.shape); bridgeConnE.fixedEndLimit = 0.25; this.particles = [bridgePAA, bridgePA, bridgePB, bridgePC, bridgePD, bridgePDD]; this.constraints = [bridgeConnA, bridgeConnB, bridgeConnC, bridgeConnD, bridgeConnE]; }; Bridge.prototype = new Group(); /** class Capsule extends Group */ var Capsule = function(style1, container) { Group.call(this); var style2 = new Style(5, style1.lineColor, style1.fillColor); var capsuleP1 = new SVGCircle(300, 10, 14, false, 1.3, 0.4, 0, style1); container.appendChild(capsuleP1.shape); var capsuleP2 = new SVGCircle(325, 35, 14, false, 1.3, 0.4, 0, style1); container.appendChild(capsuleP2.shape); var capsule = new Spring(capsuleP1, capsuleP2, 1); var capsuleP = new SVGRectSpring(capsule, 24, 1, true, style2); capsule.collidableParticle = capsuleP; container.appendChild(capsuleP.shape); this.particles = [capsuleP1, capsuleP2]; this.constraints = [capsule]; }; Capsule.prototype = new Group(); /** class Car extends Group */ var Car = function(style, container) { Group.call(this); var wheelParticleA = new SVGWheel(140, 10, 14, false, 2, 0.3, 0, 1, style); container.appendChild(wheelParticleA.shape); var wheelParticleB = new SVGWheel(200, 10, 14, false, 2, 0.3, 0, 1, style); container.appendChild(wheelParticleB.shape); var wheelConnector = new Spring(wheelParticleA, wheelParticleB, 0.5); var wheelConnectorP = new SVGRectSpring(wheelConnector, 8, 1, true, style); wheelConnector.collidableParticle = wheelConnectorP; container.appendChild(wheelConnectorP.shape); this.particles = [wheelParticleA, wheelParticleB]; this.constraints = [wheelConnector]; this.wheelParticleA = wheelParticleA; this.wheelParticleB = wheelParticleB; }; Car.prototype = Util.concat(new Group(), { set speed(s) { this.wheelParticleA.angularVelocity = s; this.wheelParticleB.angularVelocity = s; }, get x() { return (this.wheelParticleA.x + this.wheelParticleB.x)/2; } }); /** class CarDemo */ var CarDemo = function(container) { var engine = new Engine(1/4); engine.gravity = new Vector(0, 3); var surfaces = new Surfaces(new Style(0, '#6699aa', '#6699aa'), new Style(1, '#6699aa', '#3366aa'), new Style(0, '#6699aa', '#996633'), container); var bridge = new Bridge(new Style(1, '#aabbbb', '#3366aa'), container); var capsule = new Capsule(new Style(1, '#aabbbb', '#aabbbb'), container); var rotator = new Rotator(new Style(0, '#3366aa', '#3366aa'), new Style(1, '#3366aa', '#778877'), new Style(2, '#778877'), container); var swingDoor = new SwingDoor(new Style(1, '#aabbbb', '#aabbbb'), container); var car = new Car(new Style(1, '#aabbbb', '#778877'), container); engine.groups = [surfaces, bridge, capsule, rotator, swingDoor, car]; car.collisionList = [surfaces, bridge, rotator, swingDoor, capsule]; capsule.collisionList = [surfaces, bridge, rotator, swingDoor]; this.engine = engine; this.car = car; this.rotator = rotator; this.container = container; }; CarDemo.prototype = { run: function() { this.engine.step(); this.engine.paint(); this.rotator.rotate(.02); var x = Math.max(Math.min(0, -this.car.x + (450/2)), 450-650); this.container.setAttributeNS(null, "transform", "translate("+x+" 0)"); }, keyDownHandler: function(keyEvt) { var keySpeed = 0.2; if (keyEvt.keyCode == 65) { // 'A' this.car.speed = -keySpeed; } else if (keyEvt.keyCode == 68) { // 'D' this.car.speed = keySpeed; } }, keyUpHandler: function(keyEvt) { this.car.speed = 0; } }; /** class RectComposite extends Composite */ var RectComposite = function(ctr, style, container) { Composite.call(this); var rw = 75; var rh = 18; var rad = 4; cpA = new SVGCircle(ctr.x-rw/2, ctr.y-rh/2, rad, true, 1, 0.3, 0, style); container.appendChild(cpA.shape); var cpB = new SVGCircle(ctr.x+rw/2, ctr.y-rh/2, rad, true, 1, 0.3, 0, style); container.appendChild(cpB.shape); cpC = new SVGCircle(ctr.x+rw/2, ctr.y+rh/2, rad, true, 1, 0.3, 0, style); container.appendChild(cpC.shape); var cpD = new SVGCircle(ctr.x-rw/2, ctr.y+rh/2, rad, true, 1, 0.3, 0, style); container.appendChild(cpD.shape); var sprA = new Spring(cpA, cpB, 0.5); var sprAP = new SVGRectSpring(sprA, rad * 2, 1, true, style); sprA.collidableParticle = sprAP; container.appendChild(sprAP.shape); var sprB = new Spring(cpB, cpC, 0.5); var sprBP = new SVGRectSpring(sprB, rad * 2, 1, true, style); sprB.collidableParticle = sprBP; container.appendChild(sprBP.shape); var sprC = new Spring(cpC, cpD, 0.5); var sprCP = new SVGRectSpring(sprC, rad * 2, 1, true, style); sprC.collidableParticle = sprCP; container.appendChild(sprCP.shape); var sprD = new Spring(cpD, cpA, 0.5); var sprDP = new SVGRectSpring(sprD, rad * 2, 1, true, style); sprD.collidableParticle = sprDP; container.appendChild(sprDP.shape); this.particles = [cpA, cpB, cpC, cpD]; this.constraints = [sprA, sprB, sprC, sprD]; this.cpA = cpA; this.cpC = cpC; }; RectComposite.prototype = Util.concat(new Composite(), { get pa() { return this.cpA; }, get pc() { return this.cpC; } }); /** class Rotator extends Group */ var Rotator = function(style0, style1, style2, container) { Group.call(this); this.collideInternal = true; this.ctr = new Vector(555, 175); this.rectComposite = new RectComposite(this.ctr, style0, container); var circA = new SVGCircle(this.ctr.x, this.ctr.y, 5, false, 1, 0.3, 0, style1); container.appendChild(circA.shape); var rectA = new SVGRect(555, 160, 10, 10, 0, false, 3, 0.3, 0, style1); container.appendChild(rectA.shape); var connectorA = new SVGLineSpring(this.rectComposite.pa, rectA, 1, style2); container.appendChild(connectorA.shape); var rectB = new SVGRect(555, 190, 10, 10, 0, false, 3, 0.3, 0, style1); container.appendChild(rectB.shape); var connectorB = new SVGLineSpring(this.rectComposite.pc, rectB, 1, style2); container.appendChild(connectorB.shape); this.particles = [circA, rectA, rectB]; this.constraints = [connectorA, connectorB]; this.composites = [this.rectComposite]; }; Rotator.prototype = Util.concat(new Group(), { rotate: function(a) { this.rectComposite.rotate(a, this.ctr); } }); /** class SVGCircle extends Circle */ var SVGCircle = function(x, y, radius, fixed, mass, elasticity, friction, style) { Circle.call(this, x, y, radius, fixed, mass, elasticity, friction); var shape = document.createElementNS(SVG, "circle"); shape.setAttributeNS(null, "cx", x); shape.setAttributeNS(null, "cy", y); shape.setAttributeNS(null, "r", radius); shape.setAttributeNS(null, "fill", style.fillColor); shape.setAttributeNS(null, "stroke", style.lineColor); shape.setAttributeNS(null, "stroke-width", style.lineThickness); this.shape = shape; }; SVGCircle.prototype = Util.concat(new Circle(), { paint: function() { this.shape.setAttributeNS(null, "cx", this.x); this.shape.setAttributeNS(null, "cy", this.y); } }); /** class SVGLineSpring extends Spring */ var SVGLineSpring = function(p1, p2, stiffness, style) { Spring.call(this, p1, p2, stiffness); var line = document.createElementNS(SVG, "line"); line.setAttributeNS(null, "stroke", style.lineColor); line.setAttributeNS(null, "stroke-width", style.lineThickness); this.shape = line; }; SVGLineSpring.prototype = Util.concat(new Spring(), { paint: function() { var line = this.shape; var p1 = this.p1.curr; var p2 = this.p2.curr; line.setAttributeNS(null, "x1", p1.x); line.setAttributeNS(null, "y1", p1.y); line.setAttributeNS(null, "x2", p2.x); line.setAttributeNS(null, "y2", p2.y); } }); /** class SVGRect extends Rect */ var SVGRect = function(x, y, width, height, rotation, fixed, mass, elasticity, friction, style) { Rect.call(this, x, y, width, height, rotation, fixed, mass, elasticity, friction); var x0 = x - width / 2; var y0 = y - height / 2; var shape = document.createElementNS(SVG, "rect"); shape.setAttributeNS(null, "x", x0); shape.setAttributeNS(null, "y", y0); shape.setAttributeNS(null, "width", width); shape.setAttributeNS(null, "height", height); shape.setAttributeNS(null, "fill", style.fillColor); shape.setAttributeNS(null, "stroke", style.lineColor); shape.setAttributeNS(null, "stroke-width", style.lineThickness); shape.setAttributeNS(null, "transform", "rotate("+this.angle+" "+x+" "+y+")"); this.shape = shape; }; SVGRect.prototype = Util.concat(new Rect(), { paint: function() { var shape = this.shape; var x = this.x; var x0 = x - this.width/2; var y = this.y; var y0 = y - this.height/2; var a = this.angle; shape.setAttributeNS(null, "x", x0); shape.setAttributeNS(null, "y", y0); shape.setAttributeNS(null, "transform", "rotate("+a+" "+x+" "+y+")"); } }); /** class SVGRectSpring extends SpringRect */ var SVGRectSpring = function(spring, rectHeight, rectScale, scaleToLength, style) { rectHeight = rectHeight || 1; rectScale = rectScale || 1; scaleToLength = scaleToLength || false; SpringRect.call(this, spring, rectHeight, rectScale, scaleToLength); var center = spring.center; var x = center.x; var y = center.y; var w = spring.currLength * rectScale; var h = rectHeight; var shape = document.createElementNS(SVG, "rect"); shape.setAttributeNS(null, "x", x-w/2); shape.setAttributeNS(null, "y", y-h/2); shape.setAttributeNS(null, "width", w); shape.setAttributeNS(null, "height", h); shape.setAttributeNS(null, "fill", style.fillColor); shape.setAttributeNS(null, "stroke", style.lineColor); shape.setAttributeNS(null, "stroke-width", style.lineThickness); this.shape = shape; }; SVGRectSpring.prototype = Util.concat(new SpringRect(), { paint: function() { var spring = this._spring; var center = spring.center; var x = center.x; var y = center.y; var w = spring.currLength; if (this.scaleToLength) { w = w * this.rectScale; } var h = this.rectHeight; var a = spring.angle; var shape = this.shape; shape.setAttributeNS(null, "x", x-w/2); shape.setAttributeNS(null, "y", y-h/2); shape.setAttributeNS(null, "transform", "rotate("+a+" "+x+" "+y+")"); } }); /** class SVGWheel extends Wheel */ var SVGWheel = function(x, y, radius, fixed, mass, elasticity, friction, traction, style) { Wheel.call(this, x, y, radius, fixed, mass, elasticity, friction, traction); var g = document.createElementNS(SVG, "g"); g.setAttributeNS(null, "transform", "translate("+x+" "+y+")"); g.setAttributeNS(null, "stroke", style.lineColor); g.setAttributeNS(null, "stroke-width", style.lineThickness); var circle = document.createElementNS(SVG, "circle"); circle.setAttributeNS(null, "cx", 0); circle.setAttributeNS(null, "cy", 0); circle.setAttributeNS(null, "r", radius); circle.setAttributeNS(null, "fill", style.fillColor); g.appendChild(circle); var l1 = document.createElementNS(SVG, "line"); l1.setAttributeNS(null, "x1", 0); l1.setAttributeNS(null, "y1", -radius); l1.setAttributeNS(null, "x2", 0); l1.setAttributeNS(null, "y2", radius); g.appendChild(l1); var l2 = document.createElementNS(SVG, "line"); l2.setAttributeNS(null, "x1", -radius); l2.setAttributeNS(null, "y1", 0); l2.setAttributeNS(null, "x2", radius); l2.setAttributeNS(null, "y2", 0); g.appendChild(l2); this.shape = g; }; SVGWheel.prototype = Util.concat(new Wheel(), { paint: function() { var p = this.curr; var a = this.angle; this.shape.setAttributeNS(null, "transform", "translate("+p.x+" "+p.y+") rotate("+a+")"); } }); /** class Surfaces extends Group */ var Surfaces = function(style1, style2, style3, container) { Group.call(this); var floor = new SVGRect(340,327,550,50,0,true, 1, 0.3, 0, style1); container.appendChild(floor.shape); var ceil = new SVGRect(325,-33,649,80,0,true, 1, 0.3, 0, style1); container.appendChild(ceil.shape); var rampRight = new SVGRect(375,220,390,20,0.405,true, 1, 0.3, 0, style1); container.appendChild(rampRight.shape); var rampLeft = new SVGRect(90,200,102,20,-.7,true, 1, 0.3, 0, style1); container.appendChild(rampLeft.shape); var rampLeft2 = new SVGRect(96,129,102,20,-.7,true, 1, 0.3, 0, style1); container.appendChild(rampLeft2.shape); var rampCircle = new SVGCircle(175,190,60,true, 1, 0.3, 0, style2); container.appendChild(rampCircle.shape); var floorBump = new SVGCircle(600,660,400,true, 1, 0.3, 0, style2); container.appendChild(floorBump.shape); var bouncePad = new SVGRect(35,370,40,60,0,true, 1, 0.3, 0, style3); container.appendChild(bouncePad.shape); bouncePad.elasticity = 4; var leftWall = new SVGRect(1,99,30,500,0,true, 1, 0.3, 0, style1); container.appendChild(leftWall.shape); var leftWallChannelInner = new SVGRect(54,300,20,150,0,true, 1, 0.3, 0, style1); container.appendChild(leftWallChannelInner.shape); var leftWallChannel = new SVGRect(54,122,20,94,0,true, 1, 0.3, 0, style1); container.appendChild(leftWallChannel.shape); var leftWallChannelAng = new SVGRect(75,65,60,25,- 0.7,true, 1, 0.3, 0, style1); container.appendChild(leftWallChannelAng.shape); var topLeftAng = new SVGRect(23,11,65,40,-0.7,true, 1, 0.3, 0, style1); container.appendChild(topLeftAng.shape); var rightWall = new SVGRect(654,230,50,500,0,true, 1, 0.3, 0, style1); container.appendChild(rightWall.shape); var bridgeStart = new SVGRect(127,49,75,25,0,true, 1, 0.3, 0, style1); container.appendChild(bridgeStart.shape); var bridgeEnd = new SVGRect(483,55,100,15,0,true, 1, 0.3, 0, style1); container.appendChild(bridgeEnd.shape); this.particles = [floor, ceil, rampRight, rampLeft, rampLeft2, rampCircle, floorBump, bouncePad, leftWall, leftWallChannelInner, leftWallChannel, leftWallChannelAng, topLeftAng, rightWall, bridgeStart, bridgeEnd]; this.constraints = []; }; Surfaces.prototype = Util.concat(new Group(), { paint: function() {/*N/A*/} }); /** class SwingDoor extends Group */ var SwingDoor = function(style1, container) { Group.call(this); this.collideInternal = true; var style2 = new Style(2, style1.lineColor, style1.fillColor); var swingDoorP1 = new SVGCircle(543, 55, 7, false, 1, 0.3, 0, style1); container.appendChild(swingDoorP1.shape); swingDoorP1.mass = 0.001; var swingDoorP2 = new SVGCircle(620, 55, 7, true, 1, 0.3, 0, style1); container.appendChild(swingDoorP2.shape); var swingDoor = new Spring(swingDoorP1, swingDoorP2, 1); var swingDoorP = new SVGRectSpring(swingDoor, 13, 1, true, style2); swingDoor.collidableParticle = swingDoorP; container.appendChild(swingDoorP.shape); var swingDoorAnchor = new Circle(543, 5, 2, true, 1, 0.3, 0); swingDoorAnchor.collidable = false; var swingDoorSpring = new Spring(swingDoorP1, swingDoorAnchor, 0.02); swingDoorSpring.restLength = 40; var stopperA = new Circle(550, -60, 70, true, 1, 0.3, 0); var stopperB = new Rect(650, 130, 42, 70, 0, true, 1, 0.3, 0); this.particles = [swingDoorP1, swingDoorP2, swingDoorAnchor, stopperA, stopperB]; this.constraints = [swingDoor, swingDoorSpring]; }; SwingDoor.prototype = new Group(); ////////////////////////////////////////////////////////////////////// var demo = null; var enabled = true; function loaded() { var svg = document.createElementNS(SVG, "svg"); svg.setAttributeNS(null, "width", "450px"); svg.setAttributeNS(null, "height", "350px"); svg.setAttributeNS(null, "viewBox", "0 0 450 350"); svg.setAttributeNS(null, "version", "1.1"); var rect = document.createElementNS(SVG, "rect"); rect.setAttributeNS(null, "x", "0"); rect.setAttributeNS(null, "y", "0"); rect.setAttributeNS(null, "width", "450"); rect.setAttributeNS(null, "height", "350"); rect.setAttributeNS(null, "fill", "#334433"); svg.appendChild(rect); var g = document.createElementNS(SVG, "g"); svg.appendChild(g); demo = new CarDemo(g); document.body.appendChild(svg); window.setInterval(function() { if (enabled) { demo.run(); } }, 1000/30); } function onoff(button) { if (enabled) { enabled = false; button.innerHTML = "START"; } else { enabled = true; button.innerHTML = "STOP"; } } function lefton() { demo.keyDownHandler({keyCode:65}); } function leftoff() { demo.keyUpHandler(); } function righton() { demo.keyDownHandler({keyCode:68}); } function rightoff() { demo.keyUpHandler(); } loaded(); <div> <button id="onoff" onclick="onoff(this)">STOP</button> <button id="left" onmousedown="lefton()" onmouseup="leftoff()" ontouchstart="lefton()" ontouchend="leftoff()">LEFT</button> <button id="right" onmousedown="righton()" onmouseup="rightoff()" ontouchstart="righton()" ontouchend="rightoff()">RIGHT</button> </div> Car Demo body { background-color: #DDDDDD; font: 12px sans-serif; } /* Copyright (c) 2006, 2007 Alec Cove Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* ported by flashrod 2008 ActionScript ported by flashrod 2011 JavaScript */ var Util = { concat: function(d, s) { for (var key in s) { if (s.hasOwnProperty(key)) { var getter = s.__lookupGetter__(key); if (getter) { d.__defineGetter__(key, getter); } var setter = s.__lookupSetter__(key); if (setter) { d.__defineSetter__(key, setter); } if (!(getter || setter)) { d[key] = s[key]; } } } return d; } }; var Vector = function(x, y) { this.x = x || 0; this.y = y || 0; }; Vector.prototype = { setTo: function(x, y) { this.x = x; this.y = y; }, copy: function(v) { this.x = v.x; this.y = v.y; }, dot: function(v) { return this.x * v.x + this.y * v.y; }, cross: function(v) { return this.x * v.y - this.y * v.x; }, plus: function(v) { return new Vector(this.x + v.x, this.y + v.y); }, plusEquals: function(v) { this.x += v.x; this.y += v.y; return this; }, minus: function(v) { return new Vector(this.x - v.x, this.y - v.y); }, minusEquals: function(v) { this.x -= v.x; this.y -= v.y; return this; }, mult: function(s) { return new Vector(this.x * s, this.y * s); }, multEquals: function(s) { this.x *= s; this.y *= s; return this; }, times: function(v) { return new Vector(this.x * v.x, this.y * v.y); }, divEquals: function(s) { if (s == 0) { s = 0.0001; } this.x /= s; this.y /= s; return this; }, magnitude: function() { return Math.sqrt(this.x * this.x + this.y * this.y); }, distance: function(v) { var delta = this.minus(v); return delta.magnitude(); }, normalize: function() { var m = this.magnitude(); if (m == 0) { m = 0.0001; } return this.mult(1 / m); }, toString: function() { return "(" + this.x + ", " + this.y + ")"; } }; var Particle = function(x, y, fixed, mass, elasticity, friction) { this.samp = new Vector(0, 0); this.interval = { min:0, max:0 }; this.forces = new Vector(0, 0); this.collision = {vn: new Vector(0, 0), vt: new Vector(0, 0)}; this._collidable = true; this._multisample = 0; this.curr = new Vector(x, y); this.prev = new Vector(x, y); this._fixed = fixed; this._mass = mass; this._invMass = fixed ? 0 : 1 / mass; this._elasticity = elasticity; this._friction = friction; }; Particle.prototype = { get mass() { return this._mass; }, set mass(m) { if (m <= 0) { throw new ArgumentError("mass may not be set <= 0"); } this._mass = m; this._invMass = this._fixed ? 0 : 1 / this._mass; }, get invMass() { return this._invMass; }, get elasticity() { return this._elasticity; }, set elasticity(k) { this._elasticity = k; }, get multisample() { return this._multisample; }, set multisample(m) { this._multisample = m; }, get friction() { return this._friction; }, set friction(f) { if (f < 0 || f > 1) throw new ArgumentError("Legal friction must be >= 0 and <=1"); this._friction = f; }, get fixed() { return this._fixed; }, set fixed(f) { this._fixed = f; }, get position() { return new Vector(this.curr.x, this.curr.y); }, set position(p) { this.curr.copy(p); this.prev.copy(p); }, get x() { return this.curr.x; }, set x(x) { this.prev.x = this.curr.x = x; }, get y() { return this.curr.y; }, set y(y) { this.prev.y = this.curr.y = y; }, get velocity() { return this.curr.minus(this.prev); }, set velocity(v) { this.prev = this.curr.minus(v); }, get collidable() { return this._collidable; }, set collidable(b) { this._collidable = b; }, addForce: function(f) { this.forces.plusEquals(f.mult(this.invMass)); }, addMasslessForce: function(f) { this.forces.plusEquals(f); }, update: function(dt2, engine) { if (this.fixed) { return; } this.addForce(engine.force); this.addMasslessForce(engine.gravity); var temp = new Vector(this.curr.x, this.curr.y); var nv = this.velocity.plus(this.forces.multEquals(dt2)); this.curr.plusEquals(nv.multEquals(engine.damping)); this.prev.copy(temp); this.forces.setTo(0, 0); }, getComponents: function(collisionNormal) { var vel = this.velocity; var vdotn = collisionNormal.dot(vel); this.collision.vn = collisionNormal.mult(vdotn); this.collision.vt = vel.minus(this.collision.vn); return this.collision; }, resolveCollision: function(mtd, vel, n, d, o, p) { this.curr.plusEquals(mtd); this.velocity = vel; }, updatePosition: function() {}, paint: function() {}, toString: function() { return "Particle()"; } }; var Circle = function(x, y, radius, fixed, mass, elasticity, friction) { fixed = fixed || false; mass = mass || 1; elasticity = elasticity || 0.3; friction = friction || 0; Particle.call(this, x, y, fixed, mass, elasticity, friction); this.radius = radius; }; Circle.prototype = Util.concat(new Particle(), { getProjection: function(axis) { var c = this.samp.dot(axis); this.interval.min = c - this.radius; this.interval.max = c + this.radius; return this.interval; }, getIntervalX: function() { this.interval.min = this.curr.x - this.radius; this.interval.max = this.curr.x + this.radius; return this.interval; }, getIntervalY: function() { this.interval.min = this.curr.y - this.radius; this.interval.max = this.curr.y + this.radius; return this.interval; }, toString: function() { return "Circle("+this.x+", "+this.y+", "+this.radius+")"; } }); var Rect = function(x, y, width, height, rotation, fixed, mass, elasticity, friction) { rotation = rotation || 0; fixed = fixed || false; mass = mass || 1; elasticity = elasticity || 0.3; friction = friction || 0; Particle.call(this, x, y, fixed, mass, elasticity, friction); this._axes = [new Vector(0, 0), new Vector(0, 0)]; this._extents = [width / 2, height / 2]; this.radian = rotation; }; Rect.prototype = Util.concat(new Particle(), { get radian() { return this._radian; }, set radian(t) { this._radian = t; this.setAxes(t); }, get angle() { return this.radian * (180 / Math.PI); }, set angle(a) { this.radian = a * (Math.PI / 180); }, get width() { return this._extents[0] * 2; }, set width(w) { this._extents[0] = w / 2; }, get height() { return this._extents[1] * 2; }, set height(h) { this._extents[1] = h / 2; }, get axes() { return this._axes; }, get extents() { return this._extents; }, getProjection: function(axis) { var radius = this._extents[0] * Math.abs(axis.dot(this._axes[0]))+ this._extents[1] * Math.abs(axis.dot(this._axes[1])); var c = this.samp.dot(axis); this.interval.min = c - radius; this.interval.max = c + radius; return this.interval; }, setAxes: function(t) { var s = Math.sin(t); var c = Math.cos(t); this._axes[0].x = c; this._axes[0].y = s; this._axes[1].x = -s; this._axes[1].y = c; }, toString: function() { return "Rect("+this.x+","+this.y+","+this.width+","+this.height+")"; } }); var Constraint = function(stiffness) { this.stiffness = stiffness; }; Constraint.prototype = { get collidableParticle() { return null; }, set collidableParticle(p) {}, isConnectedTo: function(p) { return false; }, resolve: function() {}, paint: function() {}, toString: function() { return "Constraint("+this.stiffness+")"; } }; var Spring = function(p1, p2, stiffness) { stiffness = stiffness || 0.5; Constraint.call(this, stiffness); if (p1 && p2) { this.p1 = p1; this.p2 = p2; this.checkParticlesLocation(); this._restLength = this.currLength; } this._scp = null; }; Spring.prototype = Util.concat(new Constraint(), { get radian() { var d = this.delta; return Math.atan2(d.y, d.x); }, get angle() { return this.radian * (180 / Math.PI); }, get center() { return (this.p1.curr.plus(this.p2.curr)).divEquals(2); }, set rectScale(s) { if (this._scp != null) { this._scp.rectScale = s; } }, get rectScale() { if (this._scp != null) { return this._scp.rectScale; } return NaN; }, get currLength() { return this.p1.curr.distance(this.p2.curr); }, get rectHeight() { if (this._scp != null) { return this._scp.rectHeight; } return NaN; }, set rectHeight(h) { if (this._scp != null) { this._scp.rectHeight = h; } }, get restLength() { return this._restLength; }, set restLength(r) { if (r <= 0) { throw new ArgumentError("restLength must be greater than 0"); } this._restLength = r; }, get fixedEndLimit() { if (this._scp != null) { return this._scp.fixedEndLimit; } return NaN; }, set fixedEndLimit(f) { if (this._scp != null) { this._scp.fixedEndLimit = f; } }, get collidableParticle() { return this._scp; }, set collidableParticle(p) { this._scp = p; }, isConnectedTo: function(p) { return (p == this.p1 || p == this.p2); }, get fixed() { return (this.p1.fixed && this.p2.fixed); }, get delta() { return this.p1.curr.minus(this.p2.curr); }, resolve: function() { if (this.p1.fixed && this.p2.fixed) { return; } var deltaLength = this.currLength; var diff = (deltaLength - this.restLength) / (deltaLength * (this.p1.invMass + this.p2.invMass)); var dmds = this.delta.mult(diff * this.stiffness); this.p1.curr.minusEquals(dmds.mult(this.p1.invMass)); this.p2.curr.plusEquals (dmds.mult(this.p2.invMass)); }, checkParticlesLocation: function() { if (this.p1.curr.x == this.p2.curr.x && this.p1.curr.y == this.p2.curr.y) { this.p2.curr.x += 0.0001; } }, paint: function() { if (this._scp != null) { this._scp.paint(); } }, toString: function() { return "Spring()"; } }); var Collection = function() { this.particles = []; this.constraints = []; }; Collection.prototype = { integrate: function(dt2, engine) { var a = this.particles; var l = a.length; for (var i = 0; i < l; ++i) { var p = a[i]; p.update(dt2, engine); } }, satisfyConstraints: function() { var a = this.constraints; var l = a.length; for (var i = 0; i < l; ++i) { var c = a[i]; c.resolve(); } }, checkInternalCollisions: function() { var a1 = this.particles; var l1 = a1.length; var a2 = this.constraints; var l2 = a2.length; for (var j = 0; j < l1; j++) { var pj = a1[j]; if (! pj.collidable) { continue; } for (var i = j + 1; i < l1; i++) { var pi = a1[i]; if (pi.collidable) { collisionTest(pj, pi); } } for (var k = 0; k < l2; ++k) { var c = a2[k]; var pk = c.collidableParticle; if (pk != null && ! c.isConnectedTo(pj)) { pk.updatePosition(); collisionTest(pj, pk); } } } }, checkCollisionsVsCollection: function(that) { var a1 = this.particles; var l1 = a1.length; var a2 = that.particles; var l2 = a2.length; var a3 = this.constraints; var l3 = a3.length; var a4 = that.constraints; var l4 = a4.length; for (var i1 = 0; i1 < l1; ++i1) { var pga = a1[i1]; if (! pga.collidable) { continue; } for (var i2 = 0; i2 < l2; ++i2) { var pgb = a2[i2]; if (pgb.collidable) { collisionTest(pga, pgb); } } for (var i4 = 0; i4 < l4; ++i4) { var cgb = a4[i4]; var pk = cgb.collidableParticle; if (pk != null && ! cgb.isConnectedTo(pga)) { pk.updatePosition(); collisionTest(pga, pk); } } } for (var i3 = 0; i3 < l3; ++i3) { var cga = a3[i3]; var pl = cga.collidableParticle; if (pl == null) { continue; } for (i2 = 0; i2 < l2; ++i2) { pgb = a2[i2]; if (pgb.collidable && !cga.isConnectedTo(pgb)) { pl.updatePosition(); collisionTest(pgb, pl); } } } }, paint: function() { var a1 = this.particles; var l1 = a1.length; for (var i1 = 0; i1 < l1; ++i1) { var p = a1[i1]; p.paint(); } var a2 = this.constraints; var l2 = a2.length; for (var i2 = 0; i2 < l2; ++i2) { var c = a2[i2]; c.paint(); } }, toString: function() { return "Collection()"; } }; var Composite = function() { Collection.call(this); }; Composite.prototype = Util.concat(new Collection(), { rotate: function(angleRadians, center) { var a = this.particles; var l = a.length; for (var i = 0; i < l; ++i) { var p = a[i]; var other = p.position; var radius = other.distance(center); var angle = Math.atan2(other.y - center.y, other.x - center.x) + angleRadians; p.x = (Math.cos(angle) * radius) + center.x; p.y = (Math.sin(angle) * radius) + center.y; } }, toString: function() { return "Composite()"; } }); var Group = function(collideInternal) { Collection.call(this); this.collideInternal = collideInternal || false; this.composites = []; this.collisionList = []; }; Group.prototype = Util.concat(new Collection(), { integrate: function(dt2, engine) { Collection.prototype.integrate.call(this, dt2, engine); var a = this.composites; var l = a.length; for (var i = 0; i < l; ++i) { var cmp = a[i]; cmp.integrate(dt2, engine); } }, satisfyConstraints: function() { Collection.prototype.satisfyConstraints.call(this); var a = this.composites; var l = a.length; for (var i = 0; i < l; ++i) { var cmp = a[i]; cmp.satisfyConstraints(); } }, checkCollisions: function() { if (this.collideInternal) { this.checkCollisionGroupInternal(); } var a = this.collisionList; var l = a.length; for (var i = 0; i < l; ++i) { var g = a[i]; this.checkCollisionVsGroup(g); } }, checkCollisionGroupInternal: function() { this.checkInternalCollisions(); var clen = this.composites.length; for (var j = 0; j < clen; j++) { var cj = this.composites[j]; cj.checkCollisionsVsCollection(this); for (var i = j + 1; i < clen; i++) { var ci = this.composites[i]; cj.checkCollisionsVsCollection(ci); } } }, checkCollisionVsGroup: function(that) { this.checkCollisionsVsCollection(that); var a1 = this.composites; var l1 = a1.length; var a2 = that.composites; var l2 = a2.length; for (var i = 0; i < l1; ++i) { var c = a1[i]; c.checkCollisionsVsCollection(that); for (var j = 0; j < l2; ++j) { var gc = a2[j]; c.checkCollisionsVsCollection(gc); } } for (j = 0; j < l2; ++j) { gc = a2[j]; this.checkCollisionsVsCollection(gc); } }, paint: function() { Collection.prototype.paint.call(this); var a = this.composites; var l = a.length; for (var i = 0; i < l; ++i) { var c = a[i]; c.paint(); } }, toString: function() { return "Group()"; } }); function collisionTest(objA, objB) { if (objA.fixed && objB.fixed) { return; } if (objA.multisample == 0 && objB.multisample == 0) { normVsNorm(objA, objB); } else if (objA.multisample > 0 && objB.multisample == 0) { sampVsNorm(objA, objB); } else if (objB.multisample > 0 && objA.multisample == 0) { sampVsNorm(objB, objA); } else if (objA.multisample == objB.multisample) { sampVsSamp(objA, objB); } else { normVsNorm(objA, objB); } } function normVsNorm(objA, objB) { objA.samp.copy(objA.curr); objB.samp.copy(objB.curr); testTypes(objA, objB); } function sampVsNorm(objA, objB) { var s = 1 / (objA.multisample + 1); var t = s; objB.samp.copy(objB.curr); for (var i = 0; i <= objA.multisample; i++) { objA.samp.setTo(objA.prev.x + t * (objA.curr.x - objA.prev.x), objA.prev.y + t * (objA.curr.y - objA.prev.y)); if (testTypes(objA, objB)) { return; } t += s; } } function sampVsSamp(objA, objB) { var s = 1 / (objA.multisample + 1); var t = s; for (var i = 0; i <= objA.multisample; i++) { objA.samp.setTo(objA.prev.x + t * (objA.curr.x - objA.prev.x), objA.prev.y + t * (objA.curr.y - objA.prev.y)); objB.samp.setTo(objB.prev.x + t * (objB.curr.x - objB.prev.x), objB.prev.y + t * (objB.curr.y - objB.prev.y)); if (testTypes(objA, objB)) { return; } t += s; } } function testTypes(objA, objB) { if ((objA instanceof Circle) && (objB instanceof Circle)) { testCirclevsCircle(objA, objB); } else if ((objA instanceof Circle) && (objB instanceof Rect)) { testCirclevsOBB(objA, objB); } else if ((objA instanceof Rect) && (objB instanceof Circle)) { testOBBvsCircle(objA, objB); } else { testOBBvsOBB(objA, objB); } } function testOBBvsOBB(ra, rb) { var collisionNormal; var collisionDepth = Number.POSITIVE_INFINITY; for (var i = 0; i < 2; i++) { var axisA = ra.axes[i]; var depthA = testIntervals(ra.getProjection(axisA), rb.getProjection(axisA)); if (depthA == 0) { return false; } var axisB = rb.axes[i]; var depthB = testIntervals(ra.getProjection(axisB), rb.getProjection(axisB)); if (depthB == 0) { return false; } var absA = Math.abs(depthA); var absB = Math.abs(depthB); if (absA < Math.abs(collisionDepth) || absB < Math.abs(collisionDepth)) { var altb = absA < absB; collisionNormal = altb ? axisA : axisB; collisionDepth = altb ? depthA : depthB; } } resolveParticleParticle(ra, rb, collisionNormal, collisionDepth); return true; } function testCirclevsOBB(ca, ra) { return testOBBvsCircle(ra, ca); } function testOBBvsCircle(ra, ca) { var collisionNormal; var collisionDepth = Number.POSITIVE_INFINITY; var depths = new Array(2); // first go through the axes of the rectangle for (var i = 0; i < 2; i++) { var boxAxis = ra.axes[i]; var depth = testIntervals(ra.getProjection(boxAxis), ca.getProjection(boxAxis)); if (depth == 0) { return false; } if (Math.abs(depth) < Math.abs(collisionDepth)) { collisionNormal = boxAxis; collisionDepth = depth; } depths[i] = depth; } // determine if the circle's center is in a vertex region var r = ca.radius; if (Math.abs(depths[0]) < r && Math.abs(depths[1]) < r) { var vertex = closestVertexOnOBB(ca.samp, ra); // get the distance from the closest vertex on rect to circle center collisionNormal = vertex.minus(ca.samp); var mag = collisionNormal.magnitude(); collisionDepth = r - mag; if (collisionDepth > 0) { // there is a collision in one of the vertex regions collisionNormal.divEquals(mag); } else { // ra is in vertex region, but is not colliding return false; } } resolveParticleParticle(ra, ca, collisionNormal, collisionDepth); return true; } function testCirclevsCircle(ca, cb) { var depthX = testIntervals(ca.getIntervalX(), cb.getIntervalX()); if (depthX == 0) { return false; } var depthY = testIntervals(ca.getIntervalY(), cb.getIntervalY()); if (depthY == 0) { return false; } var collisionNormal = ca.samp.minus(cb.samp); var mag = collisionNormal.magnitude(); var collisionDepth = (ca.radius + cb.radius) - mag; if (collisionDepth > 0) { collisionNormal.divEquals(mag); resolveParticleParticle(ca, cb, collisionNormal, collisionDepth); return true; } return false; } function testIntervals(intervalA, intervalB) { if (intervalA.max < intervalB.min) { return 0; } if (intervalB.max < intervalA.min) { return 0; } var lenA = intervalB.max - intervalA.min; var lenB = intervalB.min - intervalA.max; return (Math.abs(lenA) < Math.abs(lenB)) ? lenA : lenB; } function closestVertexOnOBB(p, r) { var d = p.minus(r.samp); var q = new Vector(r.samp.x, r.samp.y); for (var i = 0; i < 2; i++) { var dist = d.dot(r.axes[i]); if (dist >= 0) { dist = r.extents[i]; } else if (dist < 0) { dist = -r.extents[i]; } q.plusEquals(r.axes[i].mult(dist)); } return q; } function clamp01(n) { if (n < 0) return 0; if (n > 1) return 1; return n; } // thanks to Jim Bonacci for changes using the inverse mass instead of mass function resolveParticleParticle(pa, pb, normal, depth) { // a collision has occured. set the current positions to sample locations pa.curr.copy(pa.samp); pb.curr.copy(pb.samp); var mtd = normal.mult(depth); var te = pa.elasticity + pb.elasticity; var sumInvMass = pa.invMass + pb.invMass; // the total friction in a collision is combined but clamped to [0,1] var tf = clamp01(1 - (pa.friction + pb.friction)); // get the collision, vn and vt var ca = pa.getComponents(normal); var cb = pb.getComponents(normal); // calculate the coefficient of restitution based on the mass, as the normal component var vnA = (cb.vn.mult((te + 1) * pa.invMass).plus(ca.vn.mult(pb.invMass - te * pa.invMass))).divEquals(sumInvMass); var vnB = (ca.vn.mult((te + 1) * pb.invMass).plus(cb.vn.mult(pa.invMass - te * pb.invMass))).divEquals(sumInvMass); // apply friction to the tangental component ca.vt.multEquals(tf); cb.vt.multEquals(tf); // scale the mtd by the ratio of the masses. heavier particles move less var mtdA = mtd.mult( pa.invMass / sumInvMass); var mtdB = mtd.mult(-pb.invMass / sumInvMass); // add the tangental component to the normal component for the new velocity vnA.plusEquals(ca.vt); vnB.plusEquals(cb.vt); if (! pa.fixed) { pa.resolveCollision(mtdA, vnA, normal, depth, -1, pb); } if (! pb.fixed) { pb.resolveCollision(mtdB, vnB, normal, depth, 1, pa); } } var Rim = function(r, mt, parent) { this.curr = new Vector(r, 0); this.prev = new Vector(0, 0); this.sp = 0; this.av = 0; this.maxTorque = mt; this.wr = r; this.parent = parent; }; Rim.prototype = { get speed() { return this.sp; }, set speed(s) { this.sp = s; }, get angularVelocity() { return this.av; }, set angularVelocity(s) { this.av = s; }, update: function(dt, engine) { //clamp torques to valid range this.sp = Math.max(-this.maxTorque, Math.min(this.maxTorque, this.sp + this.av)); //apply torque //this is the tangent vector at the rim particle var dx = -this.curr.y; var dy = this.curr.x; //normalize so we can scale by the rotational speed var len = Math.sqrt(dx * dx + dy * dy); dx /= len; dy /= len; this.curr.x += this.sp * dx; this.curr.y += this.sp * dy; var ox = this.prev.x; var oy = this.prev.y; var px = this.prev.x = this.curr.x; var py = this.prev.y = this.curr.y; this.curr.x += engine.damping * (px - ox); this.curr.y += engine.damping * (py - oy); // hold the rim particle in place var clen = Math.sqrt(this.curr.x * this.curr.x + this.curr.y * this.curr.y); var diff = (clen - this.wr) / clen; this.curr.x -= this.curr.x * diff; this.curr.y -= this.curr.y * diff; }, toString: function() { return "Rim()"; } }; var Wheel = function(x, y, radius, fixed, mass, elasticity, friction, traction) { fixed = fixed || false; mass = mass || 1; elasticity = elasticity || 0.3; friction = friction || 0; traction = traction || 1; Circle.call(this, x, y, radius, fixed, mass, elasticity, friction); this.tan = new Vector(); this.normSlip = new Vector(); this.orientation = new Vector(); this.rim = new Rim(radius, 2, this); this.traction = traction; }; Wheel.prototype = Util.concat(new Circle(), { get speed() { return this.rim.speed; }, set speed(s) { this.rim.speed = s; }, get angularVelocity() { return this.rim.angularVelocity; }, set angularVelocity(a) { this.rim.angularVelocity = a; }, get traction() { return 1 - this._traction; }, set traction(t) { this._traction = 1 - t; }, get radian() { this.orientation.setTo(this.rim.curr.x, this.rim.curr.y); return Math.atan2(this.orientation.y, this.orientation.x) + Math.PI; }, get angle() { return this.radian * (180 / Math.PI); }, update: function(dt, engine) { Circle.prototype.update.call(this, dt, engine); this.rim.update(dt, engine); }, resolveCollision: function(mtd, vel, n, d, o, p) { // review the o (order) need here - its a hack fix Circle.prototype.resolveCollision.call(this, mtd, vel, n, d, o, p); this.resolve(n.mult(Wheel.sign(d * o))); }, resolve: function(n) { // this is the tangent vector at the this.rim particle this.tan.setTo(-this.rim.curr.y, this.rim.curr.x); // normalize so we can scale by the rotational speed this.tan = this.tan.normalize(); // velocity of the wheel's surface var wheelSurfaceVelocity = this.tan.mult(this.rim.speed); // the velocity of the wheel's surface relative to the ground var combinedVelocity = this.velocity.plusEquals(wheelSurfaceVelocity); // the wheel's comb velocity projected onto the contact normal var cp = combinedVelocity.cross(n); // set the wheel's spinspeed to track the ground this.tan.multEquals(cp); this.rim.prev.copy(this.rim.curr.minus(this.tan)); // some of the wheel's torque is removed and converted into linear displacement var slipSpeed = (1 - this._traction) * this.rim.speed; this.normSlip.setTo(slipSpeed * n.y, slipSpeed * n.x); this.curr.plusEquals(this.normSlip); this.rim.speed = this.rim.speed * this._traction; }, toString: function() { return "Wheel()"; } }); Wheel.sign = function(val) { return (val < 0) ? -1 : 1; } var SpringRect = function(spring, rectHeight, rectScale, scaleToLength) { Rect.call(this, 0, 0, 0, 0, 0, false); if (spring) { this._spring = spring; this.p1 = spring.p1; this.p2 = spring.p2; } this.rectScale = rectScale || 1; this.rectHeight = rectHeight || 1; this.scaleToLength = scaleToLength || false; this.avgVelocity = new Vector(); this.lambda = new Vector(); this.rca = new Vector(); this.rcb = new Vector(); this.fixedEndLimit = 0; }; SpringRect.prototype = Util.concat(new Rect(), { get mass() { return (this.p1.mass + this.p2.mass) / 2; }, get elasticity() { return (this.p1.elasticity + this.p2.elasticity) / 2; }, get friction() { return (this.p1.friction + this.p2.friction) / 2; }, get velocity() { var p1v = this.p1.velocity; var p2v = this.p2.velocity; this.avgVelocity.setTo(((p1v.x + p2v.x) / 2), ((p1v.y + p2v.y) / 2)); return this.avgVelocity; }, get invMass() { if (this.p1.fixed && this.p2.fixed) { return 0; } return 1 / ((this.p1.mass + this.p2.mass) / 2); }, updatePosition: function() { var c = this._spring.center; this.curr.setTo(c.x, c.y); this.width = this.scaleToLength ? this._spring.currLength * this.rectScale : this._spring.restLength * this.rectScale; this.height = this.rectHeight; this.radian = this._spring.radian; }, resolveCollision: function(mtd, vel, n, d, o, p) { var t = this.getContactPointParam(p); var c1 = 1 - t; var c2 = t; // if one is fixed then move the other particle the entire way out of collision. // also, dispose of collisions at the sides of the scp. The higher the this.fixedEndLimit // value, the more of the scp not be effected by collision. if (this.p1.fixed) { if (c2 <= this.fixedEndLimit) { return; } this.lambda.setTo(mtd.x / c2, mtd.y / c2); this.p2.curr.plusEquals(this.lambda); this.p2.velocity = vel; } else if (this.p2.fixed) { if (c1 <= this.fixedEndLimit) { return; } this.lambda.setTo(mtd.x / c1, mtd.y / c1); this.p1.curr.plusEquals(this.lambda); this.p1.velocity = vel; // else both non fixed - move proportionally out of collision } else { var denom = (c1 * c1 + c2 * c2); if (denom == 0) { return; } this.lambda.setTo(mtd.x / denom, mtd.y / denom); this.p1.curr.plusEquals(this.lambda.mult(c1)); this.p2.curr.plusEquals(this.lambda.mult(c2)); // if collision is in the middle of SCP set the velocity of both end particles if (t == 0.5) { this.p1.velocity = vel; this.p2.velocity = vel; // otherwise change the velocity of the particle closest to contact } else { var corrParticle = (t < 0.5) ? this.p1 : this.p2; corrParticle.velocity = vel; } } }, closestParamPoint: function(c) { var ab = this.p2.curr.minus(this.p1.curr); var t = (ab.dot(c.minus(this.p1.curr))) / (ab.dot(ab)); return clamp01(t); }, getContactPointParam: function(p) { var t; if (p instanceof Circle) { t = this.closestParamPoint(p.curr); } else if (p instanceof Rect) { // go through the sides of the colliding rectangle as line segments var shortestIndex; var paramList = new Array(4); var shortestDistance = Number.POSITIVE_INFINITY; for (var i = 0; i < 4; i++) { this.setCorners(p, i); // check for closest points on SCP to side of rectangle var d = this.closestPtSegmentSegment(); if (d < shortestDistance) { shortestDistance = d; shortestIndex = i; paramList[i] = this.s; } } t = paramList[shortestIndex]; } return t; }, setCorners: function(r, i) { var rx = r.curr.x; var ry = r.curr.y; var axes = r.axes; var extents = r.extents; var ae0_x = axes[0].x * extents[0]; var ae0_y = axes[0].y * extents[0]; var ae1_x = axes[1].x * extents[1]; var ae1_y = axes[1].y * extents[1]; var emx = ae0_x - ae1_x; var emy = ae0_y - ae1_y; var epx = ae0_x + ae1_x; var epy = ae0_y + ae1_y; if (i == 0) { // 0 and 1 this.rca.x = rx - epx; this.rca.y = ry - epy; this.rcb.x = rx + emx; this.rcb.y = ry + emy; } else if (i == 1) { // 1 and 2 this.rca.x = rx + emx; this.rca.y = ry + emy; this.rcb.x = rx + epx; this.rcb.y = ry + epy; } else if (i == 2) { // 2 and 3 this.rca.x = rx + epx; this.rca.y = ry + epy; this.rcb.x = rx - emx; this.rcb.y = ry - emy; } else if (i == 3) { // 3 and 0 this.rca.x = rx - emx; this.rca.y = ry - emy; this.rcb.x = rx - epx; this.rcb.y = ry - epy; } }, closestPtSegmentSegment: function() { var pp1 = this.p1.curr; var pq1 = this.p2.curr; var pp2 = this.rca; var pq2 = this.rcb; var d1 = pq1.minus(pp1); var d2 = pq2.minus(pp2); var r = pp1.minus(pp2); var t; var a = d1.dot(d1); var e = d2.dot(d2); var f = d2.dot(r); var c = d1.dot(r); var b = d1.dot(d2); var denom = a * e - b * b; if (denom != 0.0) { this.s = clamp01((b * f - c * e) / denom); } else { this.s = 0.5; // give the midpoint for parallel lines } t = (b * this.s + f) / e; if (t < 0) { t = 0; this.s = clamp01(-c / a); } else if (t > 0) { t = 1; this.s = clamp01((b - c) / a); } var c1 = pp1.plus(d1.mult(this.s)); var c2 = pp2.plus(d2.mult(t)); var c1mc2 = c1.minus(c2); return c1mc2.dot(c1mc2); }, toString: function() { return "SpringRect()"; } }); /** * Initializes the engine. You must call this method prior to adding * any particles or constraints. * * @param dt The delta time value for the engine. This * parameter can be used -- in conjunction with speed at which * <code>Engine.step()</code> is called -- to change the speed * of the simulation. Typical values are 1/3 or 1/4. Lower * values result in slower, but more accurate simulations, and * higher ones result in faster, less accurate ones. Note * that this only applies to the forces added to particles. If * you do not add any forces, the <code>dt</code> value won't * matter. */ var Engine = function(dt) { dt = dt || 0.25; this.force = new Vector(0, 0); this.gravity = new Vector(0, 0); this.groups = []; this._timeStep = dt * dt; this.damping = 1; this.constraintCycles = 0; this.constraintCollisionCycles = 1; }; Engine.prototype = { step: function() { var a = this.groups; var l = a.length; for (var i = 0; i < l; ++i) { var g = a[i]; g.integrate(this._timeStep, this); } for (var j = 0; j < this.constraintCycles; j++) { for (i = 0; i < l; ++i) { g = a[i]; g.satisfyConstraints(); } } for (j = 0; j < this.constraintCollisionCycles; j++) { for (i = 0; i < l; ++i) { g = a[i]; g.satisfyConstraints(); } for (i = 0; i < l; ++i) { g = a[i]; g.checkCollisions(); } } }, paint: function() { var a = this.groups; var l = a.length; for (var i = 0; i < l; ++i) { var g = a[i]; g.paint(); } } }; ////////////////////////////////////////////////////////////////////// const SVG = "http://www.w3.org/2000/svg"; /** class Style */ var Style = function(lineThickness, lineColor, fillColor) { this.lineThickness = lineThickness || 0; this.lineColor = lineColor || '#000000'; this.fillColor = fillColor || '#ffffff'; }; /** class Bridge extends Group */ var Bridge = function(style, container) { Group.call(this); // just a cut and paste class here. everything should be parameterized // if you want to do it the right way, eg locations, section size, etc var bx = 170; var by = 40; var bsize = 51.5; var yslope = 2.4; var particleSize = 4; var bridgePAA = new SVGCircle(bx,by,particleSize,true, 1, 0.3, 0, style); container.appendChild(bridgePAA.shape); bx += bsize; by += yslope; var bridgePA = new SVGCircle(bx,by,particleSize, false, 1, 0.3, 0, style); container.appendChild(bridgePA.shape); bx += bsize; by += yslope; var bridgePB = new SVGCircle(bx,by,particleSize, false, 1, 0.3, 0, style); container.appendChild(bridgePB.shape); bx += bsize; by += yslope; var bridgePC = new SVGCircle(bx,by,particleSize, false, 1, 0.3, 0, style); container.appendChild(bridgePC.shape); bx += bsize; by += yslope; var bridgePD = new SVGCircle(bx,by,particleSize, false, 1, 0.3, 0, style); container.appendChild(bridgePD.shape); bx += bsize; by += yslope; var bridgePDD = new SVGCircle(bx,by,particleSize,true, 1, 0.3, 0, style); container.appendChild(bridgePDD.shape); var bridgeConnA = new Spring(bridgePAA, bridgePA, 0.9); var bridgeConnAp = new SVGRectSpring(bridgeConnA, 10, 0.8, true, style); bridgeConnA.collidableParticle = bridgeConnAp; container.appendChild(bridgeConnAp.shape); // collision response on the bridgeConnA will be ignored on // on the first 1/4 of the constraint. this avoids blow ups // particular to springcontraints that have 1 fixed particle. bridgeConnA.fixedEndLimit = 0.25; var bridgeConnB = new Spring(bridgePA, bridgePB, 0.9); var bridgeConnBp = new SVGRectSpring(bridgeConnB, 10, 0.8, true, style); bridgeConnB.collidableParticle = bridgeConnBp; container.appendChild(bridgeConnBp.shape); var bridgeConnC = new Spring(bridgePB, bridgePC, 0.9); var bridgeConnCp = new SVGRectSpring(bridgeConnC, 10, 0.8, true, style); bridgeConnC.collidableParticle = bridgeConnCp; container.appendChild(bridgeConnCp.shape); var bridgeConnD = new Spring(bridgePC, bridgePD, 0.9); var bridgeConnDp = new SVGRectSpring(bridgeConnD, 10, 0.8, true, style); bridgeConnD.collidableParticle = bridgeConnDp; container.appendChild(bridgeConnDp.shape); var bridgeConnE = new Spring(bridgePD, bridgePDD, 0.9); var bridgeConnEp = new SVGRectSpring(bridgeConnE, 10, 0.8, true, style); bridgeConnE.collidableParticle = bridgeConnEp; container.appendChild(bridgeConnEp.shape); bridgeConnE.fixedEndLimit = 0.25; this.particles = [bridgePAA, bridgePA, bridgePB, bridgePC, bridgePD, bridgePDD]; this.constraints = [bridgeConnA, bridgeConnB, bridgeConnC, bridgeConnD, bridgeConnE]; }; Bridge.prototype = new Group(); /** class Capsule extends Group */ var Capsule = function(style1, container) { Group.call(this); var style2 = new Style(5, style1.lineColor, style1.fillColor); var capsuleP1 = new SVGCircle(300, 10, 14, false, 1.3, 0.4, 0, style1); container.appendChild(capsuleP1.shape); var capsuleP2 = new SVGCircle(325, 35, 14, false, 1.3, 0.4, 0, style1); container.appendChild(capsuleP2.shape); var capsule = new Spring(capsuleP1, capsuleP2, 1); var capsuleP = new SVGRectSpring(capsule, 24, 1, true, style2); capsule.collidableParticle = capsuleP; container.appendChild(capsuleP.shape); this.particles = [capsuleP1, capsuleP2]; this.constraints = [capsule]; }; Capsule.prototype = new Group(); /** class Car extends Group */ var Car = function(style, container) { Group.call(this); var wheelParticleA = new SVGWheel(140, 10, 14, false, 2, 0.3, 0, 1, style); container.appendChild(wheelParticleA.shape); var wheelParticleB = new SVGWheel(200, 10, 14, false, 2, 0.3, 0, 1, style); container.appendChild(wheelParticleB.shape); var wheelConnector = new Spring(wheelParticleA, wheelParticleB, 0.5); var wheelConnectorP = new SVGRectSpring(wheelConnector, 8, 1, true, style); wheelConnector.collidableParticle = wheelConnectorP; container.appendChild(wheelConnectorP.shape); this.particles = [wheelParticleA, wheelParticleB]; this.constraints = [wheelConnector]; this.wheelParticleA = wheelParticleA; this.wheelParticleB = wheelParticleB; }; Car.prototype = Util.concat(new Group(), { set speed(s) { this.wheelParticleA.angularVelocity = s; this.wheelParticleB.angularVelocity = s; }, get x() { return (this.wheelParticleA.x + this.wheelParticleB.x)/2; } }); /** class CarDemo */ var CarDemo = function(container) { var engine = new Engine(1/4); engine.gravity = new Vector(0, 3); var surfaces = new Surfaces(new Style(0, '#6699aa', '#6699aa'), new Style(1, '#6699aa', '#3366aa'), new Style(0, '#6699aa', '#996633'), container); var bridge = new Bridge(new Style(1, '#aabbbb', '#3366aa'), container); var capsule = new Capsule(new Style(1, '#aabbbb', '#aabbbb'), container); var rotator = new Rotator(new Style(0, '#3366aa', '#3366aa'), new Style(1, '#3366aa', '#778877'), new Style(2, '#778877'), container); var swingDoor = new SwingDoor(new Style(1, '#aabbbb', '#aabbbb'), container); var car = new Car(new Style(1, '#aabbbb', '#778877'), container); engine.groups = [surfaces, bridge, capsule, rotator, swingDoor, car]; car.collisionList = [surfaces, bridge, rotator, swingDoor, capsule]; capsule.collisionList = [surfaces, bridge, rotator, swingDoor]; this.engine = engine; this.car = car; this.rotator = rotator; this.container = container; }; CarDemo.prototype = { run: function() { this.engine.step(); this.engine.paint(); this.rotator.rotate(.02); var x = Math.max(Math.min(0, -this.car.x + (450/2)), 450-650); this.container.setAttributeNS(null, "transform", "translate("+x+" 0)"); }, keyDownHandler: function(keyEvt) { var keySpeed = 0.2; if (keyEvt.keyCode == 65) { // 'A' this.car.speed = -keySpeed; } else if (keyEvt.keyCode == 68) { // 'D' this.car.speed = keySpeed; } }, keyUpHandler: function(keyEvt) { this.car.speed = 0; } }; /** class RectComposite extends Composite */ var RectComposite = function(ctr, style, container) { Composite.call(this); var rw = 75; var rh = 18; var rad = 4; cpA = new SVGCircle(ctr.x-rw/2, ctr.y-rh/2, rad, true, 1, 0.3, 0, style); container.appendChild(cpA.shape); var cpB = new SVGCircle(ctr.x+rw/2, ctr.y-rh/2, rad, true, 1, 0.3, 0, style); container.appendChild(cpB.shape); cpC = new SVGCircle(ctr.x+rw/2, ctr.y+rh/2, rad, true, 1, 0.3, 0, style); container.appendChild(cpC.shape); var cpD = new SVGCircle(ctr.x-rw/2, ctr.y+rh/2, rad, true, 1, 0.3, 0, style); container.appendChild(cpD.shape); var sprA = new Spring(cpA, cpB, 0.5); var sprAP = new SVGRectSpring(sprA, rad * 2, 1, true, style); sprA.collidableParticle = sprAP; container.appendChild(sprAP.shape); var sprB = new Spring(cpB, cpC, 0.5); var sprBP = new SVGRectSpring(sprB, rad * 2, 1, true, style); sprB.collidableParticle = sprBP; container.appendChild(sprBP.shape); var sprC = new Spring(cpC, cpD, 0.5); var sprCP = new SVGRectSpring(sprC, rad * 2, 1, true, style); sprC.collidableParticle = sprCP; container.appendChild(sprCP.shape); var sprD = new Spring(cpD, cpA, 0.5); var sprDP = new SVGRectSpring(sprD, rad * 2, 1, true, style); sprD.collidableParticle = sprDP; container.appendChild(sprDP.shape); this.particles = [cpA, cpB, cpC, cpD]; this.constraints = [sprA, sprB, sprC, sprD]; this.cpA = cpA; this.cpC = cpC; }; RectComposite.prototype = Util.concat(new Composite(), { get pa() { return this.cpA; }, get pc() { return this.cpC; } }); /** class Rotator extends Group */ var Rotator = function(style0, style1, style2, container) { Group.call(this); this.collideInternal = true; this.ctr = new Vector(555, 175); this.rectComposite = new RectComposite(this.ctr, style0, container); var circA = new SVGCircle(this.ctr.x, this.ctr.y, 5, false, 1, 0.3, 0, style1); container.appendChild(circA.shape); var rectA = new SVGRect(555, 160, 10, 10, 0, false, 3, 0.3, 0, style1); container.appendChild(rectA.shape); var connectorA = new SVGLineSpring(this.rectComposite.pa, rectA, 1, style2); container.appendChild(connectorA.shape); var rectB = new SVGRect(555, 190, 10, 10, 0, false, 3, 0.3, 0, style1); container.appendChild(rectB.shape); var connectorB = new SVGLineSpring(this.rectComposite.pc, rectB, 1, style2); container.appendChild(connectorB.shape); this.particles = [circA, rectA, rectB]; this.constraints = [connectorA, connectorB]; this.composites = [this.rectComposite]; }; Rotator.prototype = Util.concat(new Group(), { rotate: function(a) { this.rectComposite.rotate(a, this.ctr); } }); /** class SVGCircle extends Circle */ var SVGCircle = function(x, y, radius, fixed, mass, elasticity, friction, style) { Circle.call(this, x, y, radius, fixed, mass, elasticity, friction); var shape = document.createElementNS(SVG, "circle"); shape.setAttributeNS(null, "cx", x); shape.setAttributeNS(null, "cy", y); shape.setAttributeNS(null, "r", radius); shape.setAttributeNS(null, "fill", style.fillColor); shape.setAttributeNS(null, "stroke", style.lineColor); shape.setAttributeNS(null, "stroke-width", style.lineThickness); this.shape = shape; }; SVGCircle.prototype = Util.concat(new Circle(), { paint: function() { this.shape.setAttributeNS(null, "cx", this.x); this.shape.setAttributeNS(null, "cy", this.y); } }); /** class SVGLineSpring extends Spring */ var SVGLineSpring = function(p1, p2, stiffness, style) { Spring.call(this, p1, p2, stiffness); var line = document.createElementNS(SVG, "line"); line.setAttributeNS(null, "stroke", style.lineColor); line.setAttributeNS(null, "stroke-width", style.lineThickness); this.shape = line; }; SVGLineSpring.prototype = Util.concat(new Spring(), { paint: function() { var line = this.shape; var p1 = this.p1.curr; var p2 = this.p2.curr; line.setAttributeNS(null, "x1", p1.x); line.setAttributeNS(null, "y1", p1.y); line.setAttributeNS(null, "x2", p2.x); line.setAttributeNS(null, "y2", p2.y); } }); /** class SVGRect extends Rect */ var SVGRect = function(x, y, width, height, rotation, fixed, mass, elasticity, friction, style) { Rect.call(this, x, y, width, height, rotation, fixed, mass, elasticity, friction); var x0 = x - width / 2; var y0 = y - height / 2; var shape = document.createElementNS(SVG, "rect"); shape.setAttributeNS(null, "x", x0); shape.setAttributeNS(null, "y", y0); shape.setAttributeNS(null, "width", width); shape.setAttributeNS(null, "height", height); shape.setAttributeNS(null, "fill", style.fillColor); shape.setAttributeNS(null, "stroke", style.lineColor); shape.setAttributeNS(null, "stroke-width", style.lineThickness); shape.setAttributeNS(null, "transform", "rotate("+this.angle+" "+x+" "+y+")"); this.shape = shape; }; SVGRect.prototype = Util.concat(new Rect(), { paint: function() { var shape = this.shape; var x = this.x; var x0 = x - this.width/2; var y = this.y; var y0 = y - this.height/2; var a = this.angle; shape.setAttributeNS(null, "x", x0); shape.setAttributeNS(null, "y", y0); shape.setAttributeNS(null, "transform", "rotate("+a+" "+x+" "+y+")"); } }); /** class SVGRectSpring extends SpringRect */ var SVGRectSpring = function(spring, rectHeight, rectScale, scaleToLength, style) { rectHeight = rectHeight || 1; rectScale = rectScale || 1; scaleToLength = scaleToLength || false; SpringRect.call(this, spring, rectHeight, rectScale, scaleToLength); var center = spring.center; var x = center.x; var y = center.y; var w = spring.currLength * rectScale; var h = rectHeight; var shape = document.createElementNS(SVG, "rect"); shape.setAttributeNS(null, "x", x-w/2); shape.setAttributeNS(null, "y", y-h/2); shape.setAttributeNS(null, "width", w); shape.setAttributeNS(null, "height", h); shape.setAttributeNS(null, "fill", style.fillColor); shape.setAttributeNS(null, "stroke", style.lineColor); shape.setAttributeNS(null, "stroke-width", style.lineThickness); this.shape = shape; }; SVGRectSpring.prototype = Util.concat(new SpringRect(), { paint: function() { var spring = this._spring; var center = spring.center; var x = center.x; var y = center.y; var w = spring.currLength; if (this.scaleToLength) { w = w * this.rectScale; } var h = this.rectHeight; var a = spring.angle; var shape = this.shape; shape.setAttributeNS(null, "x", x-w/2); shape.setAttributeNS(null, "y", y-h/2); shape.setAttributeNS(null, "transform", "rotate("+a+" "+x+" "+y+")"); } }); /** class SVGWheel extends Wheel */ var SVGWheel = function(x, y, radius, fixed, mass, elasticity, friction, traction, style) { Wheel.call(this, x, y, radius, fixed, mass, elasticity, friction, traction); var g = document.createElementNS(SVG, "g"); g.setAttributeNS(null, "transform", "translate("+x+" "+y+")"); g.setAttributeNS(null, "stroke", style.lineColor); g.setAttributeNS(null, "stroke-width", style.lineThickness); var circle = document.createElementNS(SVG, "circle"); circle.setAttributeNS(null, "cx", 0); circle.setAttributeNS(null, "cy", 0); circle.setAttributeNS(null, "r", radius); circle.setAttributeNS(null, "fill", style.fillColor); g.appendChild(circle); var l1 = document.createElementNS(SVG, "line"); l1.setAttributeNS(null, "x1", 0); l1.setAttributeNS(null, "y1", -radius); l1.setAttributeNS(null, "x2", 0); l1.setAttributeNS(null, "y2", radius); g.appendChild(l1); var l2 = document.createElementNS(SVG, "line"); l2.setAttributeNS(null, "x1", -radius); l2.setAttributeNS(null, "y1", 0); l2.setAttributeNS(null, "x2", radius); l2.setAttributeNS(null, "y2", 0); g.appendChild(l2); this.shape = g; }; SVGWheel.prototype = Util.concat(new Wheel(), { paint: function() { var p = this.curr; var a = this.angle; this.shape.setAttributeNS(null, "transform", "translate("+p.x+" "+p.y+") rotate("+a+")"); } }); /** class Surfaces extends Group */ var Surfaces = function(style1, style2, style3, container) { Group.call(this); var floor = new SVGRect(340,327,550,50,0,true, 1, 0.3, 0, style1); container.appendChild(floor.shape); var ceil = new SVGRect(325,-33,649,80,0,true, 1, 0.3, 0, style1); container.appendChild(ceil.shape); var rampRight = new SVGRect(375,220,390,20,0.405,true, 1, 0.3, 0, style1); container.appendChild(rampRight.shape); var rampLeft = new SVGRect(90,200,102,20,-.7,true, 1, 0.3, 0, style1); container.appendChild(rampLeft.shape); var rampLeft2 = new SVGRect(96,129,102,20,-.7,true, 1, 0.3, 0, style1); container.appendChild(rampLeft2.shape); var rampCircle = new SVGCircle(175,190,60,true, 1, 0.3, 0, style2); container.appendChild(rampCircle.shape); var floorBump = new SVGCircle(600,660,400,true, 1, 0.3, 0, style2); container.appendChild(floorBump.shape); var bouncePad = new SVGRect(35,370,40,60,0,true, 1, 0.3, 0, style3); container.appendChild(bouncePad.shape); bouncePad.elasticity = 4; var leftWall = new SVGRect(1,99,30,500,0,true, 1, 0.3, 0, style1); container.appendChild(leftWall.shape); var leftWallChannelInner = new SVGRect(54,300,20,150,0,true, 1, 0.3, 0, style1); container.appendChild(leftWallChannelInner.shape); var leftWallChannel = new SVGRect(54,122,20,94,0,true, 1, 0.3, 0, style1); container.appendChild(leftWallChannel.shape); var leftWallChannelAng = new SVGRect(75,65,60,25,- 0.7,true, 1, 0.3, 0, style1); container.appendChild(leftWallChannelAng.shape); var topLeftAng = new SVGRect(23,11,65,40,-0.7,true, 1, 0.3, 0, style1); container.appendChild(topLeftAng.shape); var rightWall = new SVGRect(654,230,50,500,0,true, 1, 0.3, 0, style1); container.appendChild(rightWall.shape); var bridgeStart = new SVGRect(127,49,75,25,0,true, 1, 0.3, 0, style1); container.appendChild(bridgeStart.shape); var bridgeEnd = new SVGRect(483,55,100,15,0,true, 1, 0.3, 0, style1); container.appendChild(bridgeEnd.shape); this.particles = [floor, ceil, rampRight, rampLeft, rampLeft2, rampCircle, floorBump, bouncePad, leftWall, leftWallChannelInner, leftWallChannel, leftWallChannelAng, topLeftAng, rightWall, bridgeStart, bridgeEnd]; this.constraints = []; }; Surfaces.prototype = Util.concat(new Group(), { paint: function() {/*N/A*/} }); /** class SwingDoor extends Group */ var SwingDoor = function(style1, container) { Group.call(this); this.collideInternal = true; var style2 = new Style(2, style1.lineColor, style1.fillColor); var swingDoorP1 = new SVGCircle(543, 55, 7, false, 1, 0.3, 0, style1); container.appendChild(swingDoorP1.shape); swingDoorP1.mass = 0.001; var swingDoorP2 = new SVGCircle(620, 55, 7, true, 1, 0.3, 0, style1); container.appendChild(swingDoorP2.shape); var swingDoor = new Spring(swingDoorP1, swingDoorP2, 1); var swingDoorP = new SVGRectSpring(swingDoor, 13, 1, true, style2); swingDoor.collidableParticle = swingDoorP; container.appendChild(swingDoorP.shape); var swingDoorAnchor = new Circle(543, 5, 2, true, 1, 0.3, 0); swingDoorAnchor.collidable = false; var swingDoorSpring = new Spring(swingDoorP1, swingDoorAnchor, 0.02); swingDoorSpring.restLength = 40; var stopperA = new Circle(550, -60, 70, true, 1, 0.3, 0); var stopperB = new Rect(650, 130, 42, 70, 0, true, 1, 0.3, 0); this.particles = [swingDoorP1, swingDoorP2, swingDoorAnchor, stopperA, stopperB]; this.constraints = [swingDoor, swingDoorSpring]; }; SwingDoor.prototype = new Group(); ////////////////////////////////////////////////////////////////////// var demo = null; var enabled = true; function loaded() { var svg = document.createElementNS(SVG, "svg"); svg.setAttributeNS(null, "width", "450px"); svg.setAttributeNS(null, "height", "350px"); svg.setAttributeNS(null, "viewBox", "0 0 450 350"); svg.setAttributeNS(null, "version", "1.1"); var rect = document.createElementNS(SVG, "rect"); rect.setAttributeNS(null, "x", "0"); rect.setAttributeNS(null, "y", "0"); rect.setAttributeNS(null, "width", "450"); rect.setAttributeNS(null, "height", "350"); rect.setAttributeNS(null, "fill", "#334433"); svg.appendChild(rect); var g = document.createElementNS(SVG, "g"); svg.appendChild(g); demo = new CarDemo(g); document.body.appendChild(svg); window.setInterval(function() { if (enabled) { demo.run(); } }, 1000/30); } function onoff(button) { if (enabled) { enabled = false; button.innerHTML = "START"; } else { enabled = true; button.innerHTML = "STOP"; } } function lefton() { demo.keyDownHandler({keyCode:65}); } function leftoff() { demo.keyUpHandler(); } function righton() { demo.keyDownHandler({keyCode:68}); } function rightoff() { demo.keyUpHandler(); } loaded(); <div> <button id="onoff" onclick="onoff(this)">STOP</button> <button id="left" onmousedown="lefton()" onmouseup="leftoff()" ontouchstart="lefton()" ontouchend="leftoff()">LEFT</button> <button id="right" onmousedown="righton()" onmouseup="rightoff()" ontouchstart="righton()" ontouchend="rightoff()">RIGHT</button> </div> body { background-color: #DDDDDD; font: 12px sans-serif; } use an iframe compat browser, deer Play on jsdo.it games Share Embed QR Tag Download Complete! Description どんなゲームですか? Control Device スマートフォンコントローラー jsdo.it WebSocket Controller» マウス キーボード タッチデバイス Fullscreen 有効 無効 jsdo.it games から削除する Submit Tweet style Design view Code view code <script type="text/javascript" src="http://jsdo.it/blogparts/wpll/js?view=design"></script><p class="ttlBpJsdoit" style="width: 465px; margin: 0; text-align: right; font-size: 11px;"><a href="http://jsdo.it/flashrod/cardemo" title="Car Demo">Car Demo - jsdo.it - share JavaScript, HTML5 and CSS</a></p> Tweet twitter Tags 2d car game javascript svg Favorite by h6k k0rin gct256 kleinschmidt.. edo_m18 9re nehahelo ruriwo: javascriptsvg jscoder: 2dcargamesvg subrobot: 물리엔진 Forked sort new page view favorite forked forked: Car Demo Duong.Nguyen.. 00 18views 1593/6/1 forked: Car Demo mosanism 00 25views 1593/6/1 forked from: Car Demo K.Yoshida 00 113views 1593/6/1