shapes.js

/*  N-Gon border box for odd numbered side shapes used to deal with object rotation.

  --------------------------------------------------------------  =========
 | m  ---------------------------o----------------------------  |        |
 | |  -------------------------- | -------------------------  | |        |
 | | |                           |                          | | |        |
 | | |                           |                          | | |        |
 | | |                           |                          | | |        |
 | | |                           |                          | | |        |
 | | |                           |                          | | |        |
 | | |                           |                          | | |        |
 | | |                  radius   |                          | | |        |
 | | |                           |                          | | |        |
 | | |                           |                          | | |        |
 | | |                           |                          | | |
 | | |                           |                          | | |      height
 | | |                           |                          | | |
 | | |                           |                          | | |        |
 | | |                           |                          | | |        |
 | | |                        -- o --  n-gon center         | | |        |
 | | |                           |                          | | |        |
 | | |                           |                          | | |        |
 | | |                           |                          | | |        |
 | | |                           |                          | | |        |
 | |  -------------------------- | -------------------------  | | ====   |
 |  --------o------------------- | -------------------------- M |  lw    |
 | -------- | ------------------ | -----------------------------  ====   |
 | | |      |                    |                          | | |        |
 | | |      |                    |                          | | |        |
 | | |      |                    |                          | | |        |
 | | |      |  deltaYY           |                          | | |        |
 | | |      |                    |                          | | |        |
 | | |      |                    |                          | | |        |
 | | |      |                    |                          | | |        |
 | |  ----- | ------------------ | -------------------------  | |        |
 |  --------o--------------------o----------------------------  |        |
 S -------------------------------------------------------------  =========

 |============================ width ===========================|

   Legend:
      m   minimum x, y of original border box (minX, minY)
      M   maximum x, y of original border box (maxX, maxY)
      S   starting origin of final border box
      lw  line width

The even number sided polygons have no trouble rotating due to the fact that
their border box coincides with the n-gon center point. That is not the case
for odd number sided polygons. In this case a deltaYY value must be computed
to produce a final border box with a center that coincides with n-gon center.

      deltaYY = minY + radius * 2 - maxY

*/

function odd(n) { return (n % 2 !== 0); }

function toRadians(angle) { return angle * (Math.PI / 180); }

function toDegrees(radians) { return radians * (180 / Math.PI); }

function endPoint(x, y, l, angle) {
    const radians = toRadians(angle);
    return [(x + l * Math.cos(radians)), (y + l * Math.sin(radians))];
}

function boundingBox(coords) {
    let boundBox = [coords[0][0], coords[0][1], coords[0][0], coords[0][1]];
    for (const coord of coords) {
        boundBox[0] = (boundBox[0] > coord[0]) ? coord[0] : boundBox[0];
        boundBox[1] = (boundBox[1] > coord[1]) ? coord[1] : boundBox[1];
        boundBox[2] = (boundBox[2] < coord[0]) ? coord[0] : boundBox[2];
        boundBox[3] = (boundBox[3] < coord[1]) ? coord[1] : boundBox[3];
    }
    return boundBox;
}

function _n_gon(sides, cx, cy, radius, options = {}) {

    let lineWidth = 0;

    if (options.stroke || options.color || !options.fill) {
        lineWidth = (options.lineWidth) ? options.lineWidth :
            (options.width) ? options.width : 2;
    }

    let ngon = [];
    let angle = 360 / sides;
    let startingAngle = (odd(sides)) ? 270 : 270 - (angle / 2);
    let n_radius = radius - lineWidth / 2;

    for (let i = 0; i < sides; i++) {
        const point = endPoint(cx, cy, n_radius, startingAngle + angle * i);
        ngon.push(point);
    }

    options.deltaYY = 0; // used in polygon to deal with ngon rotation

    if (options.rotation !== undefined && odd(sides)) {
        let boundBox = boundingBox(ngon);

        options.deltaYY = boundBox[1] + n_radius * 2 - boundBox[3];
    }

    if (options.rotationVertice) {
        options.rotationOrigin = ngon[(options.rotationVertice - 1) % (sides)];
    }

    return ngon;
}

/**
 * Draw an N-sided regular polygon
 * @name n_gon
 * @function
 * @memberof Recipe
 * @param {number} cx - x-coordinate of center point of regular polygon
 * @param {number} cy - y-coordinate of center point of regular polygon
 * @param {number} radius - The radius, distance from the center of the polygon to a vertice.
 * @param {number} [sides=3] - the number of sides of the regular polygon
 * @param {Object} [options] - The options
 * @param {string|number[]} [options.color] - HexColor or DecimalColor
 * @param {string|number[]} [options.stroke] - HexColor or DecimalColor
 * @param {string|number[]} [options.fill] - HexColor or DecimalColor
 * @param {number} [options.lineWidth] - The line width
 * @param {number} [options.opacity] - The opacity
 * @param {number[]} [options.dash] - The dash style [number, number]
 * @param {number} [options.rotation=0] - Accept: +/- 0 through 360.
 * @param {number[]} [options.rotationOrigin=[cx,cy]] - [originX, originY]
 * @param {number} [options.rotationVertice] - the number of the vertice to be used as rotation origin
 * @param {number} [options.skewX] - the angle skew off the x-axis
 * @param {number} [options.skewY] - the angle skew off the y-axis.
 */
