Parse

File Parse point.js

This tree is parsed live from the source file.

Classes

  • {{ item.name }}

    • {{ key }}

Not Classes

{{ getTree() }}

Comments

{{ getTreeComments() }}

Source

            /*
files:
    relative-xy.js
    pointcast.js
categories: primary
    point
doc_readme: point/readme.md
doc_content: point/*.md
doc_imports: point
doc_loader: loadDocInfo

*/

const isPoint = function(value) {
    return value.constructor == Point
}


const isFunction = function(value) {
    return (typeof(value) == 'function')
}


const point = function(p, b) {
    if(p.constructor == Point) {
        return p
    }
    if(Array.isArray(p)) {
        return new Point({x: p[0], y:p[1]})
    }

    if(b !== undefined) {
        return new Point({x: p, y: b})
    }

    return p
}


window.loadDocInfo = function() {
    /* Return the think to resolve */
    console.log('loadDocInfo called')
    return Point;
}


class Positionable extends Relative {
    /* The base `Positionable` class provides functionality for
    plotting the X and Y of an entity. This includes any fundamental
    methods such as `multiply()`.
    */

    /* ABOVE X method

    Multiline - touching.
    */
    set x(value) {
        /* Set the _X_ (horizontal | latitude | across) value of the positionable.
        From cooridinate top left `(0,0)``

            point.x = 100

        If the given value is a function, the function is called immediately,
        with _this_ positionable as the first argument.

            stage.center.x = (p)=>200

         */
        // this._opts.x = isFunction(v)? v(this, 'x'): v
        return this.setSpecial('x', value)
    }

    set y(value) {
        /* Set the _Y_ (vertical | height | longtitude) value of the positionable.
        From cooridinate top left `(0,0)``

            point.x = 100

        If the given value is a function, the function is called immediately,
        with _this_ positionable as the first argument.

            stage.center.x = (p)=>200

         */
        // this._opts.y = isFunction(v)? v(this, 'y'): v
        return this.setSpecial('y', value)
    }

    get x() {

        // const _x = this._opts.x;
        // let r = _x == undefined? 0: _x
        // let v = this._relativeData[0]
        // r = isFunction(r)? r(this, 'x'): r
        // return r + v
        return this.getSpecial('x', 0)
    }

    get y() {

        // const _y = this._opts.y;
        // let r = _y == undefined? 0: _y
        // r = isFunction(r)? r(this, 'y'): r
        // return r + this._relativeData[1]
        return this.getSpecial('y', 1)
    }

    set radius(v) {
        // this._opts.radius = isFunction(v)? v(this, 'radius'): v
        return this.setSpecial('radius', v)
    }

    get radius() {
        /*
        Return the _radius_ of this point in base units (pixels)

            const point = new Point(100,300, 20)
            point.radius == 20
        */
        // const _radius = this._opts.radius
        // let r = _radius == undefined? 0: _radius;
        // r = isFunction(r)? r(this, 'radius'): r;
        // return r + this._relativeData[2]
        return this.getSpecial('radius', 2, 5)
    }

    setSpecial(key,  value) {
        /* Set the given `key`, `value`, assuming the given key is a "special"
        string, such as "radius".
        This method is called by the special getters and setters for this point:

            const point = new Point(100,300, 20)
            point.setSpecial('radius', 55)

        Synonymous to:

            point.radius = 55

        */
        this._opts[key] = isFunction(value)? value(this, key): value
        this.onSpecialSet(key, value)
        return true
    }

    onSpecialSet(key, value) {
        /* A callback executed by setSpecial when a "special" property is called.

        If a method on this point exists matching the _set method_ pattern,
        the method is called with the given `value`:

            class MyPoint extends Point {
                radiusSet(value) {
                    console.log('New Radius value is', value)
                }
            }

        */
        let name = `${key}Set`
        this[name] && this[name](value)
        // console.log(name)
    }

    getSpecial(key, relIndex=undefined, defaultValue=0) {
        /* Return a stored _special_ value given a `key`.

        If the `relIndex`  property is not None, the relative value found
        at the index (within this points relative data Array),
        add the stashed relative value to the result.

            const point = new Point(100,300, 20)
            point.getSpecial('radius', 2)
            // 20

        if a default value is given, and the internal `key` value does not exist,
        return the default value:

            point.getSpecial('banana', 'yellow')
            'yellow'

        */
        const internalValue = this._opts[key];
        let r = internalValue == undefined? defaultValue: internalValue
        r = isFunction(r)? r(this, key): r
        let relVal = relIndex != undefined? this.getRelativeData()[relIndex]: 0
        return r + relVal
    }

