littlejsengine
Version:
LittleJS - Tiny and Fast HTML5 Game Engine
1,362 lines (1,175 loc) • 76.7 kB
JavaScript
/**
* 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
* - Box2dTileLayer for grid based collision
* - 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);
/** @property {Object} - The Box2d body */
this.body = box2d.world.CreateBody(bodyDef);
/** @property {Color} - Line color used for default box2d drawing */
this.lineColor = BLACK;
/** @property {Array<Object>} - List of all edges for default box2d drawing */
this.edgeLists = [];
/** @property {Array<Object>} - List of all edge loops for default box2d drawing */
this.edgeLoops = [];
this.body.object = this; // link body to this object
box2d.objects.push(this); // keep track of all box2d objects
}
/** 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 {boolean} [useWebGL=glEnable]
* @param {CanvasRenderingContext2D} [context] */
drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, 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, useWebGL, 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;
}
/** Destroy a fixture from the body
* @param {Object} [fixture] */
destroyFixture(fixture) { this.body.DestroyFixture(fixture); }
/** Destroy all fixture from the body */
destroyAllFixtures()
{ this.getFixtureList().forEach(fixture=>this.destroyFixture(fixture)); }
///////////////////////////////////////////////////////////////////////////////
// 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 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 Kinematic Object - Box2d with a kinematic physics body
* @extends Box2dObject
* @memberof Box2D
*/
class Box2dKinematicObject 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 Tile Layer
* - adds Box2d support to tile layers
* - creates static box2d fixtures for solid tiles
* @extends Box2dObject
* @memberof Box2D
*/
class Box2dTileLayer extends Box2dStaticObject
{
/** Create a Box2d tile layer object
* @param {TileCollisionLayer} tileLayer - Tile layer for this object */
constructor(tileLayer)
{
ASSERT(tileLayer instanceof TileCollisionLayer, 'tileLayer must be a TileCollisionLayer');
super(tileLayer.pos, tileLayer.size);
/** @property {TileLayer} - The tile layer */
this.tileLayer = tileLayer;
this.addChild(tileLayer);
}
render()
{
// do not render fixtures, tile layer handles rendering
}
/** Create box2d collision fixtures for solid tiles
* @param {number} [friction]
* @param {number} [restitution] */
buildCollision(friction=.2, restitution=0)
{
// destroy all fixtures and create new ones
this.destroyAllFixtures();
// create box2d object for this layer
this.pos = this.tileLayer.pos.copy();
this.size = this.tileLayer.size.copy();
// track which tiles have been processed
const processed = [];
const getIndex = (x, y)=> x + y * this.size.x;
const isSolidUnprocessed = (x, y)=>
!processed[getIndex(x, y)] &&
this.tileLayer.getCollisionData(vec2(x, y)) > 0;
// combine tiles into larger boxes
for (let x = 0; x < this.size.x; ++x)
for (let y = 0; y < this.size.y; ++y)
{
if (!isSolidUnprocessed(x, y)) continue;
// find max width by scanning right
let width = 1, height = 1, canExpand = true;
while (isSolidUnprocessed(x + width, y))
++width;
// find max height by scanning up, ensuring all rows have the same width
while (canExpand)
{
for (let checkX = 0; checkX < width; ++checkX)
{
if (!isSolidUnprocessed(x + checkX, y + height))
{
canExpand = false;
break;
}
}
if (canExpand)
++height;
}
// mark all tiles in this rectangle as processed
for (let rectX = width; rectX--;)
for (let rectY = height; rectY--;)
processed[getIndex(x + rectX, y + rectY)] = true;
// create a single fixture for the entire rectangle
const shapeSize = vec2(width, height);
const offset = vec2(x + width/2, y + height/2);
this.addBox(shapeSize, offset, 0, 0, friction, restitution);
}
}
}
///////////////////////////////////////////////////////////////////////////////
/**
* 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)
{
/** @property {Object} - The Box2d joint */
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 {number} [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 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