exports.n_gon = function n_gon(cx, cy, radius, sides = 3, options = {}) {

    const MIN_SIDES = 3;

    // Handle optional 'sides' event when options present.
    if (typeof sides === 'object') {
        options = sides;
        sides = MIN_SIDES;
    }

    if (sides < MIN_SIDES) {
        sides = MIN_SIDES;
    }

    const ngon = _n_gon(sides, cx, cy, radius, options);

    this.polygon(ngon, options);

    if (options.rotationVertice) {
        delete options['rotationOrigin']; // cleanup n-gon generated point
    }

    if (options.debug) {
        this.circle(cx, cy, radius, { width: 1, stroke: '#00ff00' });
        this.circle(cx, cy, 2, { fill: '#ff0000' });
    }

    return this;
};

function _oddStar(ngon) {
    let starPath = [];
    let points = ngon.length;
    let interval = Math.floor(points / 2);

    for (let i = 0; i < points; i++) {
        starPath.push(ngon[i * interval % points]);
    }

    return starPath;
}

/**
 * Draw an N pointed star
 * @name star
 * @function
 * @memberof Recipe
 * @param {number} cx - x-coordinate of center point of regular polygon
 * @param {number} cy - y-coordinate of center point of regular polygon
 * @param {number} [points=5] - number of points on star
 * @param {Object} [options] - The options
 * @param {string|number[]} [options.color] - HexColor or DecimalColor
 * @param {string|number[]} [options.stroke] - HexColor or DecimalColor
 * @param {string|number[]} [options.fill] - HexColor or DecimalColor
 * @param {number} [options.lineWidth] - The line width
 * @param {number} [options.opacity] - The opacity
 * @param {number[]} [options.dash] - The dash style [number, number]
 * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0
 * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y
 * @param {number} [options.skewX] - the angle skew off the x-axis
 * @param {number} [options.skewY] - the angle skew off the y-axis.
 */
exports.star = function star(cx, cy, radius, points = 5, options = {}) {
    let starPath = [];
    let ngon;

    const MIN_POINTS = 5;

    // Handle optional 'points' event when options present.
    if (typeof points === 'object') {
        options = points;
        points = MIN_POINTS;
    }

    if (points < MIN_POINTS) {
        points = MIN_POINTS;
    }

    const starOptions = Object.assign({}, options);

    if (odd(points)) {
        starPath = _oddStar(_n_gon(points, cx, cy, radius, options));
    } else {
        let offset = -1;
        let halfPoints = points / 2;
        let interval = halfPoints - 1;

        let userRotation = (options.rotation) ? options.rotation : 0;

        if (odd(halfPoints)) {
            starOptions.rotation = 0 + userRotation;
            ngon = _n_gon(halfPoints, cx, cy, radius, starOptions);
            let deltaY = starOptions.deltaYY;

            if (halfPoints === 3) {
                starPath = ngon;
            } else {
                starPath = _oddStar(ngon);
            }

            this.polygon(starPath, starOptions);
            starOptions.rotation += (360 / points);
            starOptions.deltaYY = deltaY;

        } else {
            // Want a point of star to be top-most
            starOptions.rotation = (360 / points / 2) + userRotation;
            ngon = _n_gon(points, cx, cy, radius);

            for (let i = 0; i < points; i++) {
                let j = i * interval % points;
                if (j === 0) {
                    offset++;
                    if (offset > 0) {
                        this.polygon(starPath, starOptions);
                        starPath = [];
                    }
                }
                starPath.push(ngon[j + offset]);
            }
        }
    }

    this.polygon(starPath, starOptions);

    if (options.debug) {
        this.circle(cx, cy, radius, { width: 1, stroke: '#00ff00' });
        this.circle(cx, cy, 2, { fill: '#ff0000' });
    }

    return this;
};

function rotate(ox, oy, p, q, angle) {
    let [x, y] = [ox, oy];
    angle = angle % 360; // keep angle within realistic bounds

    if (angle !== 0) {
        [x, y] = [x - p, y - q];
        let theta;
        switch (angle) {
            case 90:
            case -270:
                [x, y] = [-y + p, x + q];
                break;
            case -90:
            case 270:
                [x, y] = [y + p, -x + q];
                break;
            case -180:
            case 180:
                [x, y] = [-x + p, -y + q];
                break;
            default:
                theta = toRadians(angle);
                [x, y] = [
                    (x * Math.cos(theta)) - (y * Math.sin(theta)) + p,
                    (x * Math.sin(theta)) + (y * Math.cos(theta)) + q
                ];
        }
    }

    return [x, y];
}

function center(ngon) {
    let [minX, minY, maxX, maxY] = boundingBox(ngon);
    let width = maxX - minX;
    let height = maxY - minY;
    return [minX + width / 2, minY + height / 2];
}

function translate(dx, dy, ngon) {
    let object = ngon.slice();
    for (const coord of object) {
        coord[0] += dx;
        coord[1] += dy;
    }

    return object;
}

function flipX(y, ngon) {
    let object = ngon.slice();
    for (const coord of object) {
        coord[1] = 2 * y - coord[1];
    }

    return object;
}

