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