Source: BoxSpirals.js

/*jshint esversion: 9 */

/**
 * @typedef {Object} BoxSpiralElementOptions
 * @property {number|Range} divisions The number of divisions into which to subdivide the quadrilateral sides. The default is 4.
 * @property {string} rotation The direction of the spiral. Must be 'ccw', 'cw' or 'random'. The default is 'ccw'.
 * @property {string} startCorner The corner on which to start the spiral. Must be 'nw', 'ne', 'se', 'sw' or 'random'. The default is 'nw'.
 * @property {number} size If size is specified, the quadrilateral is a square centered on the specified center.
 * @property {Point} nw The northwest corner of the quadrilateral. Overrides size.
 * @property {Point} ne The northeast corner of the quadrilateral. Overrides size.
 * @property {Point} se The southeast corner of the quadrilateral. Overrides size.
 * @property {Point} sw The southwest corner of the quadrilateral. Overrides size.
 * @property {boolean} interior If true, do not touch the sides of the quadrilateral, draw the spiral inside the quadrilateral with two fewer divisions than specified. The default is false.
 * @property {number|Range} rotate Number of degress to rotate the spiral.
 * @property {value} any Any of the TangleElementOptions may be used here.
 */

/**
 * Define the BoxSpiral Element. The BoxSpiral element is a square spiral which is used in several tangles.
 * Its size and direction of rotation (cw or ccw) can be specified in the options.
 * The spiral can be made to fit any quarilateral; it need not be limited to a square.
 * <br />
 * <img src='images/BoxSpiralElement.png' />
 */
class BoxSpiralElement extends TangleElement {

    /**
     * Create a box spiral element.
     * @param {p5.Graphics} g The graphics buffer on which to draw.
     * @param {Point} center The center of the spiral.
     * @param {BoxSpiralElementOptions} options The options list.
     */
    constructor(g, center, options) {
        if (typeof options === 'undefined') options = {};
        options.allowableOptions = {
            divisions: 4,
            nw: undefined,
            ne: undefined,
            se: undefined,
            sw: undefined,
            rotation: 'ccw',
            size: 50,
            startCorner: 'nw',
            interior: false,
            rotate: undefined,
        };
        if (!('fillColor' in options)) {
            options.fillColor = color(0, 0, 0, 0);
        }
        super(g, center, options);
        this.divisions = Entanglement.getInt(this.divisions);
        this.divisions = Math.max(this.interior ? 3 : 2, this.divisions);
        if (this.rotation === 'random') {
            this.rotation = ['cw', 'ccw'][Math.floor(random(0, 2))];
        }
        if (this.startCorner === 'random') {
            this.startCorner = ['nw', 'ne', 'se', 'sw'][Math.floor(random(0, 4))];
        }

        // If any corners are undefined at this point, create them from the size parameter
        if (this.nw === undefined) this.nw = new Point(center.x - this.size / 2, center.y - this.size / 2);
        if (this.ne === undefined) this.ne = new Point(center.x + this.size / 2, center.y - this.size / 2);
        if (this.se === undefined) this.se = new Point(center.x + this.size / 2, center.y + this.size / 2);
        if (this.sw === undefined) this.sw = new Point(center.x - this.size / 2, center.y + this.size / 2);

        // Do any requested rotation
        if (this.rotate !== undefined) {
            const degrees = Entanglement.getValue(this.rotate);
            this.nw.rotate(degrees, this.center);
            this.ne.rotate(degrees, this.center);
            this.se.rotate(degrees, this.center);
            this.sw.rotate(degrees, this.center);
        }

        // Create the enclosing polygon
        this.addVertex(this.nw);
        this.addVertex(this.ne);
        this.addVertex(this.se);
        this.addVertex(this.sw);

        this.pointPool = this._pointPool();
    }

    /**
     * Alternate BoxSpiralElement constructor using corner coordinates instead of center and size.
     * @param {p5.Graphics} g The graphics buffer on which to draw.g
     * @param {Point} nw The northwest corner of the quadrilateral.
     * @param {Point} ne The northeast corner of the quadrilateral.
     * @param {Point} se The southeast corner of the quadrilateral.
     * @param {Point} sw The southwest corner of the quadrilateral.
     * @param {BoxSpiralElementOptions} options The options list.
     * @returns {BoxSpiralElement}
     */
    static newFromCoordinates(g, nw, ne, se, sw, options) {
        if (typeof options === 'undefined') options = {};
        options.nw = nw;
        options.ne = ne;
        options.se = se;
        options.sw = sw;
        return new BoxSpiralElement(g, new Point((nw.x + ne.x + se.x + sw.x) / 4, (nw.y + ne.y + se.y + sw.y) / 4), options);
    }

    /**
     * Draw the BoxSpiralElement onto the graphics buffer.
     */
    draw() {

        // Fill Quadrilateral
        this.g.noStroke();
        this.g.fill(this.fillColor);
        this.g.beginShape();
        this.g.vertex(this.nw.x, this.nw.y);
        this.g.vertex(this.ne.x, this.ne.y);
        this.g.vertex(this.se.x, this.se.y);
        this.g.vertex(this.sw.x, this.sw.y);
        this.g.endShape(CLOSE);

        // Draw Spiral
        this.g.stroke(this.strokeColor);
        this.current = this._firstPoint();
        let i = 0;
        while (1) {

            const n = this._nextPoint();
            if (n === undefined) {
                break;
            }
            this.g.line(this.pointPool[this.current].x, this.pointPool[this.current].y, this.pointPool[n].x, this.pointPool[n].y);
            this.current = n;
            if (++i > 5 * this.divisions) {
                console.log('BoxSpiralElement - runaway spiral?');
                break;
            }
        }
    }