function flipY(x, ngon) {
    let object = ngon.slice();
    for (const coord of object) {
        coord[0] = 2 * x - coord[0];
    }

    return object;
}

/**
 * Draw a triangle, by specifying three side lengths, two side lengths and one inclusive angle, one side length and two adjacent angles, or with a set of vertices.
 * @name triangle
 * @function
 * @memberof Recipe
 * @param {number} x - x-coordinate used to position triangle, by default associated with left vertex of triangle base.
 * @param {number} y - y-coordinate used to position triangle, by default associated with left vertex of triangle base.
 * @param {number[]} traits - the data defining the triangle. Angles are specified as degrees, sides in units of points (1/72 in.).
 * @param {Object} [options] - The options
 * @param {string} [options.traitID='sss'] - indicates what type of data is being passed in the traits parameter.
 * ('sss'- three side lengths, 'sas' - side-angle-side (sideA, <C, sideB), 'asa' - angle-side-angle (<B, sideC, <A),
 * or 'vtx' - three vertex points [x,y])
 * @param {string} [options.position='b'] - the position of the triangle to be set at the given x,y coordinates.
 * The values can be one of: 'A' - the A vertex (right vertex of triangle base), 'B' - the B vertex (left vertex of triangle base),
 * 'C' - the C vertex (apex of triangle), 'centroid', 'circumcenter', or 'incenter' of the triangle.
 * @param {Boolean} [options.flipX=false] - flip triangle up to down through rotation point.
 * @param {Boolean} [options.flipY=false] - flip triangle right to left through rotation point.
 * @param {string|number[]} [options.color] - HexColor or DecimalColor
 * @param {string|number[]} [options.stroke] - HexColor or DecimalColor
 * @param {string|number[]} [options.fill] - HexColor or DecimalColor
 * @param {number} [options.lineWidth] - The line width
 * @param {number} [options.opacity] - The opacity
 * @param {number[]} [options.dash] - The dash style [number, number]
 * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0
 * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y
 * @param {number} [options.skewX] - the angle skew off the x-axis
 * @param {number} [options.skewY] - the angle skew off the y-axis. */

exports.triangle = function triangle(x, y, traits, options = {}) {
    let traitID = options.traitID || options.traitsID || 'sss';
    let position = (options.position) ? options.position.toLowerCase() : 'default';
    let triopts = Object.assign({}, options);

    if (traits.length !== 3) {
        throw new Error('Triangle requires 3 traits (sides/angles) for definition.');
    }

    traitID = traitID.toLowerCase();

    let pt, radius, trigon;
    let triangle = new Triangle(x, y, traitID, traits);

    let cc;
    let ic;
    switch (position) {
        case 'centroid':
            pt = triangle.centroid;
            break;
        case 'circumcenter':
            cc = triangle.circumcenter;
            [pt, radius] = [cc.point, cc.radius];
            break;
        case 'incenter':
            ic = triangle.incenter;
            [pt, radius] = [ic.point, ic.radius];
            triangle.incenter = [x, y]; // have to update incenter because tranlation will change it.
            break;
        case 'a':
            pt = new Point(triangle.A);
            break;
        case 'b':
            pt = new Point(triangle.B);
            break;
        case 'c':
            pt = new Point(triangle.C);
            break;
        default:
            pt = new Point(triangle.B);
            trigon = triangle.vertices;
            break;
    }

    if (!trigon) {
        trigon = translate(x - pt.x, y - pt.y, triangle.vertices);
        if (options.rotation && options.rotation !== 0) {
            triopts.rotationOrigin = [x, y];
        }
    }

    if (options.flipX) {
        trigon = flipX(y, trigon);
    }

    if (options.flipY) {
        trigon = flipY(x, trigon);
    }

    this.polygon(trigon, triopts);

    if (options.debug) {
        let angle = triopts.rotation || 0;
        let [rx, ry] = (triopts.rotationOrigin) ? triopts.rotationOrigin: center(triangle.vertices);

        // When rotation involved, easiest to just create new triangle
        // with rotated points so that labelling will work properly.
        if (angle !== 0 || options.flipX || options.flipY) {
            let tgon = [];
            for (const vertex of trigon) {
                tgon.push(rotate(vertex[0], vertex[1], rx, ry, angle));
            }
            triangle = new Triangle(tgon[0][0], tgon[0][1], 'vtx', tgon);
        }
        this.circle(x, y, 2, { color: 'red', width: .5 });

        if (radius) {
            this.circle(x, y, radius, { color: 'green', width: .5 });
        } else if (position === 'centroid') {
            const ma_A = new Line(triangle.A, triangle.BC.midpoint);
            const mb_B = new Line(triangle.B, triangle.AC.midpoint);
            const mc_C = new Line(triangle.C, triangle.AB.midpoint);
            this.line([
                [ma_A.point(1).x, ma_A.point(1).y],
                [ma_A.point(2).x, ma_A.point(2).y]
            ], { color: 'green', width: .5 });
            this.line([
                [mb_B.point(1).x, mb_B.point(1).y],
                [mb_B.point(2).x, mb_B.point(2).y]
            ], { color: 'green', width: .5 });
            this.line([
                [mc_C.point(1).x, mc_C.point(1).y],
                [mc_C.point(2).x, mc_C.point(2).y]
            ], { color: 'green', width: .5 });
        }

        this.circle(triangle.incenter.point.x, triangle.incenter.point.y, triangle.incenter.radius, { color: 'green', width: .5 });
        let ctr_A = new Line(triangle.incenter.point, triangle.A);
        let ap = ctr_A.extend(10);
        this.text('A', ap.x - 5, ap.y - 5, { color: '#a10439', size: 12 });
        // this.circle(ap.x, ap.y,10, {color:'green', width:.5});

        let ctr_B = new Line(triangle.incenter.point, triangle.B);
        ap = ctr_B.extend(10);
        this.text('B', ap.x - 5, ap.y - 5, { color: '#a10439', size: 12 });
        // this.circle(ap.x, ap.y,10, {color:'green', width:.5});

        let ctr_C = new Line(triangle.incenter.point, triangle.C);
        ap = ctr_C.extend(10);
        this.text('C', ap.x - 5, ap.y - 5, { color: '#a10439', size: 12 });
        // this.circle(ap.x, ap.y,10, {color:'green', width:.5});

        let toSideA = new Line(triangle.A, triangle.BC.midpoint);
        let toSideB = new Line(triangle.B, triangle.AC.midpoint);
        let toSideC = new Line(triangle.C, triangle.AB.midpoint);

        ap = toSideA.extend(10);
        this.text('a', ap.x - 3, ap.y - 5, { size: 10 });
        // this.circle(ap.x, ap.y,6, {color:'red', width:.5});

        ap = toSideB.extend(10);
        this.text('b', ap.x - 3, ap.y - 5, { size: 10 });
        // this.circle(ap.x, ap.y,6, {color:'red', width:.5});

        ap = toSideC.extend(10);
        this.text('c', ap.x - 3, ap.y - 5, { size: 10 });
        // this.circle(ap.x, ap.y,6, {color:'red', width:.5});
    }

    return this;
};

