/*jshint esversion: 9 */
/**
* @typedef {Object} GridTangleOptions
* @property {boolean} gridShow If true, and this is a grid-based tangle, draw the grid lines after building the tangle. The default is true.
* @property {number | Range} gridSpacing The grid size in pixels. If used, both gridXSpacing and gridYSpacing are set to this.
* @property {number | Range} gridXSpacing The horizontal grid size in pixels. The default is 40. If set to a Range, the gridSpacingMode determines how that range is used.
* @property {number | Range} gridYSpacing The vertical grid size in pixels. The default is 40. If set to a Range, the gridSpacingMode determines how that range is used.
* @property {string} gridXSpacingMode The horizontal grid size in pixels. The default is 'static'.
* @property {string} gridYSpacingMode The vertical grid size in pixels. The default is 'static'.
* @property {number} gridVary The grid point location variation in pixels. If used, both gridXVary and gridYVary are set to this.
* @property {number} gridXVary The horizontal grid point location variation in pixels.
* @property {number} gridYVary The vertical grid point location variation in pixels.
* @property {number} gridDivisions The number of columns and rows the grid should have. If used, both gridXDivisions and gridYDivisions are set to this.
* @property {number} gridXDivisions The number of columns the grid should have. If not set, this is calculated from width and gridXSpacing.
* @property {number} gridYDivisions The number of rows the grid should have. If not set, this is calculated from height and gridYSpacing.
* @property {number | Range} gridXOrigin The x-coodinate of the upper left corner of the graph. The grid mode may modify this.
* @property {number | Range} gridYOrigin The y-coodinate of the upper left corner of the graph. The grid mode may modify this.
* @property {number | Range} gridXFrequency The grid horizontal wave frequency. The grid mode may ignore this or interpret it as it pleases.
* @property {number | Range} gridYFrequency The grid vertical wave frequency. The grid mode may ignore this or interpret it as it pleases.
* @property {number | Range} gridXAmplitude The grid horizontal wave amplitude. The grid mode may ignore this or interpret it as it pleases.
* @property {number | Range} gridYAmplitude The grid vertical wave amplitude. The grid mode may ignore this or interpret it as it pleases.
*/
/**
* Base class for a grid tangle, which is an area containg a design based on a grid.
*/
class GridTangle extends Tangle {
/**
* Create a new GridTangle
* @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 {GridTangleOptions} options A map of values to be loaded into instance variables.
*/
constructor(mask, options) {
if (typeof options === 'undefined') options = {};
if (typeof options.allowableOptions == 'undefined') options.allowableOptions = {};
options.allowableOptions = {
...options.allowableOptions,
...{
gridShow: false,
gridSpacing: undefined,
gridXSpacing: 40,
gridYSpacing: 40,
gridXSpacingMode: 'static',
gridYSpacingMode: 'static',
gridVary: undefined,
gridXVary: undefined,
gridYVary: undefined,
gridDivisions: undefined,
gridXDivisions: undefined,
gridYDivisions: undefined,
gridXOrigin: undefined,
gridYOrigin: undefined,
gridXFrequency: undefined,
gridYFrequency: undefined,
gridXAmplitude: undefined,
gridYAmplitude: undefined,
},
};
super(mask, options);
this.gridPoints = [];
this.gridMeta = [];
this.gridVariation = [];
// Check gridSpacing
if (this.gridSpacing !== undefined) {
if (typeof this.gridSpacing === 'object') {
// gridSpacing is a Range
this.gridXSpacing = new Range(this.gridSpacing.min, this.gridSpacing.max);
this.gridYSpacing = new Range(this.gridSpacing.min, this.gridSpacing.max);
} else {
this.gridXSpacing = this.gridYSpacing = this.gridSpacing;
}
}
if (this.gridVary !== undefined) {
this.gridXVary = this.gridYVary = this.gridVary;
}
if (this.gridXVary === undefined) {
this.gridXVary = 0.02 * (typeof this.gridXSpacing === 'object' ? this.gridXSpacing.min : this.gridXSpacing);
}
if (this.gridYVary === undefined) {
this.gridYVary = 0.02 * (typeof this.gridYSpacing === 'object' ? this.gridYSpacing.min : this.gridYSpacing);
}
if (this.gridDivisions !== undefined) {
this.gridXDivisions = this.gridYDivisions = this.gridDivisions;
}
if (this.gridXDivisions === undefined) {
const minXSpacing = (typeof this.gridXSpacing === 'object' ? this.gridXSpacing.min : this.gridXSpacing);
this.gridXDivisions = (this.width / minXSpacing) + 2;
}
if (this.gridYDivisions === undefined) {
const minYSpacing = (typeof this.gridYSpacing === 'object' ? this.gridYSpacing.min : this.gridYSpacing);
this.gridYDivisions = (this.height / minYSpacing) + 2;
}
if (this.gridXOrigin === undefined) {
this.gridXOrigin = -Entanglement.getMinValue(this.gridXSpacing) / 2;
}
if (this.gridYOrigin === undefined) {
this.gridYOrigin = -Entanglement.getMinValue(this.gridYSpacing) / 2;
}
}
/**
* Build a set of grid points using the grid* options. There will be enough ppints to fill the rectangular area
* defined by the mask. Some of the points may be well outside the area, depending on the algorithm used to
* produce the points.
*/
buildGridPoints() {
this.gridPoints = [];
this.gridMeta = [];
let colGen, rowGen;
switch (this.gridXSpacingMode) {
case 'wave':
colGen = new GridSpacingModeWave(this);
break;
case 'compression':
colGen = new GridSpacingModeCompression(this);
break;
case 'linear':
colGen = new GridSpacingModeLinear(this);
break;
default:
colGen = new GridSpacingModeStatic(this);
break;
}
if (this.gridXSpacingMode === this.gridYSpacingMode) {
rowGen = colGen;
} else {
switch (this.gridYSpacingMode) {
case 'wave':
rowGen = new GridSpacingModeWave(this);
break;
case 'compression':
rowGen = new GridSpacingModeCompression(this);
break;
case 'linear':
rowGen = new GridSpacingModeLinear(this);
break;
default:
rowGen = new GridSpacingModeStatic(this);
break;
}
}
for (let r = 0; r < this.gridYDivisions; r++) {
this.gridPoints[r] = [];
this.gridMeta[r] = [];
for (let c = 0; c < this.gridXDivisions; c++) {
this.gridMeta[r][c] = new Point(colGen.x(r, c), rowGen.y(r, c));
this.gridPoints[r][c] = new Point(
random(this.gridMeta[r][c].x - this.gridXVary, this.gridMeta[r][c].x + this.gridXVary),
random(this.gridMeta[r][c].y - this.gridYVary, this.gridMeta[r][c].y + this.gridYVary),
);
}
}
console.log(this.gridPoints);
}
/**
* Draw the grid on the graphics buffer
*/
showGrid() {
if (this.gridPoints.length === 0)
this.buildGridPoints();
for (let r = 0; r < this.gridPoints.length; r++) {
for (let c = 0; c < this.gridPoints[r].length; c++) {
let nextPoint = this.gridPoints[r][c + 1];
if (nextPoint !== undefined) {
this.g.line(this.gridPoints[r][c].x, this.gridPoints[r][c].y, nextPoint.x, nextPoint.y);
}
if (this.gridPoints[r + 1] === undefined)
continue;
nextPoint = this.gridPoints[r + 1][c];
this.g.line(this.gridPoints[r][c].x, this.gridPoints[r][c].y, nextPoint.x, nextPoint.y);
}
}
}
/**
* 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.buildGridPoints();
this.build();
if (this.gridShow) {
this.showGrid();
}
if (!this.ignoreMask) {
this.applyMask();
}
}
}
/**
* Create a rectangular grid with even spacing.
*/
class GridSpacingModeStatic {
/**
* Create a grid spacing mode generator.
* @param {object} tangle The 'this' value from the calling Tangle class.
*/
constructor(tangle) {
this.tangle = tangle;
}
/**
* Get the x value for the specified coordinate.
* @param {integer} r Row
* @param {integer} c Column
*/
x(r, c) {
const spacing = Entanglement.getValue(this.tangle.gridXSpacing);
let x = Entanglement.getValue(this.tangle.gridXOrigin) - spacing / 2;
if (c) {
x = this.tangle.gridPoints[r][c - 1].x + spacing;
}
return x;
}
/**
* Get the y value for the specified coordinate.
* @param {integer} r Row
* @param {integer} c Column
*/
y(r, c) {
const spacing = Entanglement.getValue(this.tangle.gridYSpacing);
let y = Entanglement.getValue(this.tangle.gridYOrigin) - spacing / 2;
if (r) {
y = this.tangle.gridPoints[r - 1][c].y + spacing;
}
return y;
}
}
/**
* Create a rectangular grid with linearly increasing spacing.
*/
class GridSpacingModeLinear {
/**
* Create a grid spacing mode generator.
* @param {object} tangle The 'this' value from the calling Tangle class.
*/
constructor(tangle) {
this.tangle = tangle;
if (typeof this.tangle.gridXSpacing === 'object') {
this.xMin = this.tangle.gridXSpacing.min;
this.xRange = this.tangle.gridXSpacing.max - this.tangle.gridXSpacing.min;
} else {
this.xMin = this.tangle.gridXSpacing;
this.xRange = 0;
}
if (typeof this.tangle.gridYSpacing === 'object') {
this.yMin = this.tangle.gridYSpacing.min;
this.yRange = this.tangle.gridYSpacing.max - this.tangle.gridYSpacing.min;
} else {
this.yMin = this.tangle.gridYSpacing;
this.yRange = 0;
}
}
/**
* Get the x value for the specified coordinate.
* @param {integer} r Row
* @param {integer} c Column
*/
x(r, c) {
let x = Entanglement.getValue(this.tangle.gridXOrigin) - this.xMin / 2;
if (c) {
x = this.tangle.gridPoints[r][c - 1].x + this.xMin + this.xRange * (c / this.tangle.gridXDivisions);
}
return x;
}
/**
* Get the y value for the specified coordinate.
* @param {integer} r Row
* @param {integer} c Column
*/
y(r, c) {
let y = Entanglement.getValue(this.tangle.gridYOrigin) - this.yMin / 2;
if (r) {
y = this.tangle.gridPoints[r - 1][c].y + this.yMin + this.yRange * (r / this.tangle.gridYDivisions);
}
return y;
}
}
/**
* Create a rectangular grid with a sine wave.
*/
class GridSpacingModeWave {
/**
* Create a grid spacing mode generator.
* @param {object} tangle The 'this' value from the calling Tangle class.
*/
constructor(tangle) {
this.tangle = tangle;
this.xAmplitude = typeof this.tangle.gridXAmplitude === 'undefined' ?
1 : Entanglement.getValue(this.tangle.gridXAmplitude);
this.yAmplitude = typeof this.tangle.gridYAmplitude === 'undefined' ?
1 : Entanglement.getValue(this.tangle.gridYAmplitude);
this.xFrequency = typeof this.tangle.gridXFrequency === 'undefined' ?
360 / this.tangle.gridXDivisions : Entanglement.getValue(this.tangle.gridXFrequency);
this.yFrequency = typeof this.tangle.gridYFrequency === 'undefined' ?
360 / this.tangle.gridYDivisions : Entanglement.getValue(this.tangle.gridYFrequency);
}
/**
* Get the x value for the specified coordinate.
* @param {integer} r Row
* @param {integer} c Column
*/
x(r, c) {
const spacing = Entanglement.getValue(this.tangle.gridXSpacing);
let x = Entanglement.getValue(this.tangle.gridXOrigin) + Math.sin(radians(this.xFrequency * r)) * spacing * this.xAmplitude;
if (c) {
x = this.tangle.gridPoints[0][c - 1].x + spacing + Math.sin(radians(this.xFrequency * r)) * spacing * this.xAmplitude;
}
return x;
}
/**
* Get the y value for the specified coordinate.
* @param {integer} r Row
* @param {integer} c Column
*/
y(r, c) {
const spacing = Entanglement.getValue(this.tangle.gridYSpacing);
let y = Entanglement.getValue(this.tangle.gridYOrigin) + Math.sin(radians(this.yFrequency * c)) * spacing * this.yAmplitude;
if (r) {
y = this.tangle.gridPoints[r - 1][0].y + spacing + Math.sin(radians(this.yFrequency * c)) * spacing * this.yAmplitude;
}
return y;
}
}
/**
* Create a rectangular grid with a compression wave.
*/
class GridSpacingModeCompression {
/**
* Create a grid spacing mode generator.
* @param {object} tangle The 'this' value from the calling Tangle class.
*/
constructor(tangle) {
this.tangle = tangle;
this.xAmplitude = typeof this.tangle.gridXAmplitude === 'undefined' ?
0.5 : Entanglement.getValue(this.tangle.gridXAmplitude);
this.yAmplitude = typeof this.tangle.gridYAmplitude === 'undefined' ?
0.5 : Entanglement.getValue(this.tangle.gridYAmplitude);
this.xFrequency = typeof this.tangle.gridXFrequency === 'undefined' ?
360 / this.tangle.gridXDivisions : Entanglement.getValue(this.tangle.gridXFrequency);
this.yFrequency = typeof this.tangle.gridYFrequency === 'undefined' ?
360 / this.tangle.gridYDivisions : Entanglement.getValue(this.tangle.gridYFrequency);
}
/**
* Get the x value for the specified coordinate.
* @param {integer} r Row
* @param {integer} c Column
*/
x(r, c) {
const spacing = Entanglement.getValue(this.tangle.gridXSpacing);
let x = Entanglement.getValue(this.tangle.gridXOrigin) - spacing / 2;
if (c) {
x = this.tangle.gridPoints[0][c - 1].x + spacing + Math.sin(radians(this.xFrequency * c)) * spacing * this.xAmplitude;
}
return x;
}
/**
* Get the y value for the specified coordinate.
* @param {integer} r Row
* @param {integer} c Column
*/
y(r, c) {
const spacing = Entanglement.getValue(this.tangle.gridYSpacing);
let y = Entanglement.getValue(this.tangle.gridYOrigin) - spacing / 2;
if (r) {
y = this.tangle.gridPoints[r - 1][0].y + spacing + Math.sin(radians(this.yFrequency * r)) * spacing * this.yAmplitude;
}
return y;
}
}