Parse

File Parse cluster.js

This runs the server-side parser and regenerates the documentation tree for this source file.

Source

            /*
Perform Point clustering, ensuring many points gravitate to their siblings,
but at a general distance.
*/


const simpleCluster = function(points, targetPosition, settings, opts) {
    /* the simpleCluster function quickly initiates _clustering_ on a collection
    of points, using optional settings.

    Example:

        simpleCluster(points, pointA, settings, {func: clusterStyleB, itercount: c});

    Synonymous to:

        let c = settings.itercount
        if(c == undefined) { c = 10 }
        for (var i = 0; i < c; i++) {
            clusterStyleB(points, pointA, settings);
            // clusterStable(points, pointA, settings);
        }
    */

    // Generally "stable" settings.
    let defaultSettings = {
            minDistance: 100
            , attractionStrength: 0.001
            , repulsionStrength: 80
            , damping: 0.974
            , minVelocity: 0.01
            , maxVelocity: 9
            , itercount: 1
            // , func
        }

    settings = Object.assign(defaultSettings, settings, opts)
    let func =  settings.func || clusterStable // clusterStyleB
    let c = settings.itercount
    if(c == undefined) { c = 1 };

    for (var i = 0; i < c; i++) {
        // clusterStable(points, pointA, settings);
        func(points, targetPosition, settings);
    }
}


function clusterStyleB(items, focul, settings) {
    const centerX = focul == undefined? canvas.width * .5: focul.x
        , centerY = focul == undefined? canvas.height * .5: focul.y
        , minDistance = settings.minDistance
        , minVelocity = settings.minVelocity
        , maxVelocity = settings.maxVelocity
        , damping = settings.damping
        , repulsionStrength = settings.repulsionStrength
        , attractionStrength = settings.attractionStrength
        , method = settings.method || 'general'
        ;

    const _xyd = function(item) {
        const dx = centerX - item.x;
        const dy = centerY - item.y;
        const distance = item.distanceTo({x:centerX, y:centerY})
        return [dx,dy,distance]
    }

    const m = {

        springy(item) {
            // springy
            let xyd = _xyd(item)
            const at = (xyd[2] * .0001) + attractionStrength

            item.vx += at * xyd[0];
            item.vy += at * xyd[1];

        }
        , general(item){
            // general
            let xyd = _xyd(item)
            const at = (xyd[2] * .0001) + attractionStrength

            item.vx += at * xyd[0];
            item.vy += at * xyd[1];

        }
        , squareDistance(item) {
            // square distance
            let xyd = _xyd(item)
            // const dx = centerX - item.x;
            // const dy = centerY - item.y;
            // const distance = item.distanceTo({x:centerX, y:centerY}) * .0004
            const distance = xyd[2] * .0004

            const at = (attractionStrength * .01) +  ((distance * distance) * .03)

            item.vx +=  at * xyd[0];
            item.vy +=  at * xyd[1];
        }
    }

    const oneItem = function(items, i) {

        let item = items[i]
        for (let j = 0; j < items.length; j++) {
            if (i == j) {
                continue
            }

            const other = items[j]
            const dxy = other.subtract(item)
            const distance = item.distanceTo(other)

            if (distance > minDistance) {
                /* If the distance is too far, we dont perform the
                replusion force below ...*/
                continue
            }

            const repulsionForce = repulsionStrength / (distance * distance);
            item.vx -= repulsionForce * dxy.x / distance;
            item.vy -= repulsionForce * dxy.y / distance;
        }

        // m.general(item)
        // m.squareDistance(item)
        m[method](item)

        item.vx *= damping;
        item.vy *= damping;

        limitSpeed(minVelocity, maxVelocity, item)

        item.x += item.vx;
        item.y += item.vy;
    }

    for (let i = 0; i < items.length; i++) {
        oneItem(items, i)
    }
}


function limitSpeed(minVelocity, maxVelocity, item) {

        // Apply limits to total speed so neither threshold changes the heading.
        const speed = Math.hypot(item.vx, item.vy)

        if (speed < minVelocity) {
            item.vx = 0
            item.vy = 0
        } else if (speed > maxVelocity && speed > 0) {
            const scale = maxVelocity / speed
            item.vx *= scale
            item.vy *= scale
        }

        return item
}


function limitSpeedCardinal(minVelocity, maxVelocity, item) {

        // Limits each axis independently, biasing headings toward the 8 cardinal directions.
        let vx = item.vx
        let vy = item.vy
        let avx = Math.abs(vx)
        let avy = Math.abs(vy)

        if (avx < minVelocity){ item.vx = 0;}
        if (avy < minVelocity){ item.vy = 0;}

        if (avx > maxVelocity){ item.vx = Math.sign(vx) * maxVelocity;}
        if (avy > maxVelocity){ item.vy = Math.sign(vy) * maxVelocity;}

        return item
}


function clusterStable(items, focul, settings) {
    const centerX = focul.x
        , centerY = focul.y
        , minDistance = settings.minDistance
        , minVelocity = settings.minVelocity
        , maxVelocity = settings.maxVelocity
        , damping = settings.damping
        , repulsionStrength = settings.repulsionStrength
        , attractionStrength = settings.attractionStrength
        ;

    for (let i = 0; i < items.length; i++) {
        let item = items[i]
        for (let j = 0; j < items.length; j++) {
            if (i == j) {
                continue
            }
            const other = items[j]
            const dxy = other.subtract(item)
            const distance = item.distanceTo(other)

            if (distance > minDistance) {
                /* If the distance is too far, we dont perform the
                replusion force below ...*/
                continue
            }

            const repulsionForce = repulsionStrength / (distance * distance);
            item.vx -= repulsionForce * dxy.x / distance;
            item.vy -= repulsionForce * dxy.y / distance;
        }


        const dx = centerX - item.x;
        const dy = centerY - item.y;
        const distance = item.distanceTo({x:centerX, y:centerY})

        const distanceSpeed = distance * .0001

        item.vx += distanceSpeed + attractionStrength * dx;
        item.vy += distanceSpeed + attractionStrength * dy;

        item.vx *= damping;
        item.vy *= damping;

        limitSpeed(minVelocity, maxVelocity, item)

        item.x += item.vx;
        item.y += item.vy;
    }
}