//  Reference of triangle sides (a,b,c) and vertices (A,B,C)
//           C
//          / \
//       a /   \ b
//        /_____\
//       B   c   A

const Triangle = class Triangle {
    constructor(x, y, traitID, traits) {
        let a, b, c, angA, angB, angC;
        let sss, BC, AC, AB;
        switch (traitID.toLowerCase()) {
            case 'sss':
                sss = traits.slice().sort((a, b) => { return a - b; });
                if (sss[0] + sss[1] <= sss[2]) {
                    throw new Error('Not a valid triangle inequality (sum of 2 shortest sides must be greater than third side');
                }

                [a, b, c] = traits;
                break;

            case 'sas':
                [a, angC, b] = traits;
                c = Math.sqrt(a * a + b * b - 2 * a * b * Math.cos(toRadians(angC)));
                break;

            case 'asa':
                [angB, c, angA] = traits;
                angC = 180 - angA - angB;
                if (angC <= 0) {
                    throw new Error('Not a valid triangle angle specification (sum of 2 angles must less than 180)');
                }
                a = c * Math.sin(toRadians(angA)) / Math.sin(toRadians(angC));
                b = c * Math.sin(toRadians(angB)) / Math.sin(toRadians(angC));
                break;

            case 'vtx':
                this._B = traits[0];
                this._C = traits[1];
                this._A = traits[2];
                BC = new Line(this._B, this._C);
                AC = new Line(this._A, this._C);
                AB = new Line(this._A, this._B);
                a = BC.length;
                b = AC.length;
                c = AB.length;
                break;

            default:
                throw new Error(`Unhandled trait identification ${traitID}`);
        }

        // At this point, all side lengths are known.
        if (!angA) {
            angA = toDegrees(Math.acos((b * b + c * c - a * a) / (2 * b * c)));
        }
        if (!angB) {
            angB = toDegrees(Math.acos((a * a + c * c - b * b) / (2 * a * c)));
        }
        if (!angC) {
            angC = 180 - angA - angB;
        }
        [this._x, this._y] = [x, y];
        [this._a, this._b, this._c] = [a, b, c];
        [this._angA, this._angB, this._angC] = [angA, angB, angC];

        this._perimeter = a + b + c;

        if (!this._A) {
            this._A = [x + c, y];
        }
        if (!this._B) {
            this._B = [x, y];
        }
        if (!this._C) {
            this._C = endPoint(x, y, a, -angB);
        }
    }

    get A() { return this._A; }
    get B() { return this._B; }
    get C() { return this._C; }

    get AC() {
        if (!this._AC) {
            this._AC = new Line(this._A, this._C);
        }
        return this._AC;
    }

    get AB() {
        if (!this._AB) {
            this._AB = new Line(this._A, this._B);
        }
        return this._AB;
    }

    get BC() {
        if (!this._BC) {
            this._BC = new Line(this._B, this._C);
        }
        return this._BC;
    }

    get perimeter() {
        return this._perimeter;
    }

    get area() {
        if (!this._area) {
            // Heron's formula
            let s = this.perimeter / 2; // semi-perimeter
            this._area = Math.sqrt(s * (s - this._a) * (s - this._b) * (s - this._c));
        }

        return this._area;
    }

    get vertices() {
        return [this._B, this._C, this._A];
    }

    // The centroid is the point where all three medians of the triangle
    // intersect. A median is the line running from a vertex to the midpoint
    // of the side opposite the vertex.
    get centroid() {
        if (!this._centroid) {
            let AB = new Line(this._A, this._B);
            let AC = new Line(this._A, this._C);
            let mAB = AB.midpoint;
            let mAC = AC.midpoint;
            let C_mAB = new Line(this._C, mAB);
            let B_mAC = new Line(this._B, mAC);
            this._centroid = C_mAB.intersect(B_mAC);
        }
        return this._centroid;
    }

    // The intersection of the perpendicular bisectors of
    // each side midpoint defines the circumcenter.
    get circumcenter() {
        if (!this._circumcenter) {
            // Algorithm in use is defining a circle from three noncolinear planar points
            // http://www.ambrsoft.com/TrigoCalc/Circle3D.htm
            let [x1, y1] = this._A;
            let [x2, y2] = this._B;
            let [x3, y3] = this._C;
            let x1y1_sq = x1 * x1 + y1 * y1;
            let x2y2_sq = x2 * x2 + y2 * y2;
            let x3y3_sq = x3 * x3 + y3 * y3;

            let A = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2;
            let B = x1y1_sq * (y3 - y2) + x2y2_sq * (y1 - y3) + x3y3_sq * (y2 - y1);
            let C = x1y1_sq * (x2 - x3) + x2y2_sq * (x3 - x1) + x3y3_sq * (x1 - x2);
            // let D = x1y1_sq*(x3*y2 - x2*y3) + x2y2_sq*(x1*y3 - x3*y1) + x3y3_sq*(x2*y1 - x1*y2);
            let a2 = 2 * A;
            let x = -(B / a2);
            let y = -(C / a2);
            let r = new Line(x, y, x1, y1);
            this._circumcenter = { point: new Point(x, y), radius: r.length };
        }
        return this._circumcenter;
    }

    set incenter(center) {
        this._incenter = { point: new Point(center[0], center[1]), radius: this._incenter.radius };
    }

    get incenter() {
        if (!this._incenter) {
            // https://www.mathopenref.com/coordincenter.html
            let x = (this._a * this._A[0] + this._b * this._B[0] + this._c * this._C[0]) / this.perimeter;
            let y = (this._a * this._A[1] + this._b * this._B[1] + this._c * this._C[1]) / this.perimeter;
            let radius = 2 * this.area / this.perimeter;
            this._incenter = { point: new Point(x, y), radius: radius };
        }
        return this._incenter;
    }
};

