vector.js

//  Table indicating how to specify coloration of elements
//  -------------------------------------------------------------------
// |Color | HexColor   | DecimalColor                   | PercentColor |
// |Space | (string)   | (array)                        | (string)     |
// |------+------------+--------------------------------+--------------|
// | Gray | #GG        | [gray]                         | %G           |
// |  RGB | #rrggbb    | [red, green, blue]             | %r,g,b       |
// | CMYK | #ccmmyykk  | [cyan, magenta, yellow, black] | %c,m,y,k     |
//  -------------------------------------------------------------------
//
//   HexColor component values (two hex digits) range from 00 to FF.
//   DecimalColor component values range from 0 to 255.
//   PercentColor component values range from 1 to 100.

/**
 * Draw a circle
 * @name circle
 * @function
 * @memberof Recipe
 * @param {number} x - The coordinate x
 * @param {number} y - The coordinate y
 * @param {number} radius - The radius
 * @param {Object} [options] - The options
 * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
 * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
 * @param {string|number[]}[ options.fill] - HexColor, PercentColor or DecimalColor
 * @param {number} [options.lineWidth] - The line width
 * @param {number} [options.opacity] - The opacity
 * @param {number[]} [options.dash] - The dash style [number, number]
 */
exports.circle = function circle(x, y, radius, options = {}) {
    const {
        nx,
        ny
    } = this._calibrateCoordinate(x, y);
    const diameter = radius * 2;

    if (options.fill) {
        const pathOptions = this._getPathOptions(options, nx, ny);
        pathOptions.type = 'fill';

        if (pathOptions.fill !== undefined) {
            pathOptions.color = pathOptions.fill;
            pathOptions.colorspace = pathOptions.fillModel.colorspace;
        }

        this._drawObject(this, nx - radius, ny - radius, diameter, diameter, pathOptions, (ctx, xObject) => {
            ctx
                .gs(xObject.getGsName(pathOptions.fillGsId))
                .drawCircle(radius, radius, radius, pathOptions);
        });
    }
    if (options.stroke || options.color || !options.fill) {
        const pathOptions = this._getPathOptions(options);
        pathOptions.type = 'stroke';

        if (pathOptions.stroke !== undefined) {
            pathOptions.color = pathOptions.stroke;
            pathOptions.colorspace = pathOptions.strokeModel.colorspace;
        }

        // To honor the given width and height of the enclosing square ...

        this._drawObject(this, nx - radius, ny - radius, diameter, diameter, pathOptions, (ctx) => {
            ctx
                .d(pathOptions.dash, pathOptions.dashPhase)
                .drawCircle(radius, radius, radius - pathOptions.width / 2, pathOptions);

            // ... requires adjusting the internal drawing to accomodate line thickness.
        });
    }
    return this;
};

/**
 * Draw a rectangle
 * @name rectangle
 * @function
 * @memberof Recipe
 * @param {number} x - The coordinate x
 * @param {number} y - The coordinate y
 * @param {number} width - The width
 * @param {number} height - The height
 * @param {Object} [options] - The options
 * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
 * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
 * @param {string|number[]} [options.fill] - HexColor, PercentColor 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|number[]} [options.borderRadius] - radius size for rounded corners.Error
 * When a one to four number array can be used to give specific sizees to each corner.
 * The numbering starts from the top, left corner, and goes clockwise around the text box.
 * Missing values in the array are filled in by opposite corner values.
 */
exports.rectangle = function rectangle(x, y, width, height, options = {}) {
    const { nx, ny } = (options.useGivenCoords) ? { nx: x, ny: y } : this._calibrateCoordinate(x, y, 0, -height);

    const pathOptions = this._getPathOptions(options, nx, ny);
    let colorModel = pathOptions.colorModel;
    pathOptions.useGivenCoords = options.useGivenCoords;

    if (options.fill) {
        pathOptions.type = 'fill';

        if (pathOptions.fill !== undefined) {
            pathOptions.color = pathOptions.fill;
            pathOptions.colorspace = pathOptions.fillModel.colorspace;
            colorModel = pathOptions.fillModel;
        }

        this._drawObject(this, nx, ny, width, height, pathOptions, (ctx, xObject) => {
            ctx.gs(xObject.getGsName(pathOptions.fillGsId));
            xObject.fill(colorModel);

            if (options.borderRadius) {
                drawRoundedRectangle(ctx, 0, 0, width, height, options.borderRadius);
                ctx.f();
            } else {
                ctx.drawRectangle(0, 0, width, height, pathOptions);
            }
        });
    }

    if (options.stroke || options.color || !options.fill) {
        pathOptions.type = 'stroke';

        if (pathOptions.stroke !== undefined) {
            pathOptions.color = pathOptions.stroke;
            pathOptions.colorspace = pathOptions.strokeModel.colorspace;
            colorModel = pathOptions.strokeModel;
        }

        // To honor the given width and height of the rectangle ...

        this._drawObject(this, nx, ny, width, height, pathOptions, (ctx, xObject) => {

            // ... requires adjusting the internal drawing to accomodate line thickness.
            const margin = pathOptions.width;
            xObject.stroke(colorModel);

            if (options.borderRadius) {
                ctx
                    .w(pathOptions.width)
                    .d(pathOptions.dash, pathOptions.dashPhase);

                drawRoundedRectangle(ctx, margin / 2, margin / 2, width - margin, height - margin, options.borderRadius);
                ctx.S();
            } else {
                ctx
                    .d(pathOptions.dash, pathOptions.dashPhase)
                    .drawRectangle(margin / 2, margin / 2, width - margin, height - margin, pathOptions);
            }
        });
    }

    return this;
};

