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