// /**
//  * Determine if given point is inside given polygon
//  * @param {number} x coordinate of point
//  * @param {number} y coordinate of point
//  * @param {number[]} ngon array of x,y coordinate pairs of polygon.
//  * @returns 1 when point inside polygon, 0 when outside polygon.
//  */
// function pointInPolygon(x, y, ngon) {
//     // http://alienryderflex.com/polygon/
//     let oddNodes = 0;
//     let polyCorners = ngon.length;
//     let j = polyCorners - 1;

//     for (i = 0; i < polyCorners; i++) {
//         if ((ngon[i][1] < y && y <= ngon[j][1] ||
//                 ngon[j][1] < y && y <= ngon[i][1]) &&
//             (ngon[i][0] <= x || x >= ngon[j][0])) {
//             oddNodes ^= (ngon[i][0] + (y - ngon[i][1]) / (ngon[j][1] - ngon[i][1]) * (ngon[j][0] - ngon[i][0]) < x);
//         }
//         j = i;
//     }

//     return oddNodes;
// }

/**
 * Draw an arrow
 * @name arrow
 * @function
 * @memberof Recipe
 * @param {number} x x-coordinate position
 * @param {number} y y-coordinate position
 * @param {Object} [options] arrow and polygon options
 * @param {number} [options.type=0] indicates the type of arrow head to produce. (0-'triangle', 1-'dart', 2-'kite')
 * Number or name may be used. Note, that the value of base offset in head option overrides this value.
 * @param {number|number[]} [options.head=[10,20,0]] defines the length, width and base offset of arrow head.
 * A single number can be used to assign both the length and width of arrow, giving the base offset value as zero.
 * @param {number|number[]} [options.shaft=[10,10]] defines the length and width of the arrow shaft.
 * @param {Boolean} [options.double=false] indicate double headed arrow production.
 * @param {string} [options.at] position and/or rotate at "head" or "tail" of arrow instead of at center.
 */
