From 0a59f9d5bbc06d88e4995ce483ec4afccdb896d1 Mon Sep 17 00:00:00 2001 From: liabru Date: Wed, 11 Mar 2020 20:57:06 +0000 Subject: [PATCH 01/12] added Common.angleDiff, Common.clampAngle --- src/core/Common.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/core/Common.js b/src/core/Common.js index 895fa390..ccbe3ead 100644 --- a/src/core/Common.js +++ b/src/core/Common.js @@ -16,6 +16,9 @@ module.exports = Common; Common._warnedOnce = {}; Common._decomp = null; + Common._2PI = 2 * Math.PI; + Common._3PI = 3 * Math.PI; + /** * Extends the object in the first argument using the object in the second argument. * @method extend @@ -251,6 +254,43 @@ module.exports = Common; Common.sign = function(value) { return value < 0 ? -1 : 1; }; + + /** + * Returns the normalised signed difference between the two angles in radians [-PI, PI]. + * @method angleDiff + * @param {number} angleA the first angle in radians + * @param {number} angleB the second angle in radians + * @return {number} the normalised signed difference + */ + Common.angleDiff = function(angleA, angleB) { + return ((((angleA - angleB) % Common._2PI) + Common._3PI) % Common._2PI) - Math.PI; + }; + + /** + * Clamps the given `angle` in radians between `angleMin` and `angleMax` inclusive. + * @method clampAngle + * @param {number} angleMin the angle minimum in radians + * @param {number} angleMax the angle maximum in radians + * @return {number} the clamped angle + */ + Common.clampAngle = function(angle, angleMin, angleMax) { + if (angleMax - angleMin === 0) { + return angleMax; + } + + var deltaMin = Common.angleDiff(angle, angleMin); + var deltaMax = Common.angleDiff(angle, angleMax); + + if (deltaMin > 0 && deltaMax < 0) { + return angle; + } + + if (Math.abs(deltaMin) < Math.abs(deltaMax)) { + return angle - deltaMin; + } + + return angle - deltaMax; + }; /** * Returns the current timestamp since the time origin (e.g. from page load). From 3d332a80b4fcf0f48b2a0c55f9148ff55da8af03 Mon Sep 17 00:00:00 2001 From: liabru Date: Wed, 11 Mar 2020 22:11:28 +0000 Subject: [PATCH 02/12] added angle constraints --- src/constraint/Constraint.js | 227 ++++++++++++++++++++++++++++-- src/constraint/MouseConstraint.js | 2 +- 2 files changed, 215 insertions(+), 14 deletions(-) diff --git a/src/constraint/Constraint.js b/src/constraint/Constraint.js index cebdbc58..72624139 100644 --- a/src/constraint/Constraint.js +++ b/src/constraint/Constraint.js @@ -23,6 +23,7 @@ var Common = require('../core/Common'); Constraint._warming = 0.4; Constraint._torqueDampen = 1; + Constraint._angleLimitDampen = 0.5; Constraint._minLength = 0.000001; /** @@ -48,8 +49,9 @@ var Common = require('../core/Common'); // 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)); - + length = Vector.magnitude(Vector.sub(initialPointA, initialPointB)), + angle = Vector.angle(initialPointA, initialPointB); + constraint.length = typeof constraint.length !== 'undefined' ? constraint.length : length; // option defaults @@ -59,17 +61,32 @@ var Common = require('../core/Common'); 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.angleAPrev = constraint.bodyA ? constraint.bodyA.angle : constraint.angleAPrev; + constraint.angleBPrev = constraint.bodyB ? constraint.bodyB.angle : constraint.angleBPrev; constraint.plugin = {}; + constraint.angleA = typeof constraint.angleA !== 'undefined' ? + constraint.angleA : (constraint.bodyA ? 0 : angle); + + constraint.angleAStiffness = constraint.angleAStiffness || 0; + constraint.angleAMin = constraint.angleAMin || 0; + constraint.angleAMax = constraint.angleAMax || 0; + + constraint.angleB = typeof constraint.angleB !== 'undefined' ? + constraint.angleB : (constraint.bodyB ? 0 : angle); + + constraint.angleBStiffness = constraint.angleBStiffness || 0; + constraint.angleBMin = constraint.angleBMin || 0; + constraint.angleBMax = constraint.angleBMax || 0; + // render var render = { visible: true, lineWidth: 2, strokeStyle: '#ffffff', type: 'line', - anchors: true + anchors: true, + angles: true }; if (constraint.length === 0 && constraint.stiffness > 0.1) { @@ -154,14 +171,14 @@ var Common = require('../core/Common'); // update reference angle if (bodyA && !bodyA.isStatic) { - Vector.rotate(pointA, bodyA.angle - constraint.angleA, pointA); - constraint.angleA = bodyA.angle; + Vector.rotate(pointA, bodyA.angle - constraint.angleAPrev, pointA); + constraint.angleAPrev = bodyA.angle; } // update reference angle if (bodyB && !bodyB.isStatic) { - Vector.rotate(pointB, bodyB.angle - constraint.angleB, pointB); - constraint.angleB = bodyB.angle; + Vector.rotate(pointB, bodyB.angle - constraint.angleBPrev, pointB); + constraint.angleBPrev = bodyB.angle; } var pointAWorld = pointA, @@ -188,11 +205,29 @@ var Common = require('../core/Common'); massTotal = (bodyA ? bodyA.inverseMass : 0) + (bodyB ? bodyB.inverseMass : 0), inertiaTotal = (bodyA ? bodyA.inverseInertia : 0) + (bodyB ? bodyB.inverseInertia : 0), resistanceTotal = massTotal + inertiaTotal, + angleAStiffness = (constraint.angleAStiffness < 1 ? constraint.angleAStiffness * timeScale : constraint.angleAStiffness) * Constraint._angleLimitDampen, + angleBStiffness = (constraint.angleBStiffness < 1 ? constraint.angleBStiffness * timeScale : constraint.angleBStiffness) * Constraint._angleLimitDampen, torque, share, normal, normalVelocity, - relativeVelocity; + relativeVelocity, + angleLimitPointA, + angleLimitPointB; + + if (constraint.angleAStiffness) { + angleLimitPointA = Constraint.solveAngleLimits( + constraint, bodyA, constraint.angleA, constraint.angleAMin, + constraint.angleAMax, delta, -1, currentLength, pointAWorld, pointBWorld + ); + } + + if (constraint.angleBStiffness) { + angleLimitPointB = Constraint.solveAngleLimits( + constraint, bodyB, constraint.angleB, constraint.angleBMin, + constraint.angleBMax, delta, 1, currentLength, pointBWorld, pointAWorld + ); + } if (constraint.damping) { var zero = Vector.create(); @@ -209,6 +244,12 @@ var Common = require('../core/Common'); if (bodyA && !bodyA.isStatic) { share = bodyA.inverseMass / massTotal; + // temporarily add angular limit force from pointB if pinned + if (angleLimitPointB && (!bodyB || bodyB.isStatic)) { + force.x -= angleLimitPointB.x * angleBStiffness; + force.y -= angleLimitPointB.y * angleBStiffness; + } + // keep track of applied impulses for post solving bodyA.constraintImpulse.x -= force.x * share; bodyA.constraintImpulse.y -= force.y * share; @@ -223,15 +264,34 @@ var Common = require('../core/Common'); bodyA.positionPrev.y -= constraint.damping * normal.y * normalVelocity * share; } - // apply torque + // find torque to apply torque = (Vector.cross(pointA, force) / resistanceTotal) * Constraint._torqueDampen * bodyA.inverseInertia * (1 - constraint.angularStiffness); + + // add any torque from angular limit at pointA + if (angleLimitPointA) { + torque -= angleLimitPointA.angle * angleAStiffness; + } + + // apply torque bodyA.constraintImpulse.angle -= torque; bodyA.angle -= torque; + + // remove angular limit from pointB + if (angleLimitPointB && (!bodyB || bodyB.isStatic)) { + force.x += angleLimitPointB.x * angleBStiffness; + force.y += angleLimitPointB.y * angleBStiffness; + } } if (bodyB && !bodyB.isStatic) { share = bodyB.inverseMass / massTotal; + // add angular limit force from pointA if pinned + if (angleLimitPointA && (!bodyA || bodyA.isStatic)) { + force.x += angleLimitPointA.x * angleAStiffness; + force.y += angleLimitPointA.y * angleAStiffness; + } + // keep track of applied impulses for post solving bodyB.constraintImpulse.x += force.x * share; bodyB.constraintImpulse.y += force.y * share; @@ -246,8 +306,15 @@ var Common = require('../core/Common'); bodyB.positionPrev.y += constraint.damping * normal.y * normalVelocity * share; } - // apply torque + // find torque to apply torque = (Vector.cross(pointB, force) / resistanceTotal) * Constraint._torqueDampen * bodyB.inverseInertia * (1 - constraint.angularStiffness); + + // add any torque from angular limit at pointB + if (angleLimitPointB) { + torque += angleLimitPointB.angle * angleBStiffness; + } + + // apply torque bodyB.constraintImpulse.angle += torque; bodyB.angle += torque; } @@ -329,6 +396,46 @@ var Common = require('../core/Common'); + (constraint.pointB ? constraint.pointB.y : 0) }; }; + + /** + * Solves angle limits on the constraint. + * @private + * @method solveAngleLimits + */ + Constraint.solveAngleLimits = function(constraint, body, angle, angleMin, angleMax, delta, deltaScale, currentLength, pointAWorld, pointBWorld) { + var currentAngle = (body ? body.angle : 0) + (constraint.length > 0 ? angle : 0), + min = angleMin < angleMax ? angleMin : angleMax, + max = angleMax > angleMin ? angleMax : angleMin, + angleNormal = { x: Math.cos(currentAngle), y: Math.sin(currentAngle) }, + angleDelta; + + if (constraint.length === 0) { + // use absolute angle for pin constraints + angleDelta = Common.angleDiff(angle, currentAngle); + } else { + // otherwise use relative angle + angleDelta = Math.atan2( + angleNormal.x * delta.y * deltaScale - angleNormal.y * delta.x * deltaScale, + angleNormal.x * delta.x * deltaScale + angleNormal.y * delta.y * deltaScale + ); + } + + // no impulse required if angle within limits + if (angleDelta > min && angleDelta < max) { + return null; + } + + // find the clamped angle and clamp the normal + var angleClamped = Common.clampAngle(angleDelta, min, max), + normalLimited = Vector.rotate(angleNormal, angleClamped); + + // return the impulses required to correct the angle + return { + x: pointAWorld.x + normalLimited.x * currentLength - pointBWorld.x, + y: pointAWorld.y + normalLimited.y * currentLength - pointBWorld.y, + angle: Common.angleDiff(angleDelta, angleClamped) + }; + }; /* * @@ -337,7 +444,7 @@ var Common = require('../core/Common'); */ /** - * An integer `Number` uniquely identifying number generated in `Composite.create` by `Common.nextId`. + * An integer `Number` uniquely identifying number generated in `Constraint.create` by `Common.nextId`. * * @property id * @type number @@ -411,6 +518,14 @@ var Common = require('../core/Common'); * @default true */ + /** + * **ALPHA**: A `Boolean` that defines if the constraint's anglular limits should be rendered. + * + * @property render.angles + * @type boolean + * @default true + */ + /** * The first possible `Body` that this constraint is attached to. * @@ -473,6 +588,92 @@ var Common = require('../core/Common'); * @type number */ + /** + * **ALPHA**: A `Number` in radians that specifies the limiting angle of the constraint about `constraint.pointA`. + * It is relative to `constraint.bodyA.angle` if `constraint.bodyA` is set, otherwise is absolute. + * Defaults to the initial angle of the constraint or `0`. + * Only applies if `constraint.angleAStiffness > 0`. + * The constraint angle is measured between the vector `pointA - pointB` and the x-axis. + * + * @property angleA + * @type number + * @default the initial relative constraint angle + */ + + /** + * **ALPHA**: A `Number` that specifies the stiffness of angular limits about `constraint.pointA`. + * A value of `0` (default) means the constraint will not limit the angle. + * A value of `0.01` means the constraint will softly limit the angle. + * A value of `1` means the constraint will rigidly limit the angle. + * + * @property angleAStiffness + * @type number + * @default 0 + */ + + /** + * **ALPHA**: A `Number` in radians that specifies the lower angular limit + * about `constraint.pointA` relative to `constraint.angleA`. + * A value of `-0.5` means the constraint is limited to `0.5` radians + * anti-clockwise of `constraint.angleA`, or clockwise if the value is positive. + * + * @property angleAMin + * @type number + * @default 0 + */ + + /** + * **ALPHA**: A `Number` in radians that specifies the upper angular limit + * about `constraint.pointA` relative to `constraint.angleA`. See `constraint.angleAMin` for more. + * + * @property angleAMax + * @type number + * @default 0 + */ + + /** + * **ALPHA**: A `Number` in radians that specifies the limiting angle of the constraint about `constraint.pointB`. + * It is relative to `constraint.bodyB.angle` if `constraint.bodyB` is set, otherwise is absolute. + * Defaults to the initial angle of the constraint or `0`. + * Only applies if `constraint.angleBStiffness > 0`. + * The constraint angle is measured between the vector `pointA - pointB` and the x-axis. + * + * @property angleB + * @type number + * @default the initial relative constraint angle + */ + + /** + * **ALPHA**: A `Number` that specifies the stiffness of angular limits about `constraint.pointB`. + * A value of `0` (default) means the constraint will not limit the angle. + * A value of `0.01` means the constraint will softly limit the angle. + * A value of `1` means the constraint will rigidly limit the angle. + * + * @property angleBStiffness + * @type number + * @default 0 + */ + + /** + * **ALPHA**: A `Number` in radians that specifies the lower angular limit + * about `constraint.pointB` relative to `constraint.angleB`. + * A value of `-0.5` means the constraint is limited to `0.5` radians + * anti-clockwise of `constraint.angleB`, or clockwise if the value is positive. + * + * @property angleBMin + * @type number + * @default 0 + */ + + /** + * **ALPHA**: A `Number` in radians that specifies the upper angular limit + * about `constraint.pointB` relative to `constraint.angleB`. See `constraint.angleBMin` for more. + * + * @property angleBMax + * @type number + * @default 0 + */ + /** * An object reserved for storing plugin-specific properties. * diff --git a/src/constraint/MouseConstraint.js b/src/constraint/MouseConstraint.js index 4dc55610..7326d4d5 100644 --- a/src/constraint/MouseConstraint.js +++ b/src/constraint/MouseConstraint.js @@ -107,7 +107,7 @@ var Bounds = require('../geometry/Bounds'); constraint.pointA = mouse.position; constraint.bodyB = mouseConstraint.body = body; constraint.pointB = { x: mouse.position.x - body.position.x, y: mouse.position.y - body.position.y }; - constraint.angleB = body.angle; + constraint.angleBPrev = body.angle; Sleeping.set(body, false); Events.trigger(mouseConstraint, 'startdrag', { mouse: mouse, body: body }); From 7229408e1688cbc365c2c973aa5696e954e64f72 Mon Sep 17 00:00:00 2001 From: liabru Date: Wed, 11 Mar 2020 22:12:01 +0000 Subject: [PATCH 03/12] added angle constraint rendering to Matter.Render --- src/render/Render.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/render/Render.js b/src/render/Render.js index ae52040e..8b7ea95d 100644 --- a/src/render/Render.js +++ b/src/render/Render.js @@ -694,6 +694,32 @@ var Mouse = require('../core/Mouse'); c.closePath(); c.fill(); } + + if (constraint.render.angles && constraint.angleAStiffness) { + var relativeAngle = (constraint.length > 0 && constraint.bodyA ? constraint.bodyA.angle : 0) + constraint.angleA; + c.beginPath(); + c.moveTo(start.x, start.y); + c.arc(start.x, start.y, 12, relativeAngle + constraint.angleAMin, + relativeAngle + constraint.angleAMax + 0.0001, constraint.angleAMin <= constraint.angleAMax); + c.lineTo(start.x, start.y); + c.closePath(); + c.strokeStyle = 'rgba(255,255,255,' + (0.3 + 0.3 * constraint.angleAStiffness) + ')'; + c.lineWidth = 1.2; + c.stroke(); + } + + if (constraint.render.angles && constraint.angleBStiffness) { + var relativeAngle = (constraint.length > 0 && constraint.bodyB ? constraint.bodyB.angle : 0) + constraint.angleB; + c.beginPath(); + c.moveTo(end.x, end.y); + c.arc(end.x, end.y, 12, relativeAngle + constraint.angleBMin, + relativeAngle + constraint.angleBMax + 0.0001, constraint.angleBMin <= constraint.angleBMax); + c.lineTo(end.x, end.y); + c.closePath(); + c.strokeStyle = 'rgba(255,255,255,' + (0.3 + 0.3 * constraint.angleBStiffness) + ')'; + c.lineWidth = 1.2; + c.stroke(); + } } }; From 1224e077bea35bf0959216b55e1cfdac4e5e523d Mon Sep 17 00:00:00 2001 From: liabru Date: Wed, 11 Mar 2020 22:12:28 +0000 Subject: [PATCH 04/12] added angle constraints example --- demo/index.html | 24 ++-- demo/js/Demo.js | 106 +++++++++++++++++ examples/angleConstraints.js | 216 +++++++++++++++++++++++++++++++++++ 3 files changed, 337 insertions(+), 9 deletions(-) create mode 100644 demo/js/Demo.js create mode 100644 examples/angleConstraints.js diff --git a/demo/index.html b/demo/index.html index 666da7aa..a4278e09 100644 --- a/demo/index.html +++ b/demo/index.html @@ -1,14 +1,20 @@ - - - - - - - - + + + + + + + + Matter.js Demo