function gravitateSquareDistance(items, focul, settings) {
    const centerX = focul == undefined? canvas.width * .5: focul.x
        , centerY = focul == undefined? canvas.height * .5: focul.y
        , minDistance = settings.minDistance
        , minVelocity = settings.minVelocity
        , maxVelocity = settings.maxVelocity
        , damping = settings.damping
        , repulsionStrength = settings.repulsionStrength
        , attractionStrength = settings.attractionStrength
        ;

    for (let i = 0; i < items.length; i++) {
        let item = items[i]
        for (let j = 0; j < items.length; j++) {
            if (i == j) {
                continue
            }
            const other = items[j]
            const dxy = other.subtract(item)
            const distance = item.distanceTo(other)
            const _minDistance = item?.minDistance ?? minDistance;
            if (distance > _minDistance) {
                /* If the distance is too far, we dont perform the
                replusion force below ...*/
                continue
            }

            let _repulsionStrength = item?.repulsionStrength ?? repulsionStrength; 
            const repulsionForce = _repulsionStrength / (distance * distance);
            item.vx -= repulsionForce * dxy.x / distance;
            item.vy -= repulsionForce * dxy.y / distance;
        }


        const dx = centerX - item.x;
        const dy = centerY - item.y;
        const distance = item.distanceTo({x:centerX, y:centerY}) * .0004

        let _attractionStrength = item?.attractionStrength ?? attractionStrength;
        const at = (_attractionStrength * .01) +  ((distance * distance) * .03)

        item.vx +=  at * dx;
        item.vy +=  at * dy;
        let _damp = item?.damping ?? damping;
        let _minVelocity = item?.minVelocity ?? minVelocity;
        let _maxVelocity = item?.maxVelocity ?? maxVelocity;
        item.vx *= _damp;
        item.vy *= _damp;

        limitSpeed(_minVelocity, _maxVelocity, item)

        item.x += item.vx;
        item.y += item.vy;
    }
}


/*
Ducklings: each point follows the average position of its nearest neighbors
that are strictly closer to the target. A point with no such neighbor is a
leader and follows the target directly. Because every link leads closer to the
target, a duck can never follow a duck that follows it (no AB/BA pairs or
longer loops), and every chain ends at the target.

Usage: simpleCluster(points, target, {func: clusterDucks, followNearestCount: 2})
followNearestCount defaults to 1; each point may override it and the usual
clustering settings. Without a target, the canvas center is used.
*/
function clusterDucks(items, focul, settings) {
    // Read all positions before moving any points to avoid update-order bias.
    const positions = Array.from(items, item => ({x: item.x, y: item.y}))
    if (positions.length == 0) { return }

    const targetX = focul == undefined ? canvas.width * .5 : focul.x
        , targetY = focul == undefined ? canvas.height * .5 : focul.y
        ;
    const targetDistances = positions.map(p => Math.hypot(targetX - p.x, targetY - p.y))
    const followCounts = Array.from(items, item => {
        const count = item.followNearestCount ?? settings.followNearestCount ?? 1
        if (!Number.isInteger(count) || count < 1) {
            throw new RangeError('followNearestCount must be a positive integer')
        }
        return count
    })

    for (let i = 0; i < items.length; i++) {
        const item = items[i]
        const position = positions[i]
        const minDistance = item.minDistance ?? settings.minDistance
        const repulsionStrength = item.repulsionStrength ?? settings.repulsionStrength
        const attractionStrength = item.attractionStrength ?? settings.attractionStrength
        const damping = item.damping ?? settings.damping
        const minVelocity = item.minVelocity ?? settings.minVelocity
        const maxVelocity = item.maxVelocity ?? settings.maxVelocity
        const neighbors = []

        for (let j = 0; j < positions.length; j++) {
            if (i == j) { continue }

            const other = positions[j]
            const dx = other.x - position.x
            const dy = other.y - position.y
            const distance = Math.hypot(dx, dy)
            // Only ducks nearer the target may be followed.
            if (targetDistances[j] < targetDistances[i]) {
                neighbors.push({position: other, distance})
            }

            // Coincident points have no separation direction; avoid dividing by zero.
            if (distance > 0 && distance <= minDistance) {
                const repulsionForce = repulsionStrength / (distance * distance)
                item.vx -= repulsionForce * dx / distance
                item.vy -= repulsionForce * dy / distance
            }
        }

        let centerX, centerY
        if (neighbors.length > 0) {
            neighbors.sort((a, b) => a.distance - b.distance)
            const count = Math.min(followCounts[i], neighbors.length)
            centerX = 0
            centerY = 0
            for (let j = 0; j < count; j++) {
                centerX += neighbors[j].position.x / count
                centerY += neighbors[j].position.y / count
            }
        } else {
            centerX = targetX
            centerY = targetY
        }

        const dx = centerX - position.x
        const dy = centerY - position.y
        const distance = Math.hypot(dx, dy) * .0004
        const at = (attractionStrength * .01) + ((distance * distance) * .03)

        item.vx += at * dx
        item.vy += at * dy
        item.vx *= damping
        item.vy *= damping
        limitSpeed(minVelocity, maxVelocity, item)

        item.x += item.vx
        item.y += item.vy
    }
}

copy