mirror of
https://github.com/liabru/matter-js.git
synced 2025-01-13 16:18:50 -05:00
452 lines
15 KiB
JavaScript
452 lines
15 KiB
JavaScript
/**
|
|
* The `Matter.Constraint` module contains methods for creating and manipulating constraints.
|
|
* Constraints are used for specifying that a fixed distance must be maintained between two bodies (or a body and a fixed world-space position).
|
|
* The stiffness of constraints can be modified to create springs or elastic.
|
|
*
|
|
* See the included usage [examples](https://github.com/liabru/matter-js/tree/master/examples).
|
|
*
|
|
* @class Constraint
|
|
*/
|
|
|
|
var Constraint = {};
|
|
|
|
module.exports = Constraint;
|
|
|
|
var Vertices = require('../geometry/Vertices');
|
|
var Vector = require('../geometry/Vector');
|
|
var Sleeping = require('../core/Sleeping');
|
|
var Bounds = require('../geometry/Bounds');
|
|
var Axes = require('../geometry/Axes');
|
|
var Common = require('../core/Common');
|
|
|
|
(function() {
|
|
|
|
Constraint._warming = 0.4;
|
|
Constraint._torqueDampen = 1;
|
|
Constraint._minLength = 0.000001;
|
|
|
|
/**
|
|
* Creates a new constraint.
|
|
* All properties have default values, and many are pre-calculated automatically based on other properties.
|
|
* To simulate a revolute constraint (or pin joint) set `length: 0` and a high `stiffness` value (e.g. `0.7` or above).
|
|
* If the constraint is unstable, try lowering the `stiffness` value and / or increasing `engine.constraintIterations`.
|
|
* See the properties section below for detailed information on what you can pass via the `options` object.
|
|
* @method create
|
|
* @param {} options
|
|
* @return {constraint} constraint
|
|
*/
|
|
Constraint.create = function(options) {
|
|
var constraint = options;
|
|
|
|
// if bodies defined but no points, use body centre
|
|
if (constraint.bodyA && !constraint.pointA)
|
|
constraint.pointA = { x: 0, y: 0 };
|
|
if (constraint.bodyB && !constraint.pointB)
|
|
constraint.pointB = { x: 0, y: 0 };
|
|
|
|
// calculate static length using initial world space points
|
|
var initialPointA = constraint.bodyA ? Vector.add(constraint.bodyA.position, constraint.pointA) : constraint.pointA,
|
|
initialPointB = constraint.bodyB ? Vector.add(constraint.bodyB.position, constraint.pointB) : constraint.pointB,
|
|
length = Vector.magnitude(Vector.sub(initialPointA, initialPointB));
|
|
|
|
constraint.length = typeof constraint.length !== 'undefined' ? constraint.length : length;
|
|
|
|
// option defaults
|
|
constraint.id = constraint.id || Common.nextId();
|
|
constraint.label = constraint.label || 'Constraint';
|
|
constraint.type = 'constraint';
|
|
constraint.stiffness = constraint.stiffness || (constraint.length > 0 ? 1 : 0.7);
|
|
constraint.damping = constraint.damping || 0;
|
|
constraint.angularStiffness = constraint.angularStiffness || 0;
|
|
constraint.angleA = constraint.bodyA ? constraint.bodyA.angle : constraint.angleA;
|
|
constraint.angleB = constraint.bodyB ? constraint.bodyB.angle : constraint.angleB;
|
|
constraint.plugin = {};
|
|
|
|
// render
|
|
var render = {
|
|
visible: true,
|
|
lineWidth: 2,
|
|
strokeStyle: '#ffffff',
|
|
type: 'line',
|
|
anchors: true
|
|
};
|
|
|
|
if (constraint.length === 0 && constraint.stiffness > 0.1) {
|
|
render.type = 'pin';
|
|
render.anchors = false;
|
|
} else if (constraint.stiffness < 0.9) {
|
|
render.type = 'spring';
|
|
}
|
|
|
|
constraint.render = Common.extend(render, constraint.render);
|
|
|
|
return constraint;
|
|
};
|
|
|
|
/**
|
|
* Prepares for solving by constraint warming.
|
|
* @private
|
|
* @method preSolveAll
|
|
* @param {body[]} bodies
|
|
*/
|
|
Constraint.preSolveAll = function(bodies) {
|
|
for (var i = 0; i < bodies.length; i += 1) {
|
|
var body = bodies[i],
|
|
impulse = body.constraintImpulse;
|
|
|
|
if (body.isStatic || (impulse.x === 0 && impulse.y === 0 && impulse.angle === 0)) {
|
|
continue;
|
|
}
|
|
|
|
body.position.x += impulse.x;
|
|
body.position.y += impulse.y;
|
|
body.angle += impulse.angle;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Solves all constraints in a list of collisions.
|
|
* @private
|
|
* @method solveAll
|
|
* @param {constraint[]} constraints
|
|
* @param {number} timeScale
|
|
*/
|
|
Constraint.solveAll = function(constraints, timeScale) {
|
|
// Solve fixed constraints first.
|
|
for (var i = 0; i < constraints.length; i += 1) {
|
|
var constraint = constraints[i],
|
|
fixedA = !constraint.bodyA || (constraint.bodyA && constraint.bodyA.isStatic),
|
|
fixedB = !constraint.bodyB || (constraint.bodyB && constraint.bodyB.isStatic);
|
|
|
|
if (fixedA || fixedB) {
|
|
Constraint.solve(constraints[i], timeScale);
|
|
}
|
|
}
|
|
|
|
// Solve free constraints last.
|
|
for (i = 0; i < constraints.length; i += 1) {
|
|
constraint = constraints[i];
|
|
fixedA = !constraint.bodyA || (constraint.bodyA && constraint.bodyA.isStatic);
|
|
fixedB = !constraint.bodyB || (constraint.bodyB && constraint.bodyB.isStatic);
|
|
|
|
if (!fixedA && !fixedB) {
|
|
Constraint.solve(constraints[i], timeScale);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Solves a distance constraint with Gauss-Siedel method.
|
|
* @private
|
|
* @method solve
|
|
* @param {constraint} constraint
|
|
* @param {number} timeScale
|
|
*/
|
|
Constraint.solve = function(constraint, timeScale) {
|
|
var bodyA = constraint.bodyA,
|
|
bodyB = constraint.bodyB,
|
|
pointA = constraint.pointA,
|
|
pointB = constraint.pointB;
|
|
|
|
if (!bodyA && !bodyB)
|
|
return;
|
|
|
|
// update reference angle
|
|
if (bodyA && !bodyA.isStatic) {
|
|
Vector.rotate(pointA, bodyA.angle - constraint.angleA, pointA);
|
|
constraint.angleA = bodyA.angle;
|
|
}
|
|
|
|
// update reference angle
|
|
if (bodyB && !bodyB.isStatic) {
|
|
Vector.rotate(pointB, bodyB.angle - constraint.angleB, pointB);
|
|
constraint.angleB = bodyB.angle;
|
|
}
|
|
|
|
var pointAWorld = pointA,
|
|
pointBWorld = pointB;
|
|
|
|
if (bodyA) pointAWorld = Vector.add(bodyA.position, pointA);
|
|
if (bodyB) pointBWorld = Vector.add(bodyB.position, pointB);
|
|
|
|
if (!pointAWorld || !pointBWorld)
|
|
return;
|
|
|
|
var delta = Vector.sub(pointAWorld, pointBWorld),
|
|
currentLength = Vector.magnitude(delta);
|
|
|
|
// prevent singularity
|
|
if (currentLength < Constraint._minLength) {
|
|
currentLength = Constraint._minLength;
|
|
}
|
|
|
|
// solve distance constraint with Gauss-Siedel method
|
|
var difference = (currentLength - constraint.length) / currentLength,
|
|
stiffness = constraint.stiffness < 1 ? constraint.stiffness * timeScale : constraint.stiffness,
|
|
force = Vector.mult(delta, difference * stiffness),
|
|
massTotal = (bodyA ? bodyA.inverseMass : 0) + (bodyB ? bodyB.inverseMass : 0),
|
|
inertiaTotal = (bodyA ? bodyA.inverseInertia : 0) + (bodyB ? bodyB.inverseInertia : 0),
|
|
resistanceTotal = massTotal + inertiaTotal,
|
|
torque,
|
|
share,
|
|
normal,
|
|
normalVelocity,
|
|
relativeVelocity;
|
|
|
|
if (constraint.damping) {
|
|
var zero = Vector.create();
|
|
normal = Vector.div(delta, currentLength);
|
|
|
|
relativeVelocity = Vector.sub(
|
|
bodyB && Vector.sub(bodyB.position, bodyB.positionPrev) || zero,
|
|
bodyA && Vector.sub(bodyA.position, bodyA.positionPrev) || zero
|
|
);
|
|
|
|
normalVelocity = Vector.dot(normal, relativeVelocity);
|
|
}
|
|
|
|
if (bodyA && !bodyA.isStatic) {
|
|
share = bodyA.inverseMass / massTotal;
|
|
|
|
// keep track of applied impulses for post solving
|
|
bodyA.constraintImpulse.x -= force.x * share;
|
|
bodyA.constraintImpulse.y -= force.y * share;
|
|
|
|
// apply forces
|
|
bodyA.position.x -= force.x * share;
|
|
bodyA.position.y -= force.y * share;
|
|
|
|
// apply damping
|
|
if (constraint.damping) {
|
|
bodyA.positionPrev.x -= constraint.damping * normal.x * normalVelocity * share;
|
|
bodyA.positionPrev.y -= constraint.damping * normal.y * normalVelocity * share;
|
|
}
|
|
|
|
// apply torque
|
|
torque = (Vector.cross(pointA, force) / resistanceTotal) * Constraint._torqueDampen * bodyA.inverseInertia * (1 - constraint.angularStiffness);
|
|
bodyA.constraintImpulse.angle -= torque;
|
|
bodyA.angle -= torque;
|
|
}
|
|
|
|
if (bodyB && !bodyB.isStatic) {
|
|
share = bodyB.inverseMass / massTotal;
|
|
|
|
// keep track of applied impulses for post solving
|
|
bodyB.constraintImpulse.x += force.x * share;
|
|
bodyB.constraintImpulse.y += force.y * share;
|
|
|
|
// apply forces
|
|
bodyB.position.x += force.x * share;
|
|
bodyB.position.y += force.y * share;
|
|
|
|
// apply damping
|
|
if (constraint.damping) {
|
|
bodyB.positionPrev.x += constraint.damping * normal.x * normalVelocity * share;
|
|
bodyB.positionPrev.y += constraint.damping * normal.y * normalVelocity * share;
|
|
}
|
|
|
|
// apply torque
|
|
torque = (Vector.cross(pointB, force) / resistanceTotal) * Constraint._torqueDampen * bodyB.inverseInertia * (1 - constraint.angularStiffness);
|
|
bodyB.constraintImpulse.angle += torque;
|
|
bodyB.angle += torque;
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Performs body updates required after solving constraints.
|
|
* @private
|
|
* @method postSolveAll
|
|
* @param {body[]} bodies
|
|
*/
|
|
Constraint.postSolveAll = function(bodies) {
|
|
for (var i = 0; i < bodies.length; i++) {
|
|
var body = bodies[i],
|
|
impulse = body.constraintImpulse;
|
|
|
|
if (body.isStatic || (impulse.x === 0 && impulse.y === 0 && impulse.angle === 0)) {
|
|
continue;
|
|
}
|
|
|
|
Sleeping.set(body, false);
|
|
|
|
// update geometry and reset
|
|
for (var j = 0; j < body.parts.length; j++) {
|
|
var part = body.parts[j];
|
|
|
|
Vertices.translate(part.vertices, impulse);
|
|
|
|
if (j > 0) {
|
|
part.position.x += impulse.x;
|
|
part.position.y += impulse.y;
|
|
}
|
|
|
|
if (impulse.angle !== 0) {
|
|
Vertices.rotate(part.vertices, impulse.angle, body.position);
|
|
Axes.rotate(part.axes, impulse.angle);
|
|
if (j > 0) {
|
|
Vector.rotateAbout(part.position, impulse.angle, body.position, part.position);
|
|
}
|
|
}
|
|
|
|
Bounds.update(part.bounds, part.vertices, body.velocity);
|
|
}
|
|
|
|
// dampen the cached impulse for warming next step
|
|
impulse.angle *= Constraint._warming;
|
|
impulse.x *= Constraint._warming;
|
|
impulse.y *= Constraint._warming;
|
|
}
|
|
};
|
|
|
|
/*
|
|
*
|
|
* Properties Documentation
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* An integer `Number` uniquely identifying number generated in `Composite.create` by `Common.nextId`.
|
|
*
|
|
* @property id
|
|
* @type number
|
|
*/
|
|
|
|
/**
|
|
* A `String` denoting the type of object.
|
|
*
|
|
* @property type
|
|
* @type string
|
|
* @default "constraint"
|
|
* @readOnly
|
|
*/
|
|
|
|
/**
|
|
* An arbitrary `String` name to help the user identify and manage bodies.
|
|
*
|
|
* @property label
|
|
* @type string
|
|
* @default "Constraint"
|
|
*/
|
|
|
|
/**
|
|
* An `Object` that defines the rendering properties to be consumed by the module `Matter.Render`.
|
|
*
|
|
* @property render
|
|
* @type object
|
|
*/
|
|
|
|
/**
|
|
* A flag that indicates if the constraint should be rendered.
|
|
*
|
|
* @property render.visible
|
|
* @type boolean
|
|
* @default true
|
|
*/
|
|
|
|
/**
|
|
* A `Number` that defines the line width to use when rendering the constraint outline.
|
|
* A value of `0` means no outline will be rendered.
|
|
*
|
|
* @property render.lineWidth
|
|
* @type number
|
|
* @default 2
|
|
*/
|
|
|
|
/**
|
|
* A `String` that defines the stroke style to use when rendering the constraint outline.
|
|
* It is the same as when using a canvas, so it accepts CSS style property values.
|
|
*
|
|
* @property render.strokeStyle
|
|
* @type string
|
|
* @default a random colour
|
|
*/
|
|
|
|
/**
|
|
* A `String` that defines the constraint rendering type.
|
|
* The possible values are 'line', 'pin', 'spring'.
|
|
* An appropriate render type will be automatically chosen unless one is given in options.
|
|
*
|
|
* @property render.type
|
|
* @type string
|
|
* @default 'line'
|
|
*/
|
|
|
|
/**
|
|
* A `Boolean` that defines if the constraint's anchor points should be rendered.
|
|
*
|
|
* @property render.anchors
|
|
* @type boolean
|
|
* @default true
|
|
*/
|
|
|
|
/**
|
|
* The first possible `Body` that this constraint is attached to.
|
|
*
|
|
* @property bodyA
|
|
* @type body
|
|
* @default null
|
|
*/
|
|
|
|
/**
|
|
* The second possible `Body` that this constraint is attached to.
|
|
*
|
|
* @property bodyB
|
|
* @type body
|
|
* @default null
|
|
*/
|
|
|
|
/**
|
|
* A `Vector` that specifies the offset of the constraint from center of the `constraint.bodyA` if defined, otherwise a world-space position.
|
|
*
|
|
* @property pointA
|
|
* @type vector
|
|
* @default { x: 0, y: 0 }
|
|
*/
|
|
|
|
/**
|
|
* A `Vector` that specifies the offset of the constraint from center of the `constraint.bodyA` if defined, otherwise a world-space position.
|
|
*
|
|
* @property pointB
|
|
* @type vector
|
|
* @default { x: 0, y: 0 }
|
|
*/
|
|
|
|
/**
|
|
* A `Number` that specifies the stiffness of the constraint, i.e. the rate at which it returns to its resting `constraint.length`.
|
|
* A value of `1` means the constraint should be very stiff.
|
|
* A value of `0.2` means the constraint acts like a soft spring.
|
|
*
|
|
* @property stiffness
|
|
* @type number
|
|
* @default 1
|
|
*/
|
|
|
|
/**
|
|
* A `Number` that specifies the damping of the constraint,
|
|
* i.e. the amount of resistance applied to each body based on their velocities to limit the amount of oscillation.
|
|
* Damping will only be apparent when the constraint also has a very low `stiffness`.
|
|
* A value of `0.1` means the constraint will apply heavy damping, resulting in little to no oscillation.
|
|
* A value of `0` means the constraint will apply no damping.
|
|
*
|
|
* @property damping
|
|
* @type number
|
|
* @default 0
|
|
*/
|
|
|
|
/**
|
|
* A `Number` that specifies the target resting length of the constraint.
|
|
* It is calculated automatically in `Constraint.create` from initial positions of the `constraint.bodyA` and `constraint.bodyB`.
|
|
*
|
|
* @property length
|
|
* @type number
|
|
*/
|
|
|
|
/**
|
|
* An object reserved for storing plugin-specific properties.
|
|
*
|
|
* @property plugin
|
|
* @type {}
|
|
*/
|
|
|
|
})();
|