exports.arrow = function arrow(x, y, options = {}) {
    let defaultHeadLength = 10;
    let nock = null;
    let ox = x;
    let debug = options.debug;
    let headTypes = { 0: 0, triangle: 0, 1: .5, dart: .5, 2: -1, kite: -1 };

    let shaftLength = defaultHeadLength;
    let shaftWidth = defaultHeadLength;
    let headLength = defaultHeadLength;
    let headWidth = defaultHeadLength * 2;
    let baseOffset = 0;

    // Extract user dimensions of arrow head.
    // This will either be a simple number, or
    // 3 component array containing the data
    // elements to build a KITE shaped quadrilateral.
    if (options.head !== void(0)) {
        [headLength, headWidth, baseOffset] = (Array.isArray(options.head)) ? options.head: [options.head];
        if (headWidth === void(0)) {
            shaftWidth = shaftLength = headLength;
            headWidth = headLength * 2;
        }
        if (baseOffset === void(0)) {
            baseOffset = 0;
        }
    }

    if (headLength === void(0) || headLength === 0) {
        headLength = defaultHeadLength;
    }
    if (headWidth === void(0) || headWidth === 0) {
        headWidth = headLength * 2;
    }

    // Extract user dimensions of arrow shaft.
    if (options.shaft !== void(0)) {
        [shaftLength, shaftWidth] = (Array.isArray(options.shaft)) ? options.shaft: [options.shaft];
        if (shaftWidth === void(0)) {
            shaftWidth = shaftLength;
        }
    }

    if (shaftWidth > headWidth) {
        shaftWidth = headWidth;
    } else if (shaftWidth === 0) {
        shaftWidth = headWidth / 2;
    }

    if (baseOffset === 0 && options.type) {
        let type = headTypes[options.type];
        if (type !== void(0)) {
            baseOffset = type * headLength; // a percentage of the arrow head length.
        }
    }

    // Short cut for caller, so they don't have to specify rotation origin.
    if (options.at && options.rotation && !options.rotationOrigin) {
        options = Object.assign({}, options, { rotationOrigin: [x, y] });
    }

    // Adjust coordinates of drop point at head, tail, or middle of arrow.
    // ('default' choice represents center of arrow and default rotation point)
    if (options.double) {
        switch (options.at) {
            case 'head':
                x -= headLength;
                break;
            case 'tail':
                x += shaftLength + headLength;
                break;
            default:
                x += shaftLength / 2;
        }
        nock = new Kite(x - shaftLength, y, headLength, headWidth, baseOffset);
    } else {
        switch (options.at) {
            case 'head':
                x -= headLength;
                break;
            case 'tail':
                x += shaftLength;
                break;
            default:
                x += (shaftLength - headLength) / 2;
        }
    }

    const head = new Kite(x, y, headLength, headWidth, baseOffset);
    const arrow = new Arrow(x, y, head, shaftLength, shaftWidth, nock);

    const halfShaft = shaftWidth / 2;
    const KE = head.KE;

    // When the KE line is vertical, it means that the base offset was zero
    // Consequently, the arrow head will be a triangle, not a KITE. Note
    // that the intersection computation would also fail for vertical lines.
    if (!KE.isVertical) {
        const shaft_top = new Line(head.I[0], y - halfShaft, head.E[0], y - halfShaft);
        const ke_shaft_intercept = KE.intersect(shaft_top);
        arrow.joinShaft(ke_shaft_intercept);
    }

    if (options.double) {
        // Starting at arrow head tip (Pt I below),
        // moving clockwise to next point.
        //            K'        K
        //           /|_________|\
        //          /tl         tr\
        //      I' *               * I
        //          \bl_________br/
        //           \|         |/
        //            T'        T

        this.polygon([
            arrow.tip.I, // tip point of arrow
            arrow.tip.T,
            arrow.shaft('br'), // lower connection point to arrow tip
            arrow.shaft('bl'),

            arrow.nock.Tp, // drawing reverse arrow head at nock/tail of arrow
            arrow.nock.Ip,
            arrow.nock.Kp,

            arrow.shaft('tl'),
            arrow.shaft('tr'), // upper connection point to arrow tip
            arrow.tip.K,
            arrow.tip.I
        ], options);
    } else {
        // Starting at arrow head tip (Pt I below),
        // moving clockwise to next point.
        //                      K       _
        //       _    tl________|\       |       SW: shaftWidth
        //      |     |         tr\      |       SL: shaftLength
        //   SW-|     |            * I   |-HW    HW: headWidth
        //      |_    |_________br/      |       HL: headLength
        //            bl        |/      _|
        //                      T
        //            |_________|__|
        //                 |      |
        //                 SL     HL

        this.polygon([
            arrow.tip.I, // tip point of arrow
            arrow.tip.T,
            arrow.shaft('br'), // lower connection point to arrow tip
            arrow.shaft('bl'),
            arrow.shaft('tl'),
            arrow.shaft('tr'), // upper connection point to arrow tip
            arrow.tip.K,
            arrow.tip.I
        ], options); // back to point of arrow to close polygon
    }

    if (debug) {
        this.circle(ox, y, 2, { color: 'red' });

        if (debug === 2) { // Display Kite reference points
            const tc = 'red';
            const cc = 'red';
            const ktc = 'blue';
            const kcc = 'green';
            this.text('E', arrow.tip.E[0] - 3, arrow.tip.E[1] - 4, { size: 9, color: ktc });
            this.circle(arrow.tip.E[0], arrow.tip.E[1], 6, { color: kcc, width: .5 });
            this.text('K', arrow.tip.K[0] - 3, arrow.tip.K[1] - 10, { size: 9, color: ktc });
            this.circle(arrow.tip.K[0], arrow.tip.K[1] - 6, 6, { color: kcc, width: .5 });
            this.text('i', arrow.tip.I[0] + 5, arrow.tip.I[1] - 4, { size: 9, color: ktc });
            this.circle(arrow.tip.I[0] + 6, arrow.tip.I[1], 6, { color: kcc, width: .5 });
            this.text('T', arrow.tip.T[0] - 2, arrow.tip.T[1] + 3, { size: 9, color: ktc });
            this.circle(arrow.tip.T[0], arrow.tip.T[1] + 6, 6, { color: kcc, width: .5 });
            let br = arrow.shaft('br');
            this.text('br', br[0] - 4, br[1] - 11, { size: 9, color: tc });
            this.circle(br[0], br[1] - 8, 6, { color: cc, width: .5 });
            let bl = arrow.shaft('bl');
            this.text('bl', bl[0] + 4, bl[1] - 11, { size: 9, color: tc });
            this.circle(bl[0] + 8, bl[1] - 8, 6, { color: cc, width: .5 });
            let tl = arrow.shaft('tl');
            this.text('tl', tl[0] + 5, tl[1] + 2, { size: 9, color: tc });
            this.circle(tl[0] + 8, tl[1] + 7, 6, { color: cc, width: .5 });
            let tr = arrow.shaft('tr');
            this.text('tr', tr[0] - 3, tr[1] + 2, { size: 9, color: tc });
            this.circle(tr[0], tr[1] + 7, 6, { color: cc, width: .5 });
        }
    }

    return this;
};

