Source: Aah.js

/*jshint esversion: 9 */

/**
 * @typedef {Object} DotElementOptions
 * @property {number|Range} size Dot diameter.
 * @property {number} spacing Relative dot spacing expressed as a percentage of diameter; defines the size of the enclosing polygon.
 * @property {value} any Any of the TangleElementOptions may be used here.
 */

/**
 * Define the Dot element. . The Dot element is a simple dot or circle on the canvas.
 * <br />
 * <img src='images/DotElement.png' />
 */
class DotElement extends TangleElement {

    /**
     * Create a new DotElement
     * @param {p5.Graphics} g The graphics object to draw to.
     * @param {Point} center The position of the dot.
     * @param {DotElementOptions} options A map of values to be loaded into instance variables.
     */
    constructor(g, center, options) {
        if (typeof options === 'undefined') options = {};
        options.allowableOptions = {
            spacing: 400,
            size: 3,
        };
        super(g, center, options);
        this.spacing = Math.max(100, this.spacing);

        let dAngle = TWO_PI / 8;
        for (let angle = 0; angle < TWO_PI; angle += dAngle) {
            this.addVertex(new Polar(this.spacing / 100 * this.size, angle).toPointCenter(this.center)); // Put a vertex out beyond the tip of the arm
        }
    }

    /**
     * Draw the Dot.
     */
    draw() {
        super.draw();
        this.g.circle(this.center.x, this.center.y, this.size);
        //this.drawPoly();
    }
}

/**
 * @typedef {Object} AahElementTipTypes
 * @property {string} any Any members of this object are preset tip types to indicate special processing
 */

/**
 * @typedef {Object} AahElementOptions
 * @property {number} armCount The number of arms for the Aah.
 * @property {number} gapSDP The percentage of initial arm length (which is half the value of size) to use as a standard deviation when randomly varying central gap for each arm.
 * @property {number} lengthSDP The percentage of initial arm length (which is half the value of size) to use as a standard deviation when randomly varying actual arm length.
 * @property {boolean} rotate If true, rotate the final Aah a random number of degrees.
 * @property {number} size The expected size of the Aah. The actual size will vary depending on random factors.
 * @property {number} thetaSD The angle in degrees to use as a standard deviation when randomly varying the angles between the arms.
 * @property {number} tipDistancePercent The percentage up the arm to place the tip. A valve if 100 puts the tip at the end of each arm.
 * @property {number} tipDiameter The diameter of the tip. The special value of AahElement.tipType.gap makes the tip for each arm the same as that arm's gap.
 * @property {value} any Any of the TangleElementOptions may be used here.
 */

/**
 * Define the Aah element. An Aah element is star-shaped. It needs an approximate size and a position.
 * The size is approximate because the aah's components are built with some size variations.
 * In addition, an Aah can be rotated randomly, and the angle between the arms can vary.
 * <br />
 * <img src='images/AahElement.png' />
 */
class AahElement extends TangleElement {

    /**
     * Special setting for defining Aah tips
     * @type {AahElementTipTypes}
     */
    static tipType = {
        gap: "gap",
    };

    /**
     * Create a new Aah.
     * @param {p5.Graphics} g The graphics object to draw to.
     * @param {Point} center The position of the aah.
     * @param {AahElementOptions} options A map of values to be loaded into instance variables.
     */
    constructor(g, center, options) {
        if (typeof options === 'undefined') options = {};
        options.allowableOptions = {
            armCount: 8,
            thetaSD: 5,
            lengthSDP: 15,
            gapSDP: 10,
            rotate: true,
            tipDistancePercent: 100,
            tipDiameter: AahElement.tipType.gap,
            size: 100,
        };
        super(g, center, options);
        this.length = this.size / 2;
        if (this.armCount < 3) this.armCount = 3;
        this.arms = [];

        const dAngle = TWO_PI / this.armCount;
        const rotation = this.rotate ? random(0, dAngle) : 0;
        for (let angle = 0; angle < TWO_PI - (dAngle / 2); angle += dAngle) {

            // Create the arm
            let c = new Polar(randomGaussian(this.length, this.lengthSDP / 100 * this.length), randomGaussian(angle + rotation, this.thetaSD * Math.PI / 180));
            const gap = randomGaussian(this.gapSDP, this.gapSDP / 7) / 100 * this.length;
            let arm = {
                start: new Polar(gap, c.a).toPointCenter(this.center), // Draw line from here...
                stop: c.toPointCenter(this.center), // ...to here
                tipCenter: new Polar(c.r * (this.tipDistancePercent / 100), c.a).toPointCenter(this.center),
                tipDiameter: !isNaN(this.tipDiameter) ? this.tipDiameter : this.tipDiameter === AahElement.tipType.gap ? gap : 10,
            };
            this.arms.push(arm);

            // Polygon vertices associated with this arm
            let maxLength = Math.max(c.r, c.r * (this.tipDistancePercent / 100)) + arm.tipDiameter / 2;
            this.addVertex(new Polar(maxLength + 5 * gap, c.a).toPointCenter(this.center));
            this.addVertex(new Polar(0.6 * c.r, c.a + (dAngle / 2)).toPointCenter(this.center));
        }
        if (this.debug) console.log("aah: ", this.aah);
    }


    /**
     * Draw the AahElement to the buffer.
     */
    draw() {
        super.draw();
        this.arms.forEach(arm => {
            this.g.line(arm.start.x, arm.start.y, arm.stop.x, arm.stop.y);
            this.g.circle(arm.tipCenter.x, arm.tipCenter.y, arm.tipDiameter);
        });
        // this.drawPoly();
    }
}

