Source: Base.js

/*jshint esversion: 9 */

/**
 * Common components of TangleElement and Tangle.
 */
class TangleBase {

    /**
     * Create a new TangleBase
     */
    constructor() {}

    /**
     * Load options into instance variables.
     * @param {object} options Key/Value pairs.
     */
    loadOptions(options) {
        if (typeof options === 'undefined') options = {};
        if (!('allowableOptions' in options)) {
            options.allowableOptions = {};
        }
        for (const key in this.optionsAllowed) {
            options.allowableOptions[key] = this.optionsAllowed[key];
        }
        let allowable = options.allowableOptions;
        delete options.allowableOptions;
        for (const property in options) {
            if (property in allowable) {
                this[property] = options[property];
            } else {
                console.log("ERROR: Ignoring option: ", property);
            }
        }
        for (const property in allowable) {
            if (typeof this[property] === 'undefined' && typeof allowable[property] !== 'undefined') {
                this[property] = allowable[property];
            }
        }
    }
}

/**
 * @typedef {Object} TangleElementOptions
 * @property {number} debug The debug level.
 * @property {p5.Color} fillColor The color with which to fill shapes.
 * @property {p5.Color} strokeColor The color with which to draw lines.
 */

/**
 * Base class for a repeatable element of a tangle.
 */
class TangleElement extends TangleBase {

    /**
     * Create a TangleElement.
     * @param {p5.Graphics} g The graphics object to write to.
     * @param {Point} center The location of the element.
     * @param {TangleElementOptions} options A map of values to be loaded into instance variables.
     */
    constructor(g, center, options) {
        super();
        this.g = g;
        this.center = center == undefined ? Point(0, 0) : center;
        this.poly = [];

        this.optionsAllowed = {
            debug: 0,
            fillColor: 0,
            strokeColor: 0,
        };

        this.loadOptions(options);
    }

    /**
     * Add a vertex to the enclosing polygon for this TangleElement.
     * @param {Point} p A Point describing the vertex location.
     */
    addVertex(p) {
        this.poly.push(createVector(p.x, p.y));
    }

    /**
     * Get the vertices of the enclosing polygon.
     * @returns [p5.Vector] An array of vertices for the enclosing polygon.
     */
    getPoly() {
        return this.poly;
    }

    /**
     * Draw the enclosing polygon. This is done with some transparency, and is meant as a debugging aid.
     */
    // drawPoly() {
    //     if (!this.debug) {
    //         return;
    //     }
    //     fill(128,0,0,128);
    //     beginShape();
    //     for(let i=0; i<this.poly.length; ++i){
    //         vertex(this.poly[i].x, this.poly[i].y);
    //     }
    //     endShape(CLOSE);
    // }

    /**
     * Draw the TangleElement
     */
    draw() {
        this.g.fill(this.fillColor);
        this.g.stroke(this.strokeColor);
    }
}

/**
 * @typedef {Object} TangleOptions
 * @property {number} debug The debug level.
 * @property {p5.Color} background The color with which to fill the background.
 * @property {object[]} polys Polygons already drawn.
 * @property {boolean} avoidCollisions If true, do not draw over other elements listed in this.polys. The default is true.
 * @property {Point[]} maskPoly A set of points defining a polygon. Only the portion of the image inside the polygon will be displayed, unless ignoreMask is true.
 * @property {boolean} addStrings If true, the boundaries of the maskPoly are drawn. The default is true.
 * @property {boolean} ignoreMask If true, do not mask the result, draw the entire rectangle. The default is false.
 * @property {number} tangleRotate The number of degrees by which to rotate the tangle before applying the mask. The default is 0.
 */

/**
 * Base class for a tangle, which is an area filled with TangleElements
 */
class Tangle extends TangleBase {