// Reference points on KITE quadrangle             When a KITE becomes a Dart      ... degenerates to a Triangle
//
//                                                            K                              K
//     ------                         K                         * o                          |  o
//    |                          o    |   o                       *   o                      |     o
//                          o         |      o                      *    o                   |        o
//    w                o              |         o                     *     o                |           o
//    i           o                   |            o                    *      o             |              o
//    d       E                       |               I                  E        I          E                 I
//    t           o                   |            o                    *      o             |              o
//    h                o              |         o                     *     o                |           o
//                          o         |      o                      *    o                   |        o
//    |                          o    |   o                       *   o                      |     o
//     ------                         T                         * o                          |  o
//                                                            T                              T
//           |________________________|_______________|
//                  base offset             height

const Kite = class Kite {
    constructor(x, y, width, height, baseOffset = 0) {
        this._x = x;
        this._y = y;
        this._width = width;
        this._height = height;

        // To produce Dart shapes, baseOffset can be positive
        // but it cannot exceed the height of the arrow head.
        this._baseOffset = (baseOffset >= height) ? (height - 1) : baseOffset;
        this._type = (baseOffset > 0) ? 'dart' :
            (baseOffset === 0) ? 'triangle' : 'kite';

        this._K = new Point(x, y - (height / 2));
        this._I = new Point(x + width, y);
        this._T = new Point(x, y + (height / 2));
        this._E = new Point(x + this._baseOffset, y);
    }

    get K() { return [this._K.x, this._K.y]; }
    get I() { return [this._I.x, this._I.y]; }
    get T() { return [this._T.x, this._T.y]; }
    get E() { return [this._E.x, this._E.y]; }

    // create points I&E prime (flip, 180 degrees) to change direction of Kite on X-axis
    get Ip() { return [this._I.x - (2 * this._width), this._I.y]; }
    get Ep() { return [this._E.x + (2 * this._baseOffset), this._E.y]; }

    get Kp() { return [this._K.x, this._K.y]; } // no different than K or T, just here for consistency usage
    get Tp() { return [this._T.x, this._T.y]; }

    get KE() { // line segment between points K and E
        if (!this._KE) {
            this._KE = new Line(this._K.x, this._K.y, this._E.x, this._E.y);
        }
        return this._KE;
    }

    get TE() { // line segment between points T and E
        if (!this._TE) {
            this._TE = new Line(this._T.x, this._T.y, this._E.x, this._E.y);
        }
        return this._TE;
    }

    get type() { return this._type; }

    // position(x, y) {

    // }
};

const Arrow = class Arrow {
    constructor(x, y, arrowhead, shaftLength, shaftWidth, nock) {
        this._x = x;
        this._y = y;
        this._tip = arrowhead;
        this._nock = nock; // for double headed arrows
        this._shaftLength = shaftLength;
        this._shaftWidth = shaftWidth;
        this._connectAt_tr = new Point(this._x, this._y - shaftWidth / 2); // top, right
        this._connectAt_br = new Point(this._x, this._y + shaftWidth / 2); // bottom, right
    }

    get tip() { return this._tip; }
    get nock() { return this._nock; }

    joinShaft(pointTR) {
        this._connectAt_tr.x = pointTR.x;
        this._connectAt_br.x = pointTR.x;
    }

    shaft(point) {
        switch (point) {
            case 'br':
                return [this._connectAt_br.x, this._connectAt_br.y];
            case 'bl':
                return [this._x - this._shaftLength, this._connectAt_br.y];
            case 'tl':
                return [this._x - this._shaftLength, this._connectAt_tr.y];
            case 'tr':
                return [this._connectAt_tr.x, this._connectAt_tr.y];
        }
    }
};

const Point = class Point {
    constructor(x, y) {
        if (Array.isArray(x)) {
            this._x = x[0];
            this._y = x[1];
        } else {
            this._x = x;
            this._y = y;
        }
    }
    get x() { return this._x; }
    get y() { return this._y; }
    get point() { return [this._x, this._y]; }
    set x(xx) { this._x = xx; }
    set y(yy) { this._y = yy; }
    set point(pnt) {
        [this._x, this._y] = pnt;
    }
};