    set(x, y, radius, rotation) {
        /*
            set(x, y, radius, rotation)
            set(100)    => x,y
            set([])     => [x,y,radius, rotation]
            set({})     => *
        */
        const isUndefined = function(v) {
            return v === undefined
        }

        if(isUndefined(y)) {

            if(Array.isArray(x)) {
                let lmap = {
                    1: () => {
                        /* An array of one
                            set([200])
                        */
                    }
                    , 2: ()=> {
                        [x,y] = x
                    }
                    , 3: ()=> {
                        [x,y, radius] = x
                    }
                    , 4: ()=> {
                        [x,y, radius, rotation] = x
                    }
                }

                lmap[x.length]()
            } else if(typeof(x)=='number') {
                y = x
                x = x
            }else{
                // object
                for(let k in x) {
                    this[k] = x[k]
                }
                y = x?.y
                x = x?.x
            }
        }

        this.x = isUndefined(x)? 0: x
        this.y = isUndefined(y)? 0: y

        if(!isUndefined(radius)) {
            this.radius = radius
        }

        if(!isUndefined(rotation)) {
            this.rotation = rotation
        }
    }

    _cast(other, _2=other) {
        if(typeof(other) == 'number') { other = point(other, _2) }
        if(Array.isArray(other)){ return point(other, _2) }
        return other
    }

    subtract(other, _2=other){
        /* "subtract" this point to the _other_ point, returning a new point. */
        if(typeof(other) == 'number') {
            other = point(other, _2)
        }

        return new Point(this.x - other.x, this.y - other.y)
    }

    add(other, _b,) {
        /* "Add" this point to the _other_ point, returning a new point. */
        other = this._cast(other, _b)

        return new Point(
            this.x + other.x,
            this.y + other.y
        )
    }

    divide(other) {
        /* "Divide" this point to the _other_ point, returning a new point. */
        if(typeof(other) == 'number') {
            other = point(other, other)
        }

        let nNaN = v => isNaN(v)? 0: v;

        return new Point(
              nNaN(this.x / other.x)
            , nNaN(this.y / other.y)
        )
    }

    multiply(other) {
        /* "Multiply" this point to the _other_ point, returning a new point. */
        if(typeof(other) == 'number') {
            other = point(other, other)
        }

        return new Point(
            this.x * other.x,
            this.y * other.y
        )
    }

    // _midpoint(other, offset=.5) {
    //     /*return a new point, with the XY set at the _mid point_ between
    //     this point and the given*/
    //     let p = this.copy()
    //     p.x = (p.x + other.x) * offset
    //     p.y = (p.y + other.y) * offset
    //     return p
    // }

    midpoint(other, offset=0.5) {
        /*
        Returns a new point, with the XY set at the point that is `offset` times
        the distance from the current point to the other point.

        this function is also `lerp` for linear interpolation
        */
        let p = this.copy();
        p.x = p.x + (other.x - p.x) * offset;
        p.y = p.y + (other.y - p.y) * offset;
        return p;
    }

    lerp = this.midpoint

}



class Rotation extends Positionable {
    set rotation(value){
        /* Set the rotation in degrees (0 to 360). If `this.modulusRotate` is
        `true` (default), the given value is fixed through modulus 360

            point.rotation = 600
            // 240
            point.rotation += 1
            // 241

        To set the rotation (degrees) whilst accounting for the _UP_ vector,
        consider the `rotate()` method.
        */

        if(this.modulusRotate == false) {
            return this.setSpecial('rotation', value)
        }

        return this.setSpecial('rotation', value % 360)

    }

    rotate(degrees) {
        this.rotation = this.UP + degrees
        return this
    }

    get rotation() {
        return this.getSpecial('rotation', 3)
    }

    get radians() {
        /*Return the _radians_ of the current rotation, where _rotation returns
        the degrees*/
        return degToRad(this.getSpecial('rotation', 3))
    }

    set radians(angle) {
        /* by pushing through the existing rotations, we account for the _up_
        vector. */
        this.rotation = radiansToDegrees(angle)
    }

    lookAt(otherPoint, add=0, rotationMultiplier=undefined) {
        /* Rotate the point such that the angle relative to the
        `otherPoint` is `0`, essentially _looking at_ the other point.

            point.lookAt(otherPoint)

        Return the angle in radians.
        */
        return this.radians = this.directionTo(otherPoint, rotationMultiplier, add)// + add
    }

    directionTo(otherPoint, rotationMultiplier=undefined, addRad=0) {
        // Calculate the differences in x and y coordinates
        let delta = 0
        try {
            delta = otherPoint.subtract(this);
        } catch(e) {
            if(!isPoint(otherPoint)) {
                otherPoint = new Point(otherPoint)
                delta = otherPoint.subtract(this);
            } else {
                throw e
            }

        }
        if(rotationMultiplier != undefined) {
            let normRad = this._normalizedRadians(otherPoint, rotationMultiplier, addRad)
            return normRad
        }

        // Calculate the angle in radians
        const angleRadians = delta.atan2()
        return angleRadians + addRad
    }