/**
 * @typedef {Object} AahElementPlan
 * @property {number} sizeSDP The percentage of initial size to use as a standard deviation when randomly varying the size of each Aah.
 * @property {number} desiredCount The number of Aah elements to try to draw. The actual number drawn will depend on how many will fit.
 * @property {value} any Any of the AahOptions may be used here.
 */

/**
 * @typedef {Object} DotElementPlan
 * @property {value} any Any of the DotOptions may be used here.
 */

/**
 * @typedef {Object} AahPlan
 * @property {AahElementPlan} aah Options for generating individual Aah elements.
 * @property {DotElementPlan} dot Options for generating individual Dot elements.
 */

/**
 * @typedef {Object} AahPlans
 * @property {AahPlan} any Any members of this object are named AahPlan objects to be used as presets.
 */

/**
 * @typedef {Object} AahOptions
 * @property {AahPlan} plan A set of options for underlying elements.
 * @property {value} any Any of the TangleOptions may be used here.
 */

/**
 * Define the Aah tangle.
 * Aah is composed of repeating patterns of AahElement and DotElement, placed randomly on the screen.
 * <br />
 * <img src='images/AahTangle.png' />
 */
class Aah extends Tangle {

    /**
     * Preset plans for the Aah tangle.
     * @type {AahPlans}
     * @static
     */
    static plans = {
        zentangle: {
            aah: {},
            dot: {
                size: new Range(3, 6),
                fillColor: 255,
            },
        },
    };

    /**
     * Create the Aah tangle object.
     * @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 {AahOptions} options A map of values to be loaded into instance variables.
     */
    constructor(mask, options) {
        if (typeof options === 'undefined') options = {};
        options.allowableOptions = {
            plan: Aah.plans.zentangle,
        };
        options.plan = options.plan === undefined ? Aah.plans.zentangle : options.plan;
        super(mask, options);

        this.build = function () {

            if (this.plan.aah === undefined) this.plan.aah = {};
            if (this.plan.dot === undefined) this.plan.dot = {};

            if (this.plan.aah.enable === undefined || this.plan.aah.enable === true) {
                let drawCount = 0;
                let failCount = 0;

                const size = this.plan.aah.size === undefined ?
                    Math.min(this.width, this.height) / 8 : this.plan.aah.size;
                if (this.margin === undefined) this.margin = size / 6;
                const desiredCount = this.plan.aah.desiredCount === undefined ?
                    (this.width / size) * (this.height / size) * 10 : this.plan.aah.desiredCount;
                const sizeSDev = this.plan.aah.sizeSDP === undefined ?
                    (this.plan.aah.sizeSDP / 100) * size : this.plan.aah.sizeSDP;

                while (drawCount < desiredCount) {
                    let center = new Point(random(this.margin, this.width - this.margin), random(this.margin, this.height - this.margin));

                    let options = {
                        debug: this.debug,
                        size: randomGaussian(size, sizeSDev),
                    };
                    if (typeof this.plan.aah.armCount !== 'undefined') options.armCount = this.plan.aah.armCount;
                    if (typeof this.plan.aah.thetaSD !== 'undefined') options.thetaSD = this.plan.aah.thetaSD;
                    if (typeof this.plan.aah.lengthSDP !== 'undefined') options.lengthSDP = this.plan.aah.lengthSDP;
                    if (typeof this.plan.aah.gapSDP !== 'undefined') options.gapSDP = this.plan.aah.gapSDP;
                    if (typeof this.plan.aah.rotate !== 'undefined') options.rotate = this.plan.aah.rotate;
                    if (typeof this.plan.aah.tipDistancePercent !== 'undefined') options.tipDistancePercent = this.plan.aah.tipDistancePercent;
                    if (typeof this.plan.aah.tipDiameter !== 'undefined') options.tip.diameter = this.plan.aahTipDiameter;
                    if (typeof this.plan.aah.fillColor !== 'undefined') options.fillColor = this.plan.aah.fillColor;
                    if (typeof this.plan.aah.strokeColor !== 'undefined') options.strokeColor = this.plan.aah.strokeColor;
                    const aah = new AahElement(this.g, center, options);
                    const poly = aah.getPoly();

                    const conflict = this.collisionTest(poly);
                    if (conflict) {
                        ++failCount;
                        if (failCount > desiredCount * 3) {
                            break;
                        }
                    } else {
                        this.polys.push(poly);
                        aah.draw();
                        ++drawCount;
                    }
                }
            }

            if (this.plan.dot.enable === undefined || this.plan.dot.enable === true) {
                const size = this.plan.dot.size === undefined ? 3 : this.plan.dot.size;
                const sizeIsNum = isNaN(size) ? false : true;
                const ds = (sizeIsNum ? size : size.max) * 2;
                const desiredCount = (this.width / ds) * (this.height / ds);
                for (let i = 0; i < desiredCount; ++i) {
                    const center = new Point(random(this.margin, this.width - this.margin), random(this.margin, this.height - this.margin));
                    let options = {
                        debug: this.debug,
                        size: sizeIsNum ? size : size.rand(),
                    };
                    if (typeof this.plan.dot.spacing !== 'undefined') options.spacing = this.plan.dot.spacing;
                    if (typeof this.plan.dot.fillColor !== 'undefined') options.fillColor = this.plan.dot.fillColor;
                    if (typeof this.plan.dot.strokeColor !== 'undefined') options.strokeColor = this.plan.dot.strokeColor;
                    const dot = new DotElement(this.g, center, options);
                    const poly = dot.getPoly();

                    const conflict = this.collisionTest(poly);
                    if (!conflict) {
                        this.polys.push(poly);
                        dot.draw();
                    }
                }
            }
        };

        this.execute();
    }
}