/*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();
}
}