    _normalizedRadians(otherPoint, rotationMultiplier, addRad=0) {
        const delta = otherPoint.subtract(this);
        const targetRad = delta.atan2() + addRad;
        const currentRad = this.radians;

        let radDiff = targetRad - currentRad;
        // Normalize the angle difference to be within the range -PI to PI
        radDiff = Math.atan2(Math.sin(radDiff), Math.cos(radDiff));
        const newAngleRadians = currentRad + radDiff * rotationMultiplier;
        const normRad = Math.atan2(
                            Math.sin(newAngleRadians),
                            Math.cos(newAngleRadians)
                        );
        return normRad;
    }

    turnTo(otherPoint, rotationMultiplier=1){
        let normRad = this._normalizedRadians(otherPoint, rotationMultiplier)
        this.radians = normRad;
        return normRad
    }

    getTheta(other, direction=undefined) {
        /* Return the calculated theta value through atan2 and built to offload
        some of the boring.
        The _direction_ denotes the "gravity" pull. Generally this is `DOWN`.

        Synonymous to:

            let theta = Math.atan2(point.y,
                                   point.x);

            let theta = Math.atan2(point.y - other.y,
                                   point.x - other.x);

            let theta = Math.atan2(point.y - other.y,
                                   point.x - other.x) - DOWN;

        */
        let x = this.x
          , y = this.y
            ;

        if(other) {
            let _p = this.subtract(other)
            x = _p.x
            y = _p.y
        }

        // Gravity is generally DOWN by default
        // perhaps this should be -rotation.
        // direction = this.resolveStringOrFunction(direction, DOWN)
        let theta = Math.atan2(y, x) - direction
        return theta
    }
}


class Tooling extends Rotation {

    resolveStringOrFunction(direction, defaultValue) {
        let res = defaultValue;

        if(typeof(direction) == 'string') {
            // str such as this["rotation"]
            res = this[direction]
        }

        if(typeof(direction) == 'function') {
            return direction()
        }

        return res
    }

    atan2() {
        /* Return the theta value through the atan2 function for this point.*/
        let x = this.x
            , y = this.y
            ;

        let thetaRadians = Math.atan2(y, x);
        return thetaRadians
    }

    project(distance, rotation, relative=true) {
        if(rotation !== undefined && relative == true) {
            rotation = (this.UP + rotation) % 360
        }
        let np = new this.constructor(projectFrom(this, distance, rotation))
        np.rotation = this.rotation
        return np
    }

    copy(position, deep=false) {
        /* Given another point, replicate the value into this node.
        Else, return a new node with the same information as this point.
        */
        if(position) {
            this.set(position.x, position.y)
            if(position?.radians){
                this.radians = position.radians
            }

            if(deep==true) {
                /* Deep should inspect all given options
                to a point, and apply them all. */

                if(position.radius){
                    this.radius = position.radius
                }
            }
            return this;
        }

        return new Point(this.x, this.y, this.radius, this.rotation)
    }

    magnitude() {
        let x = this.x;
        let y = this.y;
        return Math.sqrt(x * x + y * y);
    }

    normalized(magnitude=this.magnitude()) {
        /*
        Synonymous to:

            {
                x: AB.x / magnitudeAB,
                y: AB.y / magnitudeAB
            };
        */
        return this.divide(magnitude)
        // return this.divide(this.magnitude())
    }

    interpolateTo(other, offset, pointIndex=0) {
        /* return a point relative from _this_ point towards the `other`,
        offset by the given number `offset` value.
        The `pointIndex` (default 0) identifies from which point [this, other]
        to offset.*/

        return getPointOffsetAbsolute(this, other, offset, pointIndex)
    }

    interpolateFrom(other, offset, pointIndex=0) {
        return getPointOffsetAbsolute(other, this, offset, pointIndex)
    }

    static distance(a, b){
        return Math.hypot(b.x - a.x, b.y - a.y);
    }

    quantize(amount=1) {
        let q = quantizeNumber
        return new this.constructor({
                            x: q(this.x, amount)
                            , y: q(this.y, amount)
                        })
    }

    protractorAngleTo(other, referencePoint) {
        let value = calculateAngleWithRefWithNeg(this, other, referencePoint)
        return new Angle(value)
    }