function drawRoundedRectangle(ctx, left, bottom, width, height, radii) {
    let radius = [];

    // populate radius array accordingly.
    // Missing element value comes from opposite corner.
    if (typeof radii === 'number') {
        radius = new Array(4).fill(radii);
    } else if (Array.isArray(radii)) {
        switch (radii.length) {
            case 1:
                radius = new Array(4).fill(radii[0]);
                break;
            case 2:
                radius = radii.slice(0);
                radius[2] = radii[0];
                radius[3] = radii[1];
                break;
            case 3:
                radius = radii.slice(0);
                radius[3] = radii[1];
                break;
            case 4:
                radius = radii;
                break;
        }
    }
    const K = 0.551784;
    const right = left + width;
    const top = bottom + height;
    ctx
        .m(left, top - radius[0]) // top-left
        .c(left, top - radius[0] * (1 - K), left + radius[0] * (1 - K), top, left + radius[0], top)

        .l(right - radius[1], top) // top-right
        .c(right - radius[1] * (1 - K), top, right, top - radius[1] * (1 - K), right, top - radius[1])

        .l(right, bottom + radius[2]) // bottom-right
        .c(right, bottom + radius[2] * (1 - K), right - radius[2] * (1 - K), bottom, right - radius[2], bottom)

        .l(left + radius[3], bottom) // bottom-left
        .c(left + radius[3] * (1 - K), bottom, left, bottom + radius[3] * (1 - K), left, bottom + radius[3])

        .l(left, top - radius[0]); // back to top-left
}

/**
 * Draw an ellipse
 * @name ellipse
 * @function
 * @memberof Recipe
 * @param {number} cx x-coordinate of center point of ellipse
 * @param {number} cy y-coordinate of center point of ellipse
 * @param {number} rx radius length from the center point along x-axis
 * @param {number} ry radius length from the center point along y-axis
 * @param {Object} options
 * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
 * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
 * @param {string|number[]}[ options.fill] - HexColor, PercentColor 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
 */
exports.ellipse = function ellipse(cx, cy, rx, ry, options = {}) {
    const {
        nx,
        ny
    } = this._calibrateCoordinate(cx, cy);

    const pathOptions = this._getPathOptions(options, nx, ny);
    let colorModel = pathOptions.colorModel;

    const width = rx * 2;
    const height = ry * 2;

    const drawEllipse = (ctx, x, y, w, h) => {
        const magic = 0.551784; // from https://www.tinaja.com/glib/ellipse4.pdf
        const ox = rx * magic; // control point offset horizontal
        const oy = ry * magic; // control point offset horizontal
        const xe = x + w; // x-end, opposite corner from origin
        const ye = y + h; // y-end, opposite corner from origin
        const xm = rx; // x-middle of enclosing rectangle
        const ym = ry; // y-middle of enclosing rectangle

        ctx
            .m(x, ym)
            .c(x, ym - oy, xm - ox, y, xm, y)
            .c(xm + ox, y, xe, ym - oy, xe, ym)
            .c(xe, ym + oy, xm + ox, ye, xm, ye)
            .c(xm - ox, ye, x, ym + oy, x, ym);
    };

    if (options.fill) {

        if (pathOptions.fill !== undefined) {
            colorModel = pathOptions.fillModel;
        }


        this._drawObject(this, nx - rx, ny - ry, width, height, pathOptions, (ctx, xObject) => {
            ctx.gs(xObject.getGsName(pathOptions.fillGsId));
            xObject.fill(colorModel);
            drawEllipse(ctx, 0, 0, width, height);
            ctx.f();
        });
    }

    if (options.stroke || options.color || !options.fill) {

        if (pathOptions.stroke !== undefined) {
            colorModel = pathOptions.strokeModel;
        }

        // To honor the given width and height of the enclosing rectangle ...

        this._drawObject(this, nx - rx, ny - ry, width, height, pathOptions, (ctx, xObject) => {
            const margin = pathOptions.width / 2;
            xObject.stroke(colorModel);
            ctx
                .w(pathOptions.width)
                .d(pathOptions.dash, pathOptions.dashPhase);

            // ... requires adjusting the internal drawing to accomodate line thickness.
            drawEllipse(ctx, margin, margin, width - pathOptions.width, height - pathOptions.width);
            ctx.S();
        });
    }
    return this;
};

