UNPKG

littlejsengine

Version:

LittleJS - Tiny and Fast HTML5 Game Engine

1,358 lines (1,172 loc) 72.8 kB
/** * LittleJS Box2D Physics Plugin * - Box2dObject extends EngineObject with Box2D physics * - Call box2dInit() to enable * - You will also need to include box2d.wasm.js * - Uses a super fast web assembly port of Box2D v2.3.1 * - More info: https://github.com/kripken/box2d.js * - Functions to create polygon, circle, and edge shapes * - Contact begin and end callbacks * - Wraps b2Vec2 type to/from Vector2 * - Raycasting and querying * - Every type of joint * - Debug physics drawing * @namespace Box2D */ 'use strict'; /** Global Box2d Plugin object * @type {Box2dPlugin} * @memberof Box2D */ let box2d; /** Enable Box2D debug drawing * @type {boolean} * @default * @memberof Box2D */ let box2dDebug = false; /** Enable Box2D debug drawing * @param {boolean} enable * @memberof Box2D */ function box2dSetDebug(enable) { box2dDebug = enable; } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Object - extend with your own custom physics objects * - A LittleJS object with Box2D physics, dynamic by default * - Provides interface for Box2D body and fixture functions * - Each object can have multiple fixtures and joints * @extends EngineObject * @memberof Box2D */ class Box2dObject extends EngineObject { /** Create a LittleJS object with Box2d physics * @param {Vector2} [pos] * @param {Vector2} [size] * @param {TileInfo} [tileInfo] * @param {number} [angle] * @param {Color} [color] * @param {number} [bodyType] * @param {number} [renderOrder] */ constructor(pos=vec2(), size=vec2(), tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0) { super(pos, size, tileInfo, angle, color, renderOrder); // create physics body const bodyDef = new box2d.instance.b2BodyDef(); bodyDef.set_type(bodyType); bodyDef.set_position(box2d.vec2dTo(pos)); bodyDef.set_angle(-angle); this.body = box2d.world.CreateBody(bodyDef); this.body.object = this; this.lineColor = BLACK; box2d.objects.push(this); // edge lists and loops for drawing this.edgeLists = []; this.edgeLoops = []; } /** Destroy this object and its physics body */ destroy() { if (this.destroyed) return; // destroy physics body, fixtures, and joints ASSERT(this.body, 'Box2dObject has no body to destroy'); box2d.world.DestroyBody(this.body); super.destroy(); } /** Box2d objects updated with Box2d world step */ updatePhysics() {} /** Render the object, uses box2d drawing if no tile info exists */ render() { // use default render or draw fixtures if (this.tileInfo) super.render(); else this.drawFixtures(this.color, this.lineColor, this.lineWidth); } /** Render debug info */ renderDebugInfo() { const isAsleep = !this.getIsAwake(); const isStatic = this.getBodyType() === box2d.bodyTypeStatic; const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5); this.drawFixtures(color); } /** Draws all this object's fixtures * @param {Color} [color] * @param {Color} [lineColor] * @param {number} [lineWidth] * @param {CanvasRenderingContext2D} [context] */ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context) { // draw non-edge fixtures this.getFixtureList().forEach((fixture)=> { const shape = box2d.castObjectType(fixture.GetShape()); if (shape.GetType() !== box2d.instance.b2Shape.e_edge) { box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context); } }); // draw edges using a single draw line for better connections this.edgeLists.forEach(points=> drawLineList(points, lineWidth, lineColor, false, this.pos, this.angle)); this.edgeLoops.forEach(points=> drawLineList(points, lineWidth, lineColor, true, this.pos, this.angle)); } /////////////////////////////////////////////////////////////////////////////// // physics contact callbacks /** Called when a contact begins * @param {Box2dObject} otherObject */ beginContact(otherObject) {} /** Called when a contact ends * @param {Box2dObject} otherObject */ endContact(otherObject) {} /////////////////////////////////////////////////////////////////////////////// // physics fixtures and shapes /** Add a shape fixture to the body * @param {Object} shape * @param {number} [density] * @param {number} [friction] * @param {number} [restitution] * @param {boolean} [isSensor] */ addShape(shape, density=1, friction=.2, restitution=0, isSensor=false) { ASSERT(isNumber(density), 'density must be a number'); ASSERT(isNumber(friction), 'friction must be a number'); ASSERT(isNumber(restitution), 'restitution must be a number'); const fd = new box2d.instance.b2FixtureDef(); fd.set_shape(shape); fd.set_density(density); fd.set_friction(friction); fd.set_restitution(restitution); fd.set_isSensor(isSensor); return this.body.CreateFixture(fd); } /** Add a box shape to the body * @param {Vector2} [size] * @param {Vector2} [offset] * @param {number} [angle] * @param {number} [density] * @param {number} [friction] * @param {number} [restitution] * @param {boolean} [isSensor] */ addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor) { ASSERT(isVector2(size), 'size must be a Vector2'); ASSERT(size.x > 0 && size.y > 0, 'size must be positive'); ASSERT(isVector2(offset), 'offset must be a Vector2'); ASSERT(isNumber(angle), 'angle must be a number'); const shape = new box2d.instance.b2PolygonShape(); shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle); return this.addShape(shape, density, friction, restitution, isSensor); } /** Add a polygon shape to the body * @param {Array<Vector2>} points * @param {number} [density] * @param {number} [friction] * @param {number} [restitution] * @param {boolean} [isSensor] */ addPoly(points, density, friction, restitution, isSensor) { ASSERT(isArray(points), 'points must be an array'); function box2dCreatePolygonShape(points) { function box2dCreatePointList(points) { const buffer = box2d.instance._malloc(points.length * 8); for (let i=0, offset=0; i<points.length; ++i) { box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].x; offset += 4; box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y; offset += 4; } return box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2); } ASSERT(3 <= points.length && points.length <= 8); const shape = new box2d.instance.b2PolygonShape(); const box2dPoints = box2dCreatePointList(points); shape.Set(box2dPoints, points.length); return shape; } const shape = box2dCreatePolygonShape(points); return this.addShape(shape, density, friction, restitution, isSensor); } /** Add a regular polygon shape to the body * @param {number} [diameter] * @param {number} [sides] * @param {number} [density] * @param {number} [friction] * @param {number} [restitution] * @param {boolean} [isSensor] */ addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor) { ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number'); ASSERT(isNumber(sides) && sides>2, 'sides must be a positive number greater than 2'); const points = []; const radius = diameter/2; for (let i=sides; i--;) points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2)); return this.addPoly(points, density, friction, restitution, isSensor); } /** Add a random polygon shape to the body * @param {number} [diameter] * @param {number} [density] * @param {number} [friction] * @param {number} [restitution] * @param {boolean} [isSensor] */ addRandomPoly(diameter=1, density, friction, restitution, isSensor) { ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number'); const sides = randInt(3, 9); const points = []; const radius = diameter/2; for (let i=sides; i--;) points.push(vec2(rand(radius/2,radius*1.5),0).rotate(i/sides*PI*2)); return this.addPoly(points, density, friction, restitution, isSensor); } /** Add a circle shape to the body * @param {number} [diameter] * @param {Vector2} [offset] * @param {number} [density] * @param {number} [friction] * @param {number} [restitution] * @param {boolean} [isSensor] */ addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor) { ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number'); ASSERT(isVector2(offset), 'offset must be a Vector2'); const shape = new box2d.instance.b2CircleShape(); shape.set_m_p(box2d.vec2dTo(offset)); shape.set_m_radius(diameter/2); return this.addShape(shape, density, friction, restitution, isSensor); } /** Add an edge shape to the body * @param {Vector2} point1 * @param {Vector2} point2 * @param {number} [density] * @param {number} [friction] * @param {number} [restitution] * @param {boolean} [isSensor] */ addEdge(point1, point2, density, friction, restitution, isSensor) { ASSERT(isVector2(point1), 'point1 must be a Vector2'); ASSERT(isVector2(point2), 'point2 must be a Vector2'); const shape = new box2d.instance.b2EdgeShape(); shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2)); return this.addShape(shape, density, friction, restitution, isSensor); } /** Add an edge list to the body * @param {Array<Vector2>} points * @param {number} [density] * @param {number} [friction] * @param {number} [restitution] * @param {boolean} [isSensor] */ addEdgeList(points, density, friction, restitution, isSensor) { ASSERT(isArray(points), 'points must be an array'); const fixtures = [], edgePoints = []; for (let i=0; i<points.length-1; ++i) { const shape = new box2d.instance.b2EdgeShape(); points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1])); points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0])); points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1])); points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2])); const f = this.addShape(shape, density, friction, restitution, isSensor); fixtures.push(f); edgePoints.push(points[i].copy()); } edgePoints.push(points[points.length-1].copy()); this.edgeLists.push(edgePoints); return fixtures; } /** Add an edge loop to the body, an edge loop connects the end points * @param {Array<Vector2>} points * @param {number} [density] * @param {number} [friction] * @param {number} [restitution] * @param {boolean} [isSensor] */ addEdgeLoop(points, density, friction, restitution, isSensor) { ASSERT(isArray(points), 'points must be an array'); const fixtures = [], edgePoints = []; const getPoint = i=> points[mod(i,points.length)]; for (let i=0; i<points.length; ++i) { const shape = new box2d.instance.b2EdgeShape(); shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1))); shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0))); shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1))); shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2))); const f = this.addShape(shape, density, friction, restitution, isSensor); fixtures.push(f); i < points.length && edgePoints.push(points[i].copy()); } this.edgeLoops.push(edgePoints); return fixtures; } /////////////////////////////////////////////////////////////////////////////// // physics get functions /** Gets the center of mass * @return {Vector2} */ getCenterOfMass() { return box2d.vec2From(this.body.GetWorldCenter()); } /** Gets the linear velocity * @return {Vector2} */ getLinearVelocity() { return box2d.vec2From(this.body.GetLinearVelocity()); } /** Gets the angular velocity * @return {Vector2} */ getAngularVelocity() { return this.body.GetAngularVelocity(); } /** Gets the mass * @return {number} */ getMass() { return this.body.GetMass(); } /** Gets the rotational inertia * @return {number} */ getInertia() { return this.body.GetInertia(); } /** Check if this object is awake * @return {boolean} */ getIsAwake() { return this.body.IsAwake(); } /** Gets the physics body type * @return {number} */ getBodyType() { return this.body.GetType(); } /** Get the speed of this object * @return {number} */ getSpeed() { return this.getLinearVelocity().length(); } /////////////////////////////////////////////////////////////////////////////// // physics set functions /** Sets the position and angle * @param {Vector2} pos * @param {number} angle */ setTransform(pos, angle) { this.pos = pos; this.angle = angle; this.body.SetTransform(box2d.vec2dTo(pos), angle); } /** Sets the position * @param {Vector2} pos */ setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); } /** Sets the angle * @param {number} angle */ setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); } /** Sets the linear velocity * @param {Vector2} velocity */ setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); } /** Sets the angular velocity * @param {number} angularVelocity */ setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); } /** Sets the linear damping * @param {number} damping */ setLinearDamping(damping) { this.body.SetLinearDamping(damping); } /** Sets the angular damping * @param {number} damping */ setAngularDamping(damping) { this.body.SetAngularDamping(damping); } /** Sets the gravity scale * @param {number} [scale] */ setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); } /** Should be like a bullet for continuous collision detection? * @param {boolean} [isBullet] */ setBullet(isBullet=true) { this.body.SetBullet(isBullet); } /** Set the sleep state of the body * @param {boolean} [isAwake] */ setAwake(isAwake=true) { this.body.SetAwake(isAwake); } /** Set the physics body type * @param {number} type */ setBodyType(type) { this.body.SetType(type); } /** Set whether the body is allowed to sleep * @param {boolean} [isAllowed] */ setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); } /** Set whether the body can rotate * @param {boolean} [isFixed] */ setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); } /** Set the center of mass of the body * @param {Vector2} center */ setCenterOfMass(center) { this.setMassData(center) } /** Set the mass of the body * @param {number} mass */ setMass(mass) { this.setMassData(undefined, mass) } /** Set the moment of inertia of the body * @param {number} momentOfInertia */ setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) } /** Reset the mass, center of mass, and moment */ resetMassData() { this.body.ResetMassData(); } /** Set the mass data of the body * @param {Vector2} [localCenter] * @param {number} [mass] * @param {number} [momentOfInertia] */ setMassData(localCenter, mass, momentOfInertia) { const data = new box2d.instance.b2MassData(); this.body.GetMassData(data); localCenter && data.set_center(box2d.vec2dTo(localCenter)); mass && data.set_mass(mass); momentOfInertia && data.set_I(momentOfInertia); this.body.SetMassData(data); } /** Set the collision filter data for this body * @param {number} [categoryBits] * @param {number} [ignoreCategoryBits] * @param {number} [groupIndex] */ setFilterData(categoryBits=0, ignoreCategoryBits=0, groupIndex=0) { this.getFixtureList().forEach(fixture=> { const filter = fixture.GetFilterData(); filter.set_categoryBits(categoryBits); filter.set_maskBits(0xffff & ~ignoreCategoryBits); filter.set_groupIndex(groupIndex); }); } /** Set if this body is a sensor * @param {boolean} [isSensor] */ setSensor(isSensor=true) { this.getFixtureList().forEach(f=>f.SetSensor(isSensor)); } /////////////////////////////////////////////////////////////////////////////// // physics force and torque functions /** Apply force to this object * @param {Vector2} force * @param {Vector2} [pos] */ applyForce(force, pos) { pos ||= this.getCenterOfMass(); this.setAwake(); this.body.ApplyForce(box2d.vec2dTo(force), box2d.vec2dTo(pos)); } /** Apply acceleration to this object * @param {Vector2} acceleration * @param {Vector2} [pos] */ applyAcceleration(acceleration, pos) { pos ||= this.getCenterOfMass(); this.setAwake(); this.body.ApplyLinearImpulse(box2d.vec2dTo(acceleration), box2d.vec2dTo(pos)); } /** Apply torque to this object * @param {number} torque */ applyTorque(torque) { this.setAwake(); this.body.ApplyTorque(torque); } /** Apply angular acceleration to this object * @param {number} acceleration */ applyAngularAcceleration(acceleration) { this.setAwake(); this.body.ApplyAngularImpulse(acceleration); } /////////////////////////////////////////////////////////////////////////////// // lists of fixtures and joints /** Check if this object has any fixtures * @return {boolean} */ hasFixtures() { return !box2d.isNull(this.body.GetFixtureList()); } /** Get list of fixtures for this object * @return {Array<Object>} */ getFixtureList() { const fixtures = []; for (let fixture=this.body.GetFixtureList(); !box2d.isNull(fixture); ) { fixtures.push(fixture); fixture = fixture.GetNext(); } return fixtures; } /** Check if this object has any joints * @return {boolean} */ hasJoints() { return !box2d.isNull(this.body.GetJointList()); } /** Get list of joints for this object * @return {Array<Object>} */ getJointList() { const joints = []; for (let joint=this.body.GetJointList(); !box2d.isNull(joint); ) { joints.push(joint); joint = joint.get_next(); } return joints; } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Raycast Result * - Holds results from a box2d raycast queries * - Automatically created by box2d raycast functions */ class Box2dRaycastResult { /** Create a raycast result * @param {Object} fixture * @param {Vector2} point * @param {Vector2} normal * @param {number} fraction */ constructor(fixture, point, normal, fraction) { /** @property {Box2dObject} - The box2d object */ this.object = fixture.GetBody().object; /** @property {Object} - The fixture that was hit */ this.fixture = fixture; /** @property {Vector2} - The hit point */ this.point = point; /** @property {Vector2} - The hit normal */ this.normal = normal; /** @property {number} - Distance fraction at the point of intersection */ this.fraction = fraction; } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Joint * - Base class for Box2D joints * - A joint is used to connect objects together * @memberof Box2D */ class Box2dJoint { /** Create a box2d joint, the base class is not intended to be used directly * @param {Object} jointDef */ constructor(jointDef) { this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef)); } /** Destroy this joint */ destroy() { box2d.world.DestroyJoint(this.box2dJoint); this.box2dJoint = 0; } /** Get the first object attached to this joint * @return {Box2dObject} */ getObjectA() { return this.box2dJoint.GetBodyA().object; } /** Get the second object attached to this joint * @return {Box2dObject} */ getObjectB() { return this.box2dJoint.GetBodyB().object; } /** Get the first anchor for this joint in world coordinates * @return {Vector2} */ getAnchorA() { return box2d.vec2From(this.box2dJoint.GetAnchorA());} /** Get the second anchor for this joint in world coordinates * @return {Vector2} */ getAnchorB() { return box2d.vec2From(this.box2dJoint.GetAnchorB());} /** Get the reaction force on bodyB at the joint anchor given a time step * @param {number} time * @return {Vector2} */ getReactionForce(time) { return box2d.vec2From(this.box2dJoint.GetReactionForce(1/time));} /** Get the reaction torque on bodyB in N*m given a time step * @param {number} time * @return {number} */ getReactionTorque(time) { return this.box2dJoint.GetReactionTorque(1/time);} /** Check if the connected bodies should collide * @return {boolean} */ getCollideConnected() { return this.box2dJoint.getCollideConnected();} /** Check if either connected body is active * @return {boolean} */ isActive() { return this.box2dJoint.IsActive();} } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Target Joint, also known as a mouse joint * - Used to make a point on a object track a specific world point target * - This a soft constraint with a max force * - This allows the constraint to stretch and without applying huge forces * @extends Box2dJoint * @memberof Box2D */ class Box2dTargetJoint extends Box2dJoint { /** Create a target joint * @param {Box2dObject} object * @param {Box2dObject} fixedObject * @param {Vector2} worldPos */ constructor(object, fixedObject, worldPos) { object.setAwake(); const jointDef = new box2d.instance.b2MouseJointDef(); jointDef.set_bodyA(fixedObject.body); jointDef.set_bodyB(object.body); jointDef.set_target(box2d.vec2dTo(worldPos)); jointDef.set_maxForce(2e3 * object.getMass()); super(jointDef); } /** Set the target point in world coordinates * @param {Vector2} pos */ setTarget(pos) { this.box2dJoint.SetTarget(box2d.vec2dTo(pos)); } /** Get the target point in world coordinates * @return {Vector2} */ getTarget(){ return box2d.vec2From(this.box2dJoint.GetTarget()); } /** Sets the maximum force in Newtons * @param {number} force */ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); } /** Gets the maximum force in Newtons * @return {number} */ getMaxForce() { return this.box2dJoint.GetMaxForce(); } /** Sets the joint frequency in Hertz * @param {number} hz */ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); } /** Gets the joint frequency in Hertz * @return {number} */ getFrequency() { return this.box2dJoint.GetFrequency(); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Distance Joint * - Constrains two points on two objects to remain at a fixed distance * - You can view this as a massless, rigid rod * @extends Box2dJoint * @memberof Box2D */ class Box2dDistanceJoint extends Box2dJoint { /** Create a distance joint * @param {Box2dObject} objectA * @param {Box2dObject} objectB * @param {Vector2} anchorA * @param {Vector2} anchorB * @param {boolean} [collide] */ constructor(objectA, objectB, anchorA, anchorB, collide=false) { anchorA ||= box2d.vec2From(objectA.body.GetPosition()); anchorB ||= box2d.vec2From(objectB.body.GetPosition()); const localAnchorA = objectA.worldToLocal(anchorA); const localAnchorB = objectB.worldToLocal(anchorB); const jointDef = new box2d.instance.b2DistanceJointDef(); jointDef.set_bodyA(objectA.body); jointDef.set_bodyB(objectB.body); jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA)); jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB)); jointDef.set_length(anchorA.distance(anchorB)); jointDef.set_collideConnected(collide); super(jointDef); } /** Get the local anchor point relative to objectA's origin * @return {Vector2} */ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); } /** Get the local anchor point relative to objectB's origin * @return {Vector2} */ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); } /** Set the length of the joint * @param {number} length */ setLength(length) { this.box2dJoint.SetLength(length); } /** Get the length of the joint * @return {number} */ getLength() { return this.box2dJoint.GetLength(); } /** Set the frequency in Hertz * @param {number} hz */ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); } /** Get the frequency in Hertz * @return {number} */ getFrequency() { return this.box2dJoint.GetFrequency(); } /** Set the damping ratio * @param {number} ratio */ setDampingRatio(ratio) { this.box2dJoint.SetDampingRatio(ratio); } /** Get the damping ratio * @return {number} */ getDampingRatio() { return this.box2dJoint.GetDampingRatio(); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Pin Joint * - Pins two objects together at a point * @extends Box2dDistanceJoint * @memberof Box2D */ class Box2dPinJoint extends Box2dDistanceJoint { /** Create a pin joint * @param {Box2dObject} objectA * @param {Box2dObject} objectB * @param {Vector2} [pos] * @param {boolean} [collide] */ constructor(objectA, objectB, pos=objectA.pos, collide=false) { super(objectA, objectB, undefined, pos, collide); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Rope Joint * - Enforces a maximum distance between two points on two objects * @extends Box2dJoint * @memberof Box2D */ class Box2dRopeJoint extends Box2dJoint { /** Create a rope joint * @param {Box2dObject} objectA * @param {Box2dObject} objectB * @param {Vector2} anchorA * @param {Vector2} anchorB * @param {number} extraLength * @param {boolean} [collide] */ constructor(objectA, objectB, anchorA, anchorB, extraLength=0, collide=false) { anchorA ||= box2d.vec2From(objectA.body.GetPosition()); anchorB ||= box2d.vec2From(objectB.body.GetPosition()); const localAnchorA = objectA.worldToLocal(anchorA); const localAnchorB = objectB.worldToLocal(anchorB); const jointDef = new box2d.instance.b2RopeJointDef(); jointDef.set_bodyA(objectA.body); jointDef.set_bodyB(objectB.body); jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA)); jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB)); jointDef.set_maxLength(anchorA.distance(anchorB)+extraLength); jointDef.set_collideConnected(collide); super(jointDef); } /** Get the local anchor point relative to objectA's origin * @return {Vector2} */ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); } /** Get the local anchor point relative to objectB's origin * @return {Vector2} */ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); } /** Set the max length of the joint * @param {number} length */ setMaxLength(length) { this.box2dJoint.SetMaxLength(length); } /** Get the max length of the joint * @return {number} */ getMaxLength() { return this.box2dJoint.GetMaxLength(); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Revolute Joint * - Constrains two objects to share a point while they are free to rotate around the point * - The relative rotation about the shared point is the joint angle * - You can limit the relative rotation with a joint limit * - You can use a motor to drive the relative rotation about the shared point * - A maximum motor torque is provided so that infinite forces are not generated * @extends Box2dJoint * @memberof Box2D */ class Box2dRevoluteJoint extends Box2dJoint { /** Create a revolute joint * @param {Box2dObject} objectA * @param {Box2dObject} objectB * @param {Vector2} anchor * @param {boolean} [collide] */ constructor(objectA, objectB, anchor, collide=false) { anchor ||= box2d.vec2From(objectB.body.GetPosition()); const localAnchorA = objectA.worldToLocal(anchor); const localAnchorB = objectB.worldToLocal(anchor); const jointDef = new box2d.instance.b2RevoluteJointDef(); jointDef.set_bodyA(objectA.body); jointDef.set_bodyB(objectB.body); jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA)); jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB)); jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle()); jointDef.set_collideConnected(collide); super(jointDef); } /** Get the local anchor point relative to objectA's origin * @return {Vector2} */ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); } /** Get the local anchor point relative to objectB's origin * @return {Vector2} */ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); } /** Get the reference angle, objectB angle minus objectA angle in the reference state * @return {number} */ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); } /** Get the current joint angle * @return {number} */ getJointAngle() { return this.box2dJoint.GetJointAngle(); } /** Get the current joint angle speed in radians per second * @return {number} */ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); } /** Is the joint limit enabled? * @return {boolean} */ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); } /** Enable/disable the joint limit * @param {boolean} [enable] */ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); } /** Get the lower joint limit * @return {number} */ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); } /** Get the upper joint limit * @return {number} */ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); } /** Set the joint limits * @param {number} min * @param {number} max */ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); } /** Is the joint motor enabled? * @return {boolean} */ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); } /** Enable/disable the joint motor * @param {boolean} [enable] */ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); } /** Set the motor speed * @param {number} speed */ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); } /** Get the motor speed * @return {number} */ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); } /** Set the motor torque * @param {number} torque */ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); } /** Get the max motor torque * @return {number} */ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); } /** Get the motor torque given a time step * @param {number} time * @return {number} */ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Gear Joint * - A gear joint is used to connect two joints together * - Either joint can be a revolute or prismatic joint * - You specify a gear ratio to bind the motions together * @extends Box2dJoint * @memberof Box2D */ class Box2dGearJoint extends Box2dJoint { /** Create a gear joint * @param {Box2dObject} objectA * @param {Box2dObject} objectB * @param {Box2dJoint} joint1 * @param {Box2dJoint} joint2 * @param {ratio} [ratio] */ constructor(objectA, objectB, joint1, joint2, ratio=1) { const jointDef = new box2d.instance.b2GearJointDef(); jointDef.set_bodyA(objectA.body); jointDef.set_bodyB(objectB.body); jointDef.set_joint1(joint1.box2dJoint); jointDef.set_joint2(joint2.box2dJoint); jointDef.set_ratio(ratio); super(jointDef); this.joint1 = joint1; this.joint2 = joint2; } /** Get the first joint * @return {Box2dJoint} */ getJoint1() { return this.joint1; } /** Get the second joint * @return {Box2dJoint} */ getJoint2() { return this.joint2; } /** Set the gear ratio * @param {number} ratio */ setRatio(ratio) { return this.box2dJoint.SetRatio(ratio); } /** Get the gear ratio * @return {number} */ getRatio() { return this.box2dJoint.GetRatio(); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Prismatic Joint * - Provides one degree of freedom: translation along an axis fixed in objectA * - Relative rotation is prevented * - You can use a joint limit to restrict the range of motion * - You can use a joint motor to drive the motion or to model joint friction * @extends Box2dJoint * @memberof Box2D */ class Box2dPrismaticJoint extends Box2dJoint { /** Create a prismatic joint * @param {Box2dObject} objectA * @param {Box2dObject} objectB * @param {Vector2} anchor * @param {Vector2} worldAxis * @param {boolean} [collide] */ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false) { anchor ||= box2d.vec2From(objectB.body.GetPosition()); const localAnchorA = objectA.worldToLocal(anchor); const localAnchorB = objectB.worldToLocal(anchor); const localAxisA = objectB.worldToLocalVector(worldAxis); const jointDef = new box2d.instance.b2PrismaticJointDef(); jointDef.set_bodyA(objectA.body); jointDef.set_bodyB(objectB.body); jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA)); jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB)); jointDef.set_localAxisA(box2d.vec2dTo(localAxisA)); jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle()); jointDef.set_collideConnected(collide); super(jointDef); } /** Get the local anchor point relative to objectA's origin * @return {Vector2} */ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); } /** Get the local anchor point relative to objectB's origin * @return {Vector2} */ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); } /** Get the local joint axis relative to bodyA * @return {Vector2} */ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); } /** Get the reference angle * @return {number} */ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); } /** Get the current joint translation * @return {number} */ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); } /** Get the current joint translation speed * @return {number} */ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); } /** Is the joint limit enabled? * @return {boolean} */ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); } /** Enable/disable the joint limit * @param {boolean} [enable] */ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); } /** Get the lower joint limit * @return {number} */ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); } /** Get the upper joint limit * @return {number} */ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); } /** Set the joint limits * @param {number} min * @param {number} max */ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); } /** Is the motor enabled? * @return {boolean} */ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); } /** Enable/disable the joint motor * @param {boolean} [enable] */ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); } /** Set the motor speed * @param {number} speed */ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); } /** Get the motor speed * @return {number} */ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); } /** Set the maximum motor force * @param {number} force */ setMaxMotorForce(force) { return this.box2dJoint.SetMaxMotorForce(force); } /** Get the maximum motor force * @return {number} */ getMaxMotorForce() { return this.box2dJoint.GetMaxMotorForce(); } /** Get the motor force given a time step * @param {number} time * @return {number} */ getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Static Object - Box2d with a static physics body * @extends Box2dObject * @memberof Box2D */ class Box2dStaticObject extends Box2dObject { /** Create a LittleJS object with Box2d physics * @param {Vector2} [pos] * @param {Vector2} [size] * @param {TileInfo} [tileInfo] * @param {number} [angle] * @param {Color} [color] * @param {number} [renderOrder] */ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0) { const bodyType = box2d.bodyTypeStatic; super(pos, size, tileInfo, angle, color, bodyType, renderOrder); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Kiematic Object - Box2d with a kinematic physics body * @extends Box2dObject * @memberof Box2D */ class Box2dKiematicObject extends Box2dObject { /** Create a LittleJS object with Box2d physics * @param {Vector2} [pos] * @param {Vector2} [size] * @param {TileInfo} [tileInfo] * @param {number} [angle] * @param {Color} [color] * @param {number} [renderOrder] */ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0) { const bodyType = box2d.bodyTypeKinematic; super(pos, size, tileInfo, angle, color, bodyType, renderOrder); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Wheel Joint * - Provides two degrees of freedom: translation along an axis fixed in objectA and rotation * - You can use a joint limit to restrict the range of motion * - You can use a joint motor to drive the motion or to model joint friction * - This joint is designed for vehicle suspensions * @extends Box2dJoint * @memberof Box2D */ class Box2dWheelJoint extends Box2dJoint { /** Create a wheel joint * @param {Box2dObject} objectA * @param {Box2dObject} objectB * @param {Vector2} anchor * @param {Vector2} worldAxis * @param {boolean} [collide] */ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false) { anchor ||= box2d.vec2From(objectB.body.GetPosition()); const localAnchorA = objectA.worldToLocal(anchor); const localAnchorB = objectB.worldToLocal(anchor); const localAxisA = objectB.worldToLocalVector(worldAxis); const jointDef = new box2d.instance.b2WheelJointDef(); jointDef.set_bodyA(objectA.body); jointDef.set_bodyB(objectB.body); jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA)); jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB)); jointDef.set_localAxisA(box2d.vec2dTo(localAxisA)); jointDef.set_collideConnected(collide); super(jointDef); } /** Get the local anchor point relative to objectA's origin * @return {Vector2} */ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); } /** Get the local anchor point relative to objectB's origin * @return {Vector2} */ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); } /** Get the local joint axis relative to bodyA * @return {Vector2} */ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); } /** Get the current joint translation * @return {number} */ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); } /** Get the current joint translation speed * @return {number} */ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); } /** Is the joint motor enabled? * @return {boolean} */ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); } /** Enable/disable the joint motor * @param {boolean} [enable] */ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); } /** Set the motor speed * @param {number} speed */ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); } /** Get the motor speed * @return {number} */ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); } /** Set the maximum motor torque * @param {number} torque */ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); } /** Get the max motor torque * @return {number} */ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); } /** Get the motor torque for a time step * @return {number} */ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); } /** Set the spring frequency in Hertz * @param {number} hz */ setSpringFrequencyHz(hz) { return this.box2dJoint.SetSpringFrequencyHz(hz); } /** Get the spring frequency in Hertz * @return {number} */ getSpringFrequencyHz() { return this.box2dJoint.GetSpringFrequencyHz(); } /** Set the spring damping ratio * @param {number} ratio */ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); } /** Get the spring damping ratio * @return {number} */ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Weld Joint * - Glues two objects together * @extends Box2dJoint * @memberof Box2D */ class Box2dWeldJoint extends Box2dJoint { /** Create a weld joint * @param {Box2dObject} objectA * @param {Box2dObject} objectB * @param {Vector2} anchor * @param {boolean} [collide] */ constructor(objectA, objectB, anchor, collide=false) { anchor ||= box2d.vec2From(objectB.body.GetPosition()); const localAnchorA = objectA.worldToLocal(anchor); const localAnchorB = objectB.worldToLocal(anchor); const jointDef = new box2d.instance.b2WeldJointDef(); jointDef.set_bodyA(objectA.body); jointDef.set_bodyB(objectB.body); jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA)); jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB)); jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle()); jointDef.set_collideConnected(collide); super(jointDef); } /** Get the local anchor point relative to objectA's origin * @return {Vector2} */ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); } /** Get the local anchor point relative to objectB's origin * @return {Vector2} */ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); } /** Get the reference angle * @return {number} */ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); } /** Set the frequency in Hertz * @param {number} hz */ setFrequency(hz) { return this.box2dJoint.SetFrequency(hz); } /** Get the frequency in Hertz * @return {number} */ getFrequency() { return this.box2dJoint.GetFrequency(); } /** Set the damping ratio * @param {number} ratio */ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); } /** Get the damping ratio * @return {number} */ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); } } /////////////////////////////////////////////////////////////////////////////// /** * Box2D Friction Joint * - Used to apply top-down friction * - Provides 2D translational friction and angular friction * @extends Box2dJoint * @memberof Box2D */ class Box2dFrictionJoint extends Box2dJoint { /** Create a friction joint * @param {Box2dObject} objectA * @param {Box2dObject} objectB * @param {Vector2} anchor * @param {boolean} [collide] */ constructor(objectA, objectB, anchor, collide=false) { anchor ||= box2d.vec2From(objectB.body.GetPosition()); const localAnchorA = objectA.worldToLocal(anchor); const localAnchorB = objectB.worldToLocal(anchor); const jointDef = new box2d.instance.b2FrictionJointDef(); jointDef.set_bodyA(objectA.body); jointDef.set_bodyB(objectB.body); jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA)); jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB)); jointDef.set_collideConnected(collide); super(jointDef); } /** Get the local anchor point relative to objectA's origin * @return {Vector2} */ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); } /** Get the local anchor point relative to objectB's origin * @return {Vector2} */ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); } /** Set the maximum friction force * @param {number} force */ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); } /** Get the maximum friction force * @return {number} */ getMaxForce() { return this.box2dJoint.GetMaxForce(); } /** Set the maximum friction torque * @param {number} torque */ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); } /** Get the maximum friction torque *