    lerpPixel(other, pixelDistance) {
        /*
        Returns a new point, offset by `pixelDistance` pixels in the direction
        from this point to the other point.
        */

        // Calculate the direction vector from this point to the other point
        let directionX = other.x - this.x;
        let directionY = other.y - this.y;
        let dirV = this.distance2D(other)
        let distance = this.distanceTo(other)
        // Normalize the direction vector (to unit length)
        let unitX = dirV.x / distance;
        let unitY = dirV.y / distance;

        // Scale the unit vector by the desired pixel distance
        let offsetX = unitX * pixelDistance;
        let offsetY = unitY * pixelDistance;

        // Create a new point at the scaled offset from the original point
        let p = this.copy();
        p.x = this.x + offsetX;
        p.y = this.y + offsetY;

        return p;
    }
}


class Point extends Tooling {
    /*The `Point` is the primary class for manipulating XY 2D points.

    Arguments:

        new Point(100, 200)

    Object | Array:

        new Point({x: 100, y: 200})
        new Point([100, 200])

    Properties:

        point = new Point
        point.x = 100
        point.y = 200

    */

    // x=0
    // y=0
    // radius = 5
    UP = UP_DEG
    _rotationDegrees = UP_DEG

    constructor(opts={}){
        /* A new point accepts arguments, an object, or an array.

        By default the `Point` accepts up to four arguments

            new Point(x, y, radius, rotation)

        The same properties may be supplied as a single `Array`:

            new Point([x, y, radius, rotation])

        If the given object is an `object`, we can assign properties
        immediately:

            point = new Point({x, y, radius, rotation, other:100})
            point.other
            // 100

        */
        super(opts)

        /* Ensure _opts_ is something. Default; a dict. */
        opts = arguments[0] || {}

        /* If given a Point instance, return the given point instance */
        if(opts && (opts.constructor == this.constructor) ){ return opts }
        // new Point(x,y, ...)  // reset the opts obj.
        if(arguments.length > 1 || typeof(arguments[0] == 'number')){ opts = {} }

        this.modulusRotate = undefined

        this._opts = Object.assign({relX: 0, relY: 0 }, opts)
        // set 0 or more object
        this.set.apply(this, arguments)
        this.created()
    }

    created() {
        // api hook for life.
    }

    update(data) {
        /*
        Perform an _update_ given a dictionary of other properties.

            point.update({ x: 200, color: 'red' })
        */
        for(let k in data) {
            this[k] = data[k]
        }

        return this
    }

    get uuid() {
        /* Get or create the random `_id` for this point.
        Return a unique string.
        */
        let r = this._id;
        if(r == undefined) {
            this._id = r = (~~(Math.random() * 10000)).toString(32)
        }
        return r
    }

    set uuid(v) {
        this._id = v
    }

    get [0]() {
        /* return the X value of the point:

            point[0]

        Note:

            point[0] == point.x
        */
        return this.x
    }

    set [0](value) {
        /*
        Sugar function to apply `this.x`:

            point[0] = 100
            point.x == 100
        */
        this.x = value
    }

    get [1]() {
        /* return the Y value of the point:

            point[0]

        Note:

            point[0] == point.y
        */
        return this.y
    }

    set [1](value) {
        /*
        Sugar function to apply `this.y`:

            point[0] = 300
            point.y == 300
        */
        this.y = value
    }

    get [Symbol.toStringTag]() {
        return this.toString()
    }

    [Symbol.toPrimitive](hint) {
        if (hint === 'string') {
            return this.toString()
        }
        return null
        // return Reflect.apply(...arguments)
    }

    toString(){
        let name = 'point'
        return `${name}({x:${this.x}, y:${this.y}})`;
    }

    get _liveProps() { return true }

    asArray(fix=false) {
        /*
        Return this point as an Array of 4 values:

            const point = new Point(1,2,3,4);
            point.asArray();
            // [1,2,3,4]

        Keys:

        + x
        + y
        + radius
        + rotation

        */
        // let r = [this.x, this.y, this.radius, this.rotation]
        let r = Object.values(this.asObject())
        if(fix) {
            let int = (x)=> Number( x.toFixed(Number(fix)) )
            return r.map(v=>int(v))
        }
        return r
    }

    asObject() {
        /* Return the important information about this node,
        used for _save_ or copy methods. */
        return {
            x: this.x
            , y: this.y
            , radius: this.radius
            , rotation: this.rotation
        }
    }

}


Polypoint.head.install(Point)


Polypoint.head.mixin('Point', {
    isNaN: {
        value(any=false) {
            let r = 0;
            r += +isNaN(this.x)
            r += +isNaN(this.y)
            if(r==0) { return false }

            if(r > 0) {
                if(any) { return true }
                // two NaNs, is always isNaN == true
                if(r >= 2) { return true }
            }

            return false
        }
        , writable: true
    }
})


;Object.defineProperty(Point, 'from', {
    value: function(a,b, c, d){

        if(a.offsetX && b==undefined) {
            // is an event
            return new Point(a.offsetX, a.offsetY)
        }
        return new Point(a,b, c, d)
    }
});

copy