    /**
     * Create an array of potential points for this box spiral
     * @returns {Point[]} Array of points
     * @private
     */
    _pointPool() {

        // Create the crosshatch
        const lwyPoints = new Line(this.nw, this.sw).divide(this.divisions);
        const leyPoints = new Line(this.ne, this.se).divide(this.divisions);
        const lnxPoints = new Line(this.nw, this.ne).divide(this.divisions);
        const lsxPoints = new Line(this.sw, this.se).divide(this.divisions);
        let vLines = [];
        let hLines = [];
        for (let i = 0; i <= this.divisions; i++) {
            vLines.push(new Line(lnxPoints[i], lsxPoints[i]));
            hLines.push(new Line(lwyPoints[i], leyPoints[i]));
        }

        // Create the point pool from which the spirals will be created, using line intersections
        let points = [];
        for (let h = 0; h <= this.divisions; h++) {
            for (let v = 0; v <= this.divisions; v++) {
                points.push(hLines[h].intersection(vLines[v]));
            }
        }
        return points;
    }

    /**
     * Set the direction of the next move
     * @private
     */
    _nextDirection() {
        this.direction = this.direction + (this.rotation === 'ccw' ? 1 : -1) % 4;
    }

    /**
     * Find the first point -- the point where the spiral starts.
     * @returns {number} The index of the first point in this.pointPool.
     * @private
     */
    _firstPoint() {
        // this.direction is set to some large number (so it does not go negative for cw rotations)
        this.direction = 4 * this.divisions; // divisable by 4, so the initial direction is 0 (down)
        if (this.rotation === 'cw') {
            this.direction++; // if cw, the initial direction is 1.
        }
        this.current = this.interior ? this.divisions + 2 : 0; // Index of first point
        this.step = this.interior ? 2 : 0;
        this.levelCount = 3; // We need three strokes at the first level, 2 for each subsequent level

        // Modifications if the starting corner is other than nw
        switch (this.startCorner) {
            case 'ne':
                this.current = this.interior ? 2 * this.divisions : this.divisions;
                this.direction += 3;
                break;
            case 'se':
                this.current = this.interior ? Math.pow(this.divisions, 2) + this.divisions - 2 : Math.pow(this.divisions + 1, 2) - 1;
                this.direction += 2;
                break;
            case 'sw':
                this.current = this.interior ? Math.pow(this.divisions, 2) : this.divisions * (this.divisions + 1);
                this.direction += 1;
                break;
        }

        return this.current;
    }

    /**
     * Find the next point in the spiral.
     * @returns {number|undefined} The index of the next point in this.pointPool, or undefined when there is no next point.
     * @private
     */
    _nextPoint() {
        let p;
        const interval = this.divisions - this.step;
        if (interval === 0)
            return p;
        switch (this.direction % 4) {
            case 0: // down
                p = this.current + interval * (this.divisions + 1);
                break;
            case 1: // right
                p = this.current + interval;
                break;
            case 2: // up
                p = this.current - interval * (this.divisions + 1);
                break;
            case 3: // left
                p = this.current - interval;
                break;
        }

        // If we have completed the points for this level, move to next
        if (--this.levelCount === 0) {
            ++this.step;
            this.levelCount = 2; // We need 2 points at each level after the first
        }
        this._nextDirection();

        return p;
    }
}

/**
 * @typedef {Object} BoxSpiralOptions
 * @property {number} desiredCount The number of spirals to generate.
 * @property {number|Range} size Size or size range of spirals to generate. The default is 50.
 * @property {number|Range} divisions Number of divisions for each spiral.
 * @property {value} any Any of the TangleOptions may be used here.
 */

/**
 * Define the BoxSpiral Tangle. The BoxSpiral tangle is a collection of BoxSpiralElements placed randomly.
 * It is expected that some elements will partially or completely cover other elements.
 * Generally, enough elements are placed in the area to ensure the area background is completely covered.
 * The spirals may vary in size and rotation.
 * <br />
 * <img src='images/BoxSpiralsTangle.png' />
 */
class BoxSpirals extends Tangle {

    /**
     * Create a new BoxSpiral
     * @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 {BoxSpiralOptions} options The options list.
     */
    constructor(mask, options) {
        if (typeof options === 'undefined') options = {};
        options.allowableOptions = {
            size: 50,
            desiredCount: undefined,
            divisions: undefined,
            rotation: undefined,
            startCorner: undefined,
            rotate: new Range(0, 90),
        };
        super(mask, options);

        this.build = function () {

            if (this.desiredCount === undefined) {
                const s = isNaN(this.size) ? this.size.min : this.size;
                this.desiredCount = Math.floor(this.width / s * this.height / s * 10); // An amount that should cover the buffer
            }

            for (let i = 0; i < this.desiredCount; i++) {
                let bseOpt = {
                    size: Entanglement.getInt(this.size),
                    fillColor: this.background,
                };
                if (this.divisions) bseOpt.divisions = this.divisions;
                if (this.rotation) bseOpt.rotation = this.rotation;
                if (this.rotate) bseOpt.rotate = this.rotate;
                if (this.startCorner) bseOpt.startCorner = this.startCorner;
                const bse = new BoxSpiralElement(this.g, new Point(random(0, this.width), random(0, this.height)), bseOpt);
                bse.draw();
            }
        };

        this.execute();
    }
}