Parse
File Parse constrain-distance.js
This runs the server-side parser and regenerates the documentation tree for this source file.
Source
const constraints = {
lte: (a, b) => a <= b
, gt: (a, b) => a > b
, any: (a, b) => true
, _ifDistance: function(pointA, pointB, maxDistance, action=gt) {
const d2d = pointA.distance2D(pointB)
, distance = d2d.distance
, r = action(distance, maxDistance)
;
if(r) {
/* Same as:
const ratio = maxDistance / distance;
pointB.x = pointA.x - d2d.x * ratio;
pointB.y = pointA.y - d2d.y * ratio;
*/
let t = pointA.subtract(
Point.from(d2d).multiply(maxDistance / distance)
)
pointB.copy(t)
};
return r
}
, distance(pointA, pointB, maxDistance) {
return this._ifDistance(pointA, pointB, maxDistance, this.any)
}
, within(pointA, pointB, maxDistance) {
return this._ifDistance(pointA, pointB, maxDistance, this.gt)
}
, inverse(pointA, pointB, maxDistance) {
/*
If the distance between A and B is greater than the given max distance,
move point B away from point A.
Or; Point B _avoids_ point A.
*/
return this._ifDistance(pointA, pointB, maxDistance, this.lte)
}
, cone(pointA, pointB, settings={}) {
/* Keep `pointB` inside an angular cone projected from `pointA`.
The cone is centered on `rotation` and extends `cone` degrees to either
side. If `pointB` drifts outside that arc we do not change its distance,
we only clamp its angle and project it back onto the same radius.
Example:
constraints.cone(hip, knee, { cone: 25 })
Settings:
cone: half-width of the allowed arc in degrees.
rotation: forward direction of the cone. Defaults to pointA.rotation.
distance: optional radius to project onto. Defaults to current A->B distance.
direction: flip the angle direction when using an inverted frame.
resist: how much of the correction stays on the child. `1` keeps the
parent fixed, `0` rotates the parent to absorb all overflow.
The steps are:
1. Measure the signed angle from the cone center to pointB.
2. If the angle is already inside the cone, do nothing.
3. Clamp the angle to the nearest cone edge.
4. Re-project pointB from pointA using the preserved distance.
*/
if(typeof(settings) == 'number') {
settings = { cone: settings }
}
const conf = Object.assign({
cone: pointB.cone
, rotation: pointA.rotation
, distance: undefined
, direction: 1
, resist: 1
}, settings)
if(conf.cone == undefined) {
return false
}
/* Angle is signed, so negative values are left of the cone center and
positive values are right of it. */
const angle = calculateAngle180(pointA, pointB, conf.rotation, conf.direction)
if(Math.abs(angle) <= conf.cone) {
return false
}
const distance = conf.distance == undefined
? pointA.distanceTo(pointB)
: conf.distance
if(distance == 0) {
return false
}
const resist = clamp(conf.resist, 0, 1)
/* Split the overflow correction between parent rotation and child
position. When resist is 1 the parent stays fixed. When resist is 0 the
parent rotates enough to keep the child where it was dragged. */
const lockedAngle = clamp(angle, -conf.cone, conf.cone)
const overflow = angle - lockedAngle
const rotationShift = overflow * (1 - resist) * conf.direction
pointA.rotation += rotationShift
const absoluteAngle = pointA.rotation + (lockedAngle * conf.direction)
pointB.copy(projectFrom(pointA, distance, absoluteAngle))
return true
}
}
const stringAttach = function(followPoint, originPoint, settings) {
// Apply gravity to the follow point's vertical velocity
// Calculate the vector from the originPoint to the follow point
// let dx = followPoint.x - originPoint.x ;
// let dy = followPoint.y - originPoint.y ;
const defaultSettings = {
gravity: {x:0, y:1}
, damping:.95
, dotDamping:.2
, forceMultiplier:.1
, forceValue:undefined
, distance: 100
}
const conf = Object.assign(defaultSettings, settings);
const gravity = conf.gravity
, damping = conf.damping
, dotDamping = conf.dotDamping
, forceMultiplier = conf.forceMultiplier
, forceValue = conf.forceValue
, stringLength = conf.distance
;
let dx = originPoint.x - followPoint.x;
let dy = originPoint.y - followPoint.y;
// Calculate the current distance between the follow point and the originPoint
let distance = Math.sqrt(dx * dx + dy * dy);
// Set the follow point's position to be exactly on the circumference of the string length
// const force = (distance - stringLength) * 0.01; // Tweak this factor as needed
// If the distance exceeds the string length, we need to constrain the follow point
if (distance > stringLength) {
// Normalize the direction vector
dx /= distance;
dy /= distance;
if(dotDamping!==false) {
// Adjust the velocity so that it reflects the string tension
let dotProduct = (followPoint.vx * dx + followPoint.vy * dy) * dotDamping;
followPoint.vx -= dotProduct * dx;
followPoint.vy -= dotProduct * dy;
}
if(forceMultiplier!==false){
const force = forceValue? forceValue: (distance - stringLength) * forceMultiplier; // Tweak this factor as needed
followPoint.vx += force * dx;
followPoint.vy += force * dy;
}
}
// Apply gravity
if(gravity){
followPoint.vy += gravity.y;
followPoint.vx += gravity.x;
}
// Update the follow point's position based on its velocity
followPoint.x += followPoint.vx;
followPoint.y += followPoint.vy;
if(damping) {
// Apply damping continuously to smooth the motion
followPoint.vx *= damping;
followPoint.vy *= damping;
}
};
class PointConstraints {
constructor(point) {
this.parent = point
/* string stuff */
this.gravity = {x:0, y:1}
this.damping =.95
this.dotDamping =.2
this.forceMultiplier =.1
this.forceValue =undefined
this.distance = 100
}
/* Track another point using IK - this point follows the _other_ at a
set distance. */
track(other, settings) {
// return followPoint(other, this, settings)
return constraints.distance(other, this.parent, settings)
}
/* Track another point using constraints. This point follows the other
point at a distance or less. */
leash(other, settings) {
return constraints.within(other, this.parent, settings)
}
/* Ensure this point does not overlap the _other_ point. If an overlap
occurs, this point is moved. Fundamentally this is the antethsis of leash().*/
avoid(other, settings) {
return constraints.inverse(other, this.parent, settings)
}
elbow(other, settings){
/* Connect this point and another point by their _edges_. Similar to
_track_ at a distance, but accounting for radius. */
let point = this.parent;
/* Ensure a point stays within a distance. */
this.leash(other, (other.radius + point.radius) - .01)
/* Ensure the point binds to the edge of the target. */
this.avoid(other, Math.abs(other.radius - point.radius) + .01)
}
string(other, settings){
// Manipulate the _other_; with this entity as the origin owner.
const c = Object.assign({
gravity: this.gravity
, damping: this.damping
, dotDamping: this.dotDamping
, forceMultiplier: this.forceMultiplier
, forceValue: this.forceValue
, distance: this.distance
}, settings)
return stringAttach(other, this.parent, c)
}
cone(other, settings, options=undefined) {
return constraints.cone(this.parent, other, mergeConeSettings(settings, options))
}
}
const mergeConeSettings = function(settings, options=undefined) {
if(options == undefined) {
return settings
}
if(typeof(settings) == 'number') {
return Object.assign({ cone: settings }, options)
}
return Object.assign({}, settings, options)
}
Polypoint.head.deferredProp('Point',
function constraint() {
return new PointConstraints(this)
}
);
Polypoint.head.installFunctions('Point', {
/* Track another point using IK - this point follows the _other_ at a
set distance. */
track(other, settings) {
// return followPoint(other, this, settings)
return constraints.distance(other, this, settings)
}
/* Track another point using constraints. This point follows the other
point at a distance or less. */
, leash(other, settings) {
return constraints.within(other, this, settings)
}
/* Ensure this point does not overlap the _other_ point. If an overlap
occurs, this point is moved. Fundamentally this is the antethsis of leash().*/
, avoid(other, settings) {
return constraints.inverse(other, this, settings)
}
, cone(other, settings, options=undefined) {
return constraints.cone(other, this, mergeConeSettings(settings, options))
}
})
// const _constrainDistance = function(pointA, pointB, maxDistance) {
// // Calculate the distance between pointA and pointB
// // const dx = pointA.x - pointB.x;
// // const dy = pointA.y - pointB.y;
// // const distance = Math.sqrt(dx * dx + dy * dy);
// const distance = pointA.distanceTo(pointB)
// const distance2D = pointA.distance2D(pointB)
// if(gt(distance, maxDistance)) {
// // const constrainedX = pointA.x - dx * ratio;
// // const constrainedY = pointA.y - dy * ratio;
// const ratio = maxDistance / distance;
// const constrainedX = pointA.x - distance2D.x * ratio;
// const constrainedY = pointA.y - distance2D.y * ratio;
// pointB.x = constrainedX;
// pointB.y = constrainedY;
// }
// }
// const _inverseConstrainDistance = function(pointA, pointB, maxDistance) {
// // Calculate the distance between pointA and pointB
// const dx = pointA.x - pointB.x;
// const dy = pointA.y - pointB.y;
// // const distance = Math.sqrt(dx * dx + dy * dy);
// const distance = pointA.distanceTo(pointB)
// const distance2D = pointA.distance2D(pointB)
// // If the distance is greater than the maxDistance, move pointB closer
// if(lte(distance, maxDistance) ) {
// const ratio = maxDistance / distance;
// const constrainedX = pointA.x - distance2D.x * ratio;
// const constrainedY = pointA.y - distance2D.y * ratio;
// pointB.x = constrainedX;
// pointB.y = constrainedY;
// }
// }
copy