    /**
     * Create a new Tangle
     * @param {Point[] | Polygon} mask Vertices of a polygon used as a mask. Only the portion of the tangle inside the polygon will be visible.
     * @param {TangleOptions} options A map of values to be loaded into instance variables.
     */
    constructor(mask, options) {
        super();
        this.maskPoly = mask;
        if (Array.isArray(mask)) {
            this.maskPoly = new Polygon(mask);
        }
        this.build = function () {};

        this.optionsAllowed = {
            debug: 0,
            background: undefined,
            polys: [],
            avoidCollisions: true,
            addStrings: true,
            ignoreMask: false,
            tangleRotate: 0,
        };

        this.loadOptions(options);

        const br = this.maskPoly.getBoundingRectangle().copy();
        this.origin = br.getOrigin();
        this.width = br.getWidth();
        this.height = br.getHeight();
        if (this.tangleRotate) {
            // Rotate the mask area, but make sure the bounding rectangle always covers at least the original mask area.
            let minX = this.origin.x;
            let minY = this.origin.y;
            let maxX = minX + this.width;
            let maxY = minY + this.height;
            br.rotate(this.tangleRotate);
            const origin = br.getOrigin();
            const width = br.getWidth();
            const height = br.getHeight();
            minX = Math.floor(Math.min(minX, origin.x));
            minY = Math.floor(Math.min(minY, origin.y));
            maxX = Math.ceil(Math.max(maxX, origin.x + width));
            maxY = Math.ceil(Math.max(maxY, origin.y + height));
            this.origin = new Point(minX, minY);
            this.width = maxX - minX;
            this.height = maxY - minY;
        }
        this.g = createGraphics(this.width, this.height);
        if (this.tangleRotate) {
            // Translate the center of the tangle back to the center of the bounding rectangle
            const r = radians(this.tangleRotate);
            const x = this.width / 2;
            const y = this.height / 2;
            const dx = Math.floor(x - (x * cos(r) - y * sin(r)));
            const dy = Math.floor(y - (x * sin(r) + y * cos(r)));
            this.g.translate(dx, dy);
            this.g.rotate(r);
        }

        // Set background
        if (this.background !== undefined) {
            this.g.background(this.background);
        }
    }

    /**
     * Test an polygon for collisions with existing polygons.
     * @param {p5.Vector[]} poly The polygon to test.
     * @returns {boolean} True if there is a collision.
     */
    collisionTest(poly) {
        if (!this.avoidCollisions)
            return false;
        for (let i = 0; i < this.polys.length; ++i) {
            if (collidePolyPoly(poly, this.polys[i], true)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Paste the graphics buffer onto the canvas at the specified position .
     * @param {Point} position The position at which to place the image on the canvas.
     */
    paste(position) {
        image(this.g, position.x, position.y);
    }

    /**
     * Apply the mask polygon to this tangle. Only the portion of the tangle inside the mask polygon will be displayed.
     */
    applyMask() {
        if (this.ignoreMask) {
            return;
        }

        // Create the mask from the maskPoly
        let mask = createGraphics(this.width, this.height);
        mask.noStroke();
        mask.fill(255, 255, 255, 255);
        mask.beginShape();
        for (let p = 0; p < this.maskPoly.vertices.length; p++) {
            mask.vertex(this.maskPoly.vertices[p].x - this.origin.x, this.maskPoly.vertices[p].y - this.origin.y);
        }
        mask.endShape(CLOSE);

        // Create a masked cloned image
        let clone;
        (clone = this.g.get()).mask(mask.get());

        // Recreate the renderer with the cloned image
        this.g = createGraphics(this.width, this.height);
        this.g.image(clone, 0, 0);

        // If we are drawing strings, do so now
        if (this.addStrings) {
            this.g.stroke(0);
            this.g.fill(0, 0, 0, 0);
            this.g.beginShape();
            for (let p = 0; p < this.maskPoly.vertices.length; p++) {
                this.g.vertex(this.maskPoly.vertices[p].x - this.origin.x, this.maskPoly.vertices[p].y - this.origin.y);
            }
            this.g.endShape(CLOSE);
        }
    }

    /**
     * Build the tangle. Executes the this.build method with before and after processing appropriate to the tangle type.
     * This is normally the last method called by a child class.
     */
    execute() {

        this.build();
        if (!this.ignoreMask) {
            this.applyMask();
        }
    }
}