function drawArc(ctx, x, y, radius, startAngle, endAngle, fromCenter=false) {

    const TWO_PI  = 2.0 * Math.PI;
    const HALF_PI = 0.5 * Math.PI;
    const magic = 0.551784; // from https://www.tinaja.com/glib/ellipse4.pdf
    let deltaAng;

    deltaAng = endAngle - startAngle;

    // Limit the drawing to no more than one complete circle
    if (Math.abs(deltaAng) > TWO_PI) {
        deltaAng = TWO_PI;
    }

    const numSegs = Math.ceil(Math.abs(deltaAng) / HALF_PI);
    const segAng = deltaAng / numSegs;
    const handleLen = (segAng / HALF_PI) * magic * radius;
    let curAng = startAngle;

    // distances between anchor point and control point
    let deltaCx = -Math.sin(curAng) * handleLen;
    let deltaCy =  Math.cos(curAng) * handleLen;

    // anchor point
    let ax = x + Math.cos(curAng) * radius;
    let ay = y + Math.sin(curAng) * radius;

    // draw sector lines?
    if (!fromCenter) {
        ctx.m(ax, ay);
    } else {
        ctx.m(x,y).l(ax, ay);
    }

    // generate segments of overall arc

    for (let segIdx = 0; segIdx < numSegs; segIdx++) {
        // starting control point
        const cp1x = ax + deltaCx;
        const cp1y = ay + deltaCy;

        // next angle
        curAng += segAng;

        // next control point difference
        deltaCx = -Math.sin(curAng) * handleLen;
        deltaCy =  Math.cos(curAng) * handleLen;

        // next anchor point
        ax = x + Math.cos(curAng) * radius;
        ay = y + Math.sin(curAng) * radius;

        // ending control point
        const cp2x = ax - deltaCx;
        const cp2y = ay - deltaCy;

        // produce segment
        ctx.c(cp1x, cp1y, cp2x, cp2y, ax, ay);
    }
}

/**
 * Draw an arc of a circle.
 * @name arc
 * @function
 * @memberof Recipe
 * @param {number} x - the x coordinate of the arc center point
 * @param {number} y - the y coordinate of the arc center point
 * @param {number} radius - the distance from the given x,y coordinates from which to produce the arc
 * @param {number} [startAngle=0] - the start of the arc in degree units +/- 0 through 360. Positive values go clockwise, Negative values, counterclockwise.
 * @param {number} [endAngle=360] - the end of the arc in degree units +/- 0 through 360. Positive values go clockwise, Negative values, counterclockwise.
 * @param {Object} [options]
 * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
 * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
 * @param {string|number[]}[ options.fill] - HexColor, PercentColor 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] - [originX, originY] Default: x, y
 */
exports.arc = function arc(x, y, radius, startAngle=0, endAngle=360, options = {}) {

    const { nx, ny } = this._calibrateCoordinate(x, y);
    const diameter = radius * 2;
    const pathOptions = this._getPathOptions(options, nx, ny);
    let colorModel = pathOptions.colorModel;
    const toRadians = (angle) => {return angle * (Math.PI / 180);};
    const sAng = - toRadians(startAngle);
    const eAng = - toRadians(endAngle);
    const sector = options.sector;

    if (options.fill) {
        if (pathOptions.fill !== undefined) {
            colorModel = pathOptions.fillModel;
        }

        this._drawObject(this, nx - radius, ny - radius, diameter, diameter, pathOptions, (ctx, xObject) => {
            ctx.gs(xObject.getGsName(pathOptions.fillGsId));
            xObject.fill(colorModel);

            drawArc(ctx, radius, radius, radius, sAng, eAng, sector);
            ctx.f();
        });
    }

    if (options.stroke || options.color || !options.fill) {

        if (pathOptions.stroke !== undefined) {
            colorModel = pathOptions.strokeModel;
        }

        // To honor the given width and height of the enclosing rectangle ...

        this._drawObject(this, nx - radius, ny - radius, diameter, diameter, pathOptions, (ctx, xObject) => {

            const margin = pathOptions.width / 2;
            xObject.stroke(colorModel);
            ctx
                .w(pathOptions.width)
                .d(pathOptions.dash, pathOptions.dashPhase);

            // ... requires adjusting the internal drawing to accomodate line thickness.
            drawArc(ctx, radius, radius, radius-margin, sAng, eAng, sector);
            if (sector) { ctx.h(); } // close off path to create a circle sector.
            ctx.S();
        });
    }

    return this;
};

exports.lineWidth = function lineWidth() {
    return this;
};

exports.fillOpacity = function fillOpacity() {
    return this;
};

exports.fill = function fill() {
    return this;
};

exports.stroke = function stroke() {
    return this;
};

exports.fillAndStroke = function fillAndStroke() {
    return this;
};