const Line = class Line {
    constructor(x1, y1, x2, y2) {
        // Allow user to supply Points or Arrays instead of individual coordinates
        if ((x1 instanceof Point || Array.isArray(x1)) && !(y1 instanceof Point || Array.isArray(y1))) {
            throw new Error('2nd parameter not an instance of Point or Array');
        }

        if (typeof x1 === 'number') { // individual coordinates
            this._pt1 = new Point(x1, y1);
            this._pt2 = new Point(x2, y2);
        } else {
            if (x1 instanceof Point) {
                this._pt1 = x1;
            } else {
                this._pt1 = new Point(x1); // assuming array
            }

            if (y1 instanceof Point) {
                this._pt2 = y1;
            } else {
                this._pt2 = new Point(y1); // assuming array
            }
        }
    }

    get isVertical() {
        return this._pt1.x === this._pt2.x;
    }

    point(ep) {
        return (ep === 1) ? this._pt1 :
            (ep === 2) ? this._pt2 : null;
    }

    get midpoint() {
        if (!this._midpoint) {
            let dx = (this._pt2.x - this._pt1.x) / 2;
            let dy = (this._pt2.y - this._pt1.y) / 2;
            this._midPoint = new Point(this._pt1.x + dx, this._pt1.y + dy);
        }

        return this._midPoint;
    }

    get length() {
        if (!this._length) {
            this._length = Math.sqrt(Math.pow(this._pt2.x - this._pt1.x, 2) + Math.pow(this._pt2.y - this._pt1.y, 2));
        }

        return this._length;
    }

    get slope() {
        if (!this._slope) {
            this._slope = Math.abs(this._pt2.x - this._pt1.x) < .0001 ? Infinity : (this._pt2.y - this._pt1.y) / (this._pt2.x - this._pt1.x);
        }

        return this._slope;
    }

    get inv_slope() { // inverse slope
        return -(1 / this.slope);
    }

    extend(distance, ptNbr = 2) {
        let slope = this.slope;
        let ept = this.point(ptNbr);
        let opt = this.point((ptNbr === 1) ? 2 : 1);
        let x = ept.x,
            y = ept.y;
        let epsilon = .0001;
        let newLen, deltaX = 0,
            deltaY = 0;
        let ss;

        switch (slope) {
            case 0:
                deltaX = distance;
                newLen = Math.abs(opt.x - (x + deltaX));
                break;
            case Infinity:
                deltaY = distance;
                newLen = Math.abs(opt.y - (y + deltaY));
                break;

            default:
                ss = Math.sqrt(1 / (1 + Math.pow(slope, 2)));
                deltaX = distance * ss;
                deltaY = slope * deltaX;
                newLen = Math.sqrt(Math.pow(opt.x - (x + deltaX), 2) + Math.pow(opt.y - (y + deltaY), 2));
                break;
        }

        // Since there are 2 possible solutions, the idea is to choose the one which
        // effectively gives a distance equivalent to the length of the line plus the given distance.
        let direction = (Math.abs(newLen - (this.length + distance)) < epsilon) ? 1 : -1;
        x += direction * deltaX;
        y += direction * deltaY;

        return new Point(x, y);
    }

    // Equation to solve for intersection of two line segments
    // when given 4 sets of points representing the segments.
    // due to sign negation in program, terms don't match here.
    //
    //      (x2y1 - x1y2)(x4 - x3) - (x4y3 - x3y4)(x2 - x1)     c1(b2) - c2(b1)
    //  x = -----------------------------------------------     ---------------
    //          (x2 - x1)(y4 - y3) - (x4 - x3)(y2 - y1)         b1(a2) - b2(a1)
    //
    //      (x2y1 - x1y2)(y4 - y3) - (x4y3 - x3y4)(y2 - y1)     c1(a2) - c2(a1)
    //  y = -----------------------------------------------     ---------------
    //          (x2 - x1)(y4 - y3) - (x4 - x3)(y2 - y1)         b1(a2) - b2(a1)

    intersect(CD) {
        let A = this._pt1,
            B = this._pt2;
        let C = CD,
            D = CD;

        let x1 = A.x,
            y1 = A.y;
        let x2 = B.x,
            y2 = B.y;

        let x3 = C.point(1).x,
            y3 = C.point(1).y;
        let x4 = D.point(2).x,
            y4 = D.point(2).y;

        // Line AB represented as a1x + b1y = c1
        let a1 = y2 - y1;
        //let b1 = x2 - x1;
        let b1 = x1 - x2;
        //let c1 = b1*y1 - a1*x1;
        let c1 = b1 * y1 + a1 * x1;

        // Line CD represented as a2x + b2y = c2
        let a2 = y4 - y3;
        //let b2 = x4 - x3;
        let b2 = x3 - x4;
        //let c2 = b2*y3 - a2*x3;
        let c2 = b2 * y3 + a2 * x3;

        // If the lines are parallel their slopes will be the same
        // causing the determinantinator to be zero, so check for that first.

        //let determinant = a2*b1 - a1*b2;
        let determinant = a1 * b2 - a2 * b1;

        if (determinant === 0) { return null; }

        let x = (b2 * c1 - b1 * c2) / determinant;
        //let y = (a2*c1 - a1*c2) / determinant;
        let y = (a1 * c2 - a2 * c1) / determinant;

        return new Point(x, y);
    }
};