/**
* An extension of p5.js.
* Including module: no-more-for-loops (Copyright 2018 FAL, licensed under MIT).
* GitHub repository: {@link https://github.com/fal-works/p5ex}
* @module p5ex
* @copyright 2018 FAL
* @author FAL <falworks.contact@gmail.com>
* @license MIT
* @version 0.5.6
*/
/**
* Spatial region.
*/
class Region {
}
/**
* Rectangle-shaped spatial region.
*/
class RectangleRegion extends Region {
get width() { return this.rightPositionX - this.leftPositionX; }
get height() { return this.bottomPositionY - this.topPositionY; }
get area() { return this.width * this.height; }
constructor(x1, y1, x2, y2, margin = 0) {
super();
this.leftPositionX = x1 - margin;
this.topPositionY = y1 - margin;
this.rightPositionX = x2 + margin;
this.bottomPositionY = y2 + margin;
}
contains(position, margin = 0) {
return (position.x >= this.leftPositionX - margin && position.x <= this.rightPositionX + margin &&
position.y >= this.topPositionY - margin && position.y <= this.bottomPositionY + margin);
}
constrain(position, margin = 0) {
if (position.x < this.leftPositionX - margin)
position.x = this.leftPositionX - margin;
else if (position.x > this.rightPositionX + margin)
position.x = this.rightPositionX + margin;
if (position.y < this.topPositionY - margin)
position.y = this.topPositionY - margin;
else if (position.y > this.bottomPositionY + margin)
position.y = this.bottomPositionY + margin;
}
}
// default region -> add
/**
* (To be filled)
* @hideConstructor
*/
class ScalableCanvas {
constructor(p5Instance, parameter, node, rendererType) {
this.p = p5Instance;
this.canvasElement = p5Instance.createCanvas(parameter.scaledWidth, parameter.scaledHeight, rendererType);
if (this.canvasElement && 'parent' in this.canvasElement) {
this.canvasElement.parent(node);
}
this.region = new RectangleRegion(0, 0, 0, 0);
this.nonScaledShortSideLength = parameter.nonScaledShortSideLength;
this.updateSize();
}
/**
* (To be filled)
*/
get scaleFactor() {
return this._scaleFactor;
}
/**
* (To be filled)
*/
get nonScaledWidth() {
return this._nonScaledWidth;
}
/**
* (To be filled)
*/
get nonScaledHeight() {
return this._nonScaledHeight;
}
/**
* (To be filled)
*/
get aspectRatio() {
return this._aspectRatio;
}
/**
* (To be filled)
* @param parameter
*/
resize(parameter) {
this.p.resizeCanvas(parameter.scaledWidth, parameter.scaledHeight);
this.nonScaledShortSideLength = parameter.nonScaledShortSideLength;
this.updateSize();
}
/**
* (To be filled)
*/
updateSize() {
const p = this.p;
this._scaleFactor = Math.min(p.width, p.height) / this.nonScaledShortSideLength;
this._inversedScaleFactor = 1 / this._scaleFactor;
this._nonScaledWidth = p.width / this._scaleFactor;
this._nonScaledHeight = p.height / this._scaleFactor;
this._aspectRatio = p.width / p.height;
this.region.rightPositionX = this._nonScaledWidth;
this.region.bottomPositionY = this._nonScaledHeight;
}
/**
* Runs scale() of the current p5 instance for fitting the sketch to the current canvas.
* Should be called every frame before drawing objects on the canvas.
*/
scale() {
this.p.scale(this._scaleFactor);
}
/**
* Runs scale() with the inversed scale factor.
*/
cancelScale() {
this.p.scale(this._inversedScaleFactor);
}
/**
* Converts a length value on the scaled canvas to the non-scaled one.
* Typically used for interpreting mouseX and mouseY.
* @param {number} scaledLength - scaled length value
*/
getNonScaledValueOf(scaledLength) {
return scaledLength / this._scaleFactor;
}
}
ScalableCanvas.DUMMY_PARAMETERS = {
scaledWidth: 100,
scaledHeight: 100,
nonScaledShortSideLength: 100,
};
/**
* (To be filled)
* (This is not implemented as an enum because it is not supported by rollup)
*/
const ScalableCanvasTypes = {
SQUARE640x640: 'SQUARE640x640',
RECT640x480: 'RECT640x480',
FULL: 'FULL',
CUSTOM: 'CUSTOM',
};
class NormalColorUnit {
constructor(p, p5Color) {
this.p = p;
this.p5Color = p5Color;
}
stroke() {
this.p.currentRenderer.stroke(this.p5Color);
}
fill() {
this.p.currentRenderer.fill(this.p5Color);
}
}
class NoColorUnit {
constructor(p) {
this.p = p;
}
stroke() {
this.p.currentRenderer.noStroke();
}
fill() {
this.p.currentRenderer.noFill();
}
}
class UndefinedColorUnit {
stroke() {
}
fill() {
}
}
class AlphaColorUnit {
constructor(p, c, alphaResolution = 256) {
this.p = p;
const array = [];
for (let alphaFactor = 0; alphaFactor < alphaResolution; alphaFactor += 1) {
array.push(p.color(p.red(c), p.green(c), p.blue(c), p.alpha(c) * alphaFactor / (alphaResolution - 1)));
}
this.colorArray = array;
this.maxIndex = alphaResolution - 1;
}
stroke(alphaValue) {
this.p.currentRenderer.stroke(this.getColor(alphaValue));
}
fill(alphaValue) {
this.p.currentRenderer.fill(this.getColor(alphaValue));
}
getColor(alphaValue) {
return this.colorArray[alphaValue ? Math.floor(this.p.map(alphaValue, 0, 255, 0, this.maxIndex)) : this.maxIndex];
}
}
function colorUnit(p, p5Color, alphaEnabled, alphaResolution) {
if (!p || p5Color === undefined)
return new UndefinedColorUnit();
if (p5Color === null)
return new NoColorUnit(p);
if (alphaEnabled)
return new AlphaColorUnit(p, p5Color, alphaResolution);
return new NormalColorUnit(p, p5Color);
}
/**
* Composition of two p5.Color instances. One for stroke(), one for fill().
*/
class ShapeColor {
/**
*
* @param p - p5ex instance.
* @param {p5.Color | null | undefined} strokeColor - Color for stroke(). Null means noStroke().
* @param {p5.Color | null | undefined} fillColor - Color for fill(). Null means noFill().
* @param {boolean} [alphaEnabled]
* @param {number} [alphaResolution]
*/
constructor(p, strokeColor, fillColor, alphaEnabled, alphaResolution) {
this.strokeColor = colorUnit(p, strokeColor, alphaEnabled, alphaResolution);
this.fillColor = colorUnit(p, fillColor, alphaEnabled, alphaResolution);
}
/**
* Applies colors to the current p5 renderer.
* @param {number} alphaValue - Alpha channel value (0 - 255)
*/
applyColor(alphaValue) {
this.strokeColor.stroke(alphaValue);
this.fillColor.fill(alphaValue);
}
}
/**
* Undefined object of p5ex.ShapeColor.
* @static
*/
ShapeColor.UNDEFINED = new ShapeColor(undefined, undefined, undefined);
/**
* An empty function.
*/
const EMPTY_FUNCTION = () => { };
/**
* 1.5 * PI
*/
const ONE_AND_HALF_PI = 1.5 * Math.PI;
const dummyP5 = new p5((p) => {
p.setup = () => {
p.noCanvas();
};
});
/**
* Calculates the squared value of the Euclidean distance between
* two points (considering a point as a vector object).
*/
function distSq(v1, v2) {
return Math.pow(v2.x - v1.x, 2) + Math.pow(v2.y - v1.y, 2) + Math.pow(v2.z - v1.z, 2);
}
/**
* Multiplies the given matrix and array.
* The number of matrix columns and the array length must be identical.
* @param {number[][]} matrix - Any matrix.
* @param {number[]} array - Any one-dimensional array of numbers.
* @param {number[]} [target] - Target array for receiving the result.
* @returns Product of the given values as an array.
*/
function multiplyMatrixAndArray(matrix, array, target) {
const matrixRowCount = matrix.length;
const matrixColumnCount = matrix[0].length;
const resultArray = target || new Array(matrixRowCount);
for (let row = 0; row < matrixRowCount; row += 1) {
resultArray[row] = 0;
for (let col = 0; col < matrixColumnCount; col += 1) {
resultArray[row] += matrix[row][col] * array[col];
}
}
return resultArray;
}
const TWO_PI = 2 * Math.PI;
/**
* Calculates the difference between two angles in range of -PI to PI.
* @param angleA - the angle to subtract from
* @param angleB - the angle to subtract
*/
function angleDifference(angleA, angleB) {
let diff = (angleA - angleB) % TWO_PI;
if (diff < -Math.PI)
diff += TWO_PI;
else if (diff > Math.PI)
diff -= TWO_PI;
return diff;
}
/**
* Calculates the direction angle from one vector to another.
* @param referencePosition
* @param targetPosition
*/
function getDirectionAngle(referencePosition, targetPosition) {
return Math.atan2(targetPosition.y - referencePosition.y, targetPosition.x - referencePosition.x);
}
// Temporal vectors for calculation use in getClosestPositionOnLineSegment()
const tmpVectorAP = dummyP5.createVector();
const tmpVectorAB = dummyP5.createVector();
/**
* Just lerp.
* @param startValue - The start value.
* @param endValue - The end value.
* @param ratio - The ratio between 0 and 1.
*/
function lerp(startValue, endValue, ratio) {
return startValue + ratio * (endValue - startValue);
}
/**
* Returns random value from the min number up to (but not including) the max number.
*/
function randomBetween(min, max) {
return min + Math.random() * (max - min);
}
/**
* Returns random integer from 0 up to (but not including) the max number.
*/
function randomInt(maxInt) {
return Math.floor(Math.random() * maxInt);
}
/**
* Returns random integer from the min number up to (but not including) the max number.
*/
function randomIntBetween(minInt, maxInt) {
return minInt + randomInt(maxInt - minInt);
}
/**
* Returns one of array elements randomly.
* @param array
*/
function getRandom(array) {
return array[randomInt(array.length)];
}
/**
* Returns n or -n randomly. (n = provided number)
* @param {number} n - any number
*/
function randomSign(n) {
if (Math.random() < 0.5)
return n;
return -n;
}
/**
* Returns and removes one array element randomly.
* @param array
*/
function popRandom(array) {
return array.splice(randomInt(array.length), 1)[0];
}
/**
* Container class of number.
*/
class NumberContainer {
/**
* @constructor
* @param {number} value
*/
constructor(value = 0) {
this.value = value;
}
valueOf() {
return this.value;
}
}
/**
* Null object of NumberContainer.
* @static
*/
NumberContainer.NULL = new NumberContainer();
/**
* (To be filled)
*/
class WeightedRandomSelector {
constructor() {
this.candidateList = [];
this.candidateCount = 0;
this.totalProbabiligyWeight = 0;
}
/**
* Adds one element with provided weight of probability.
* @param element
* @param probabilityWeight
* @chainable
*/
push(element, probabilityWeight) {
this.candidateList.push({
element,
threshold: this.totalProbabiligyWeight + probabilityWeight,
});
this.candidateCount += 1;
this.totalProbabiligyWeight += probabilityWeight;
return this;
}
/**
* Clears all elements.
* @chainable
*/
clear() {
this.candidateList.length = 0;
this.candidateCount = 0;
this.totalProbabiligyWeight = 0;
return this;
}
/**
* Returns one element randomly.
* The probability for each element is:
* (probability weight of the element) / (total probability weight)
*/
get() {
const rnd = Math.random() * this.totalProbabiligyWeight;
for (let i = 0; i < this.candidateCount; i += 1) {
if (rnd < this.candidateList[i].threshold)
return this.candidateList[i].element;
}
return this.candidateList[this.candidateCount - 1].element; // unreachable
}
}
/**
* Linear easing function.
* @param ratio
*/
function easeLinear(ratio) {
return ratio;
}
/**
* easeOutQuad.
* @param ratio
*/
function easeOutQuad(ratio) {
return -Math.pow(ratio - 1, 2) + 1;
}
/**
* easeOutCubic.
* @param ratio
*/
function easeOutCubic(ratio) {
return Math.pow(ratio - 1, 3) + 1;
}
/**
* easeOutQuart.
* @param ratio
*/
function easeOutQuart(ratio) {
return -Math.pow(ratio - 1, 4) + 1;
}
const EASE_OUT_BACK_DEFAULT_COEFFICIENT = 1.70158;
/**
* easeOutBack.
* @param ratio
*/
function easeOutBack(ratio) {
const r = ratio - 1;
return ((EASE_OUT_BACK_DEFAULT_COEFFICIENT + 1) * Math.pow(r, 3)
+ EASE_OUT_BACK_DEFAULT_COEFFICIENT * Math.pow(r, 2) + 1);
}
/**
* Returns an easeOut function.
* @param exponent - Integer from 1 to 4.
*/
function getEasingFunction(exponent) {
switch (Math.floor(exponent)) {
default:
case 1:
return easeLinear;
case 2:
return easeOutQuad;
case 3:
return easeOutCubic;
case 4:
return easeOutQuart;
}
}
/**
* (To be filled)
*/
class RandomShapeColor {
constructor() {
this.candidateArray = [];
}
/**
* (To be filled)
* @param createShapeColor - Any function which returns a p5ex.ShapeColor instance.
* @param {number} candidateCount - Number of color candidates to push.
*/
pushCandidateFromFunction(createShapeColor, candidateCount) {
for (let i = 0; i < candidateCount; i += 1) {
this.candidateArray.push(createShapeColor());
}
return this;
}
/**
* (To be filled)
* @param {p5.Color} shapeColor - Any p5.Color instance.
* @param {number} candidateCount - Number of color candidates to push.
*/
pushCandidate(shapeColor, candidateCount = 1) {
for (let i = 0; i < candidateCount; i += 1) {
this.candidateArray.push(shapeColor);
}
return this;
}
/**
* Clears all color candidates.
*/
clear() {
this.candidateArray.length = 0;
return this;
}
/**
* Returns one of color candidates randomly.
*/
get() {
return getRandom(this.candidateArray);
}
}
function createCielabToXyzFunc() {
const delta = 6 / 29;
const constantA = 16 / 116;
const constantB = 3 * delta * delta;
return (value) => {
if (value > delta)
return value * value * value;
return (value - constantA) * constantB;
};
}
const cielabToXyzFunc = createCielabToXyzFunc();
/**
* Converts color values from CIELAB (D65) to XYZ.
* @param {number[]} cielabValue - Value array of L*, a*, b* (D65).
* @param {Illuminant} illuminant - Instance of Illuminant.
* @param {number[]} [target] - Target array for receiving the result.
* @returns {number[]} XYZ value array.
*/
function cielabValueToXyzValue(cielabValue, illuminant, target) {
const yFactor = (cielabValue[0] + 16.0) / 116.0;
const xFactor = yFactor + cielabValue[1] / 500.0;
const zFactor = yFactor - cielabValue[2] / 200.0;
if (target) {
target[0] = illuminant.tristimulusValues[0] * cielabToXyzFunc(xFactor);
target[1] = illuminant.tristimulusValues[1] * cielabToXyzFunc(yFactor);
target[2] = illuminant.tristimulusValues[2] * cielabToXyzFunc(zFactor);
return target;
}
return [
illuminant.tristimulusValues[0] * cielabToXyzFunc(xFactor),
illuminant.tristimulusValues[1] * cielabToXyzFunc(yFactor),
illuminant.tristimulusValues[2] * cielabToXyzFunc(zFactor),
];
}
/**
* Matrix for conversion color values from XYZ to linear RGB.
* Values from "7. Conversion from XYZ (D65) to linear sRGB values" in
* http://www.color.org/chardata/rgb/sRGB.pdf (April 2015)
* @constant {number[][]} xyzToLinearRgbConversionMatrix
* @ignore
*/
const xyzToLinearRgbConversionMatrix = [
[3.2406255, -1.537208, -0.4986286],
[-0.9689307, 1.8757561, 0.0415175],
[0.0557101, -0.2040211, 1.0569959],
];
/**
* CIE standard illuminant.
*/
class Illuminant {
constructor(name, tristimulusValues) {
this.name = name;
this.tristimulusValues = tristimulusValues;
}
}
/**
* Map of illuminants.
*/
const Illuminants = {
D50: new Illuminant('D50', [0.9642, 1.0000, 0.8251]),
D55: new Illuminant('D55', [0.9568, 1.0000, 0.9214]),
D65: new Illuminant('D65', [0.95047, 1.00000, 1.08883]),
E: new Illuminant('E', [1, 1, 1]),
};
/**
* Applies display gamma correction to the given number.
* @param value - any number in a linear color space (0 - 1).
* @ignore
*/
function degamma(value) {
if (value <= 0.0031308)
return 12.92 * value;
return 1.055 * Math.pow(value, 1.0 / 2.4) - 0.055;
}
/**
* Returns the difference of two colors. The alpha values of the original colors will be ignored.
* @param {p5.Color} c1 - The color to subtract from
* @param {p5.Color} c2 - The color to subtract
* @param {number} [alphaValue] - Alpha value of the result color
*/
function subtractColor(c1, c2, alphaValue) {
return dummyP5.color(dummyP5.red(c1) - dummyP5.red(c2), dummyP5.green(c1) - dummyP5.green(c2), dummyP5.blue(c1) - dummyP5.blue(c2), alphaValue);
}
/**
* Creates a new p5.Color instance in HSB color mode and
* immediately reset the color mode to default.
* @param {number} h - Hue (0 - 360)
* @param {number} s - Saturation (0 - 100)
* @param {number} b - Brightness (0 - 100)
* @param {number} [a] - Alpha (0 - 255)
*/
function hsbColor(h, s, b, a) {
dummyP5.colorMode(dummyP5.HSB, 360, 100, 100, 255);
const c = dummyP5.color(h, s, b);
dummyP5.colorMode(dummyP5.RGB, 1, 1, 1, 255);
const gammaCorrectedColor = dummyP5.color(degamma(dummyP5.red(c)), degamma(dummyP5.green(c)), degamma(dummyP5.blue(c)), a);
dummyP5.colorMode(dummyP5.RGB, 255, 255, 255, 255);
return gammaCorrectedColor;
}
let currentIlluminant = Illuminants.D50;
/**
* Sets the current illuminant. (e.g. D50, D65 etc.)
* @param illuminant - Any Illuminant.
* @example setIlluminant(Illuminants.D65);
*/
function setIlluminant(illuminant) {
currentIlluminant = illuminant;
}
const temporalArray1 = [0, 0, 0];
const temporalArray2 = [0, 0, 0];
const temporalArray3 = [0, 0, 0];
const temporalArray4 = [0, 0, 0];
function assignArray(array, v0, v1, v2) {
array[0] = v0;
array[1] = v1;
array[2] = v2;
return array;
}
/**
* Clips the given linear RGB factor to the valid range (0 - 1)
* and converts it to an sRGB value (0 - 255).
* @param factor - Factor of either red, green or blue in the linear RGB color space.
* @returns sRGB value.
* @ignore
*/
function linearRgbFactorToSrgbValue(factor) {
return degamma(Math.min(Math.max(factor, 0), 1)) * 255;
}
/**
* Converts CIELAB values to an array of RGB values (0 - 255).
* @param {number} lValue - L*: Lightness (0 - 100)
* @param {number} aValue - a* (0 - ca. 100)
* @param {number} bValue - b* (0 - ca. 100)
* @param {number} [alphaValue] - Alhpa value (0 - 255)
* @returns New Array of sRGB values.
*/
function cielabColor(lValue, aValue, bValue, alphaValue) {
const labValue = assignArray(temporalArray1, lValue, aValue, bValue);
const xyzValue = cielabValueToXyzValue(labValue, currentIlluminant, temporalArray2);
const rgbFactor = multiplyMatrixAndArray(xyzToLinearRgbConversionMatrix, xyzValue, temporalArray3);
const srgbValue = assignArray(temporalArray4, linearRgbFactorToSrgbValue(rgbFactor[0]), linearRgbFactorToSrgbValue(rgbFactor[1]), linearRgbFactorToSrgbValue(rgbFactor[2]));
return alphaValue ? [
srgbValue[0],
srgbValue[1],
srgbValue[2],
alphaValue,
] : [
srgbValue[0],
srgbValue[1],
srgbValue[2],
];
}
/**
* Converts CIELCh values to an array of RGB values (0 - 255).
* @param {number} lValue - L*: Lightness (0 - 100)
* @param {number} cValue - C*: Chroma (0 - ca. 100)
* @param {number} hValue - h*: Hue (0 - 2PI)
* @param {number} [alphaValue] - Alhpa value (0 - 255)
*/
function cielchColor(lValue, cValue, hValue, alphaValue) {
return cielabColor(lValue, cValue * Math.cos(hValue), cValue * Math.sin(hValue), alphaValue);
}
/**
* (To be filled)
*/
class ScreenEffect {
constructor(p) {
this.p = p;
}
}
/**
* (To be filled)
*/
class ScreenShake extends ScreenEffect {
constructor(p, dampingRatio = 0.95) {
super(p);
this.dampingRatio = dampingRatio;
this.amplitude = 0;
this.offsetX = 0;
this.offsetY = 0;
}
apply() {
if (this.amplitude === 0)
return;
this.offsetX = Math.random() * this.amplitude;
this.offsetY = Math.random() * this.amplitude;
this.p.currentRenderer.translate(this.offsetX, this.offsetY);
this.amplitude = this.amplitude * this.dampingRatio;
if (this.amplitude < 1)
this.amplitude = 0;
}
set(amplitude) {
this.amplitude = Math.max(this.amplitude, amplitude);
}
reset() {
this.amplitude = 0;
}
cancel() {
this.p.currentRenderer.translate(-this.offsetX, -this.offsetY);
}
}
/**
* (To be filled)
*/
class ScreenFlash extends ScreenEffect {
constructor(p, flashColor = p.color(255)) {
super(p);
this.alphaValue = 0;
this.valueChange = 0;
this.flashColor = new ShapeColor(p, null, flashColor, true);
}
apply() {
if (this.alphaValue === 0)
return;
this.flashColor.applyColor(this.alphaValue);
this.p.currentRenderer.rect(0, 0, this.p.scalableCanvas.nonScaledWidth, this.p.scalableCanvas.nonScaledHeight);
this.alphaValue -= this.valueChange;
if (this.alphaValue < 1)
this.alphaValue = 0;
}
set(initialAlphaValue, durationSeconds) {
this.alphaValue = initialAlphaValue;
this.valueChange = initialAlphaValue / (durationSeconds * this.p.idealFrameRate);
}
reset() {
this.alphaValue = 0;
}
}
/**
* (To be filled)
*/
class AlphaBackground {
/**
*
* @param p5exInstance
* @param backgroundColor
* @param drawIntervalFrameCount
* @param blendModeString
* @param defaultBlendModeString
*/
constructor(p5exInstance, backgroundColor, drawIntervalFrameCount = 1, blendModeString, defaultBlendModeString) {
this.p = p5exInstance;
this.backgroundColor = backgroundColor;
this.drawIntervalFrameCount = drawIntervalFrameCount;
this.blendModeString = blendModeString;
this.defaultBlendModeString = defaultBlendModeString;
}
/**
* Draws the background.
*/
draw() {
if (this.p.frameCount % this.drawIntervalFrameCount !== 0)
return;
if (this.blendModeString)
this.p.blendMode(this.blendModeString);
this.p.noStroke();
this.p.fill(this.backgroundColor);
this.p.rect(0, 0, this.p.width, this.p.height);
if (this.defaultBlendModeString)
this.p.blendMode(this.defaultBlendModeString);
}
}
/**
* Returns true if the mouse is within the canvas.
* @param p - The p5 instance.
*/
function mouseIsInCanvas(p) {
if (p.mouseX < 0)
return false;
if (p.mouseX > p.width)
return false;
if (p.mouseY < 0)
return false;
if (p.mouseY > p.height)
return false;
return true;
}
function loopArrayLimited(array, callback, arrayLength) {
let i = 0;
while (i < arrayLength) {
callback(array[i], i, array);
i += 1;
}
}
/**
* Executes a provided function once for each array element.
* @param {Array} array
* @param {loopArrayCallBack} callback
*/
function loopArray(array, callback) {
loopArrayLimited(array, callback, array.length);
}
function loopArrayBackwardsLimited(array, callback, arrayLength) {
while (arrayLength--) {
callback(array[arrayLength], arrayLength, array);
}
}
/**
* Executes a provided function once for each array element in descending order.
* @param {Array} array
* @param {loopArrayCallback} callback
*/
function loopArrayBackwards(array, callback) {
loopArrayBackwardsLimited(array, callback, array.length);
}
/**
* @callback loopArrayCallBack
* @param {} currentValue
* @param {number} [index]
* @param {Array} [array]
*/
function roundRobinLimited(array, callback, arrayLength) {
for (let i = 0, len = arrayLength - 1; i < len; i += 1) {
for (let k = i + 1; k < arrayLength; k += 1) {
callback(array[i], array[k]);
}
}
}
/**
* Executes a provided function once for each pair within the array.
* @param {Array} array
* @param {roundRobinCallBack} callback
*/
function roundRobin(array, callback) {
roundRobinLimited(array, callback, array.length);
}
/**
* @callback roundRobinCallBack
* @param {} element
* @param {} otherElement
*/
function nestedLoopJoinLimited(array, otherArray, callback, arrayLength, otherArrayLength) {
for (let i = 0; i < arrayLength; i += 1) {
for (let k = 0; k < otherArrayLength; k += 1) {
callback(array[i], otherArray[k]);
}
}
}
/**
* Joins two arrays and executes a provided function once for each joined pair.
* @param {Array} array
* @param {Array} otherArray
* @param {nestedLoopJoinCallBack} callback
*/
function nestedLoopJoin(array, otherArray, callback) {
nestedLoopJoinLimited(array, otherArray, callback, array.length, otherArray.length);
}
/**
* @callback nestedLoopJoinCallBack
* @param {} element
* @param {} otherElement
*/
/**
* A class containing an array and several loop methods.
*/
class LoopableArray {
/**
* @param {number} initialCapacity
*/
constructor(initialCapacity = 256) {
this.array = new Array(initialCapacity);
this.length = 0;
}
/**
* Returns a specific element.
* It is recommended to check that you are going to specify a valid index number
* before calling this method.
* @returns The specified element.
*/
get(index) {
return this.array[index];
}
/**
* Returns the last element.
* It is recommended to check that this array is not empty before calling this method.
* @returns The last element.
*/
getLast() {
return this.array[this.length - 1];
}
/**
* Adds one element to the end of the array and returns the new length of the array.
* @param {} element - The element to add to the end of the array.
*/
push(element) {
this.array[this.length] = element;
this.length += 1;
return this.length;
}
/**
* Adds elements to the end of the array and returns the new length of the array.
* @param {Array} array - The elements to add to the end of the array.
*/
pushRawArray(array, arrayLength = array.length) {
for (let i = 0; i < arrayLength; i += 1) {
this.array[this.length + i] = array[i];
}
this.length += arrayLength;
return this.length;
}
/**
* Adds all elements from another LoopableArray and returns the new length of the array.
* @param {LoopableArray} otherLoopableArray
*/
pushAll(otherLoopableArray) {
return this.pushRawArray(otherLoopableArray.array, otherLoopableArray.length);
}
/**
* Removes and returns the last element.
* It is recommended to check that this array is not empty before calling this method.
* @returns The last element.
*/
pop() {
this.length -= 1;
return this.array[this.length];
}
/**
* Clears the array.
*/
clear() {
this.length = 0;
}
/**
* @callback loopArrayCallBack
* @param {} currentValue
* @param {number} [index]
* @param {Array} [array]
*/
/**
* Executes a provided function once for each array element.
* @param {loopArrayCallBack} callback
*/
loop(callback) {
loopArrayLimited(this.array, callback, this.length);
}
/**
* Executes a provided function once for each array element in descending order.
* @param {loopArrayCallBack} callback
*/
loopBackwards(callback) {
loopArrayBackwardsLimited(this.array, callback, this.length);
}
/**
* @callback elementPairCallBack
* @param {} element
* @param {} otherElement
*/
/**
* Executes a provided function once for each pair within the array.
* @param {elementPairCallback} callback
*/
roundRobin(callback) {
roundRobinLimited(this.array, callback, this.length);
}
/**
* Joins two arrays and executes a provided function once for each joined pair.
* @param {LoopableArray} otherArray
* @param {elementPairCallback} callback
*/
nestedLoopJoin(otherArray, callback) {
nestedLoopJoinLimited(this.array, otherArray.array, callback, this.length, otherArray.length);
}
}
/**
* (To be filled)
*/
class TwoDimensionalArray extends LoopableArray {
/**
* (To be filled)
* @param {number} xCount
* @param {number} yCount
* @param fillElement
*/
constructor(xCount, yCount, fillElement) {
super(xCount * yCount);
this.xCount = xCount;
this.yCount = yCount;
if (fillElement) {
for (let i = 0, len = xCount * yCount; i < len; i += 1) {
this.push(fillElement);
}
}
}
/**
* Returns the specified element.
* @param x
* @param y
*/
get2D(x, y) {
return this.array[x + this.xCount * y];
}
/**
* (To be filled)
* @param x
* @param y
* @param element
*/
set2D(x, y, element) {
this.array[x + this.xCount * y] = element;
}
}
/**
* A Naive implementation of an edge between two objects.
*/
class NaiveEdge {
/**
*
* @param nodeA
* @param nodeB
*/
constructor(nodeA, nodeB) {
this.nodeA = nodeA;
this.nodeB = nodeB;
}
/**
* Returns true if the provided node is incident to this edge.
* @param node
*/
isIncidentTo(node) {
return node === this.nodeA || node === this.nodeB;
}
/**
* Returns the adjacent node of the given node via this edge.
* If this edge is not incident to the given node, returns always the end point node.
* @param {T} node - any node which is incident to this edge
*/
getAdjacentNode(node) {
if (node === this.nodeB)
return this.nodeA;
return this.nodeB;
}
}
/**
* (To be filled)
*/
class NullCell {
getNeighborCell(relativeX, relativeY) {
return this;
}
setNeighborCell(relativeX, relativeY, cell) { }
}
const NULL = new NullCell();
/**
* (To be filled)
*/
class NaiveCell {
/**
*
* @param neighborRange
*/
constructor(neighborRange = 1) {
this.neighborCells = new TwoDimensionalArray(2 * neighborRange + 1, 2 * neighborRange + 1, NULL);
}
/**
* Returns the specified neighbor cell.
* @param {number} relativeX
* @param {number} relativeY
*/
getNeighborCell(relativeX, relativeY) {
const neighborRange = Math.floor(this.neighborCells.xCount / 2);
if (relativeX < -neighborRange || relativeX > neighborRange ||
relativeY < -neighborRange || relativeY > neighborRange)
return NULL;
return this.neighborCells.get2D(relativeX + neighborRange, relativeY + neighborRange);
}
/**
* Sets the provided cell as a neighbor of this cell.
* @param relativeX
* @param relativeY
* @param cell
*/
setNeighborCell(relativeX, relativeY, cell) {
const neighborRange = Math.floor(this.neighborCells.xCount / 2);
this.neighborCells.set2D(relativeX + neighborRange, relativeY + neighborRange, cell);
}
}
/**
* (To be filled)
*/
class Grid {
/**
*
* @param {number} xCount
* @param {number} yCount
* @param {number} neighborRange
* @param {boolean} loopAtEndOfScreen
*/
constructor(xCount, yCount, neighborRange, loopAtEndOfScreen, cellFactory, nullCell) {
this.nullCell = nullCell;
this.cell2DArray = new TwoDimensionalArray(xCount, yCount, nullCell);
this.cellIndexMap = new Map();
for (let yIndex = 0; yIndex < yCount; yIndex += 1) {
for (let xIndex = 0; xIndex < xCount; xIndex += 1) {
const cell = cellFactory(neighborRange);
this.cell2DArray.set2D(xIndex, yIndex, cell);
this.cellIndexMap.set(cell, { x: xIndex, y: yIndex });
}
}
this.cell2DArray.loop((cell) => {
this.setNeighborCells(cell, neighborRange, loopAtEndOfScreen);
});
}
/**
* Returns the specified cell.
* @param {number} x - X index.
* @param {number} y - Y index.
*/
getCell(x, y) {
return this.cell2DArray.get2D(x, y);
}
/**
* Returns the x and y index of the given cell.
* @param cell
*/
getCellIndex(cell) {
return this.cellIndexMap.get(cell) || { x: -1, y: -1 };
}
/**
* (To be filled)
* @param referenceCell
* @param {number} relX
* @param {number} relY
* @param {boolean} loopAtEndOfScreen
*/
getRelativePositionCell(referenceCell, relX, relY, loopAtEndOfScreen) {
if (referenceCell === this.nullCell)
return referenceCell;
if (relX === 0 && relY === 0)
return referenceCell;
const referenceIndex = this.getCellIndex(referenceCell);
const targetIndex = {
x: referenceIndex.x + relX,
y: referenceIndex.y + relY,
};
if (loopAtEndOfScreen) {
if (targetIndex.x < 0)
targetIndex.x += this.cell2DArray.xCount;
else if (targetIndex.x >= this.cell2DArray.xCount)
targetIndex.x -= this.cell2DArray.xCount;
if (targetIndex.y < 0)
targetIndex.y += this.cell2DArray.yCount;
else if (targetIndex.y >= this.cell2DArray.yCount)
targetIndex.y -= this.cell2DArray.yCount;
}
else {
if (targetIndex.x < 0 || targetIndex.x >= this.cell2DArray.xCount ||
targetIndex.y < 0 || targetIndex.y >= this.cell2DArray.yCount)
return this.nullCell;
}
return this.cell2DArray.get2D(targetIndex.x, targetIndex.y);
}
setNeighborCells(referenceCell, neighborRange, loopAtEndOfScreen) {
for (let relativeX = -neighborRange; relativeX <= neighborRange; relativeX += 1) {
for (let relativeY = -neighborRange; relativeY <= neighborRange; relativeY += 1) {
referenceCell.setNeighborCell(relativeX, relativeY, this.getRelativePositionCell(referenceCell, relativeX, relativeY, loopAtEndOfScreen));
}
}
}
}
/**
* (To be filled)
*/
class DrawableArray extends LoopableArray {
static drawFunction(value) {
value.draw();
}
/**
* Draws all child elements.
*/
draw() {
this.loop(DrawableArray.drawFunction);
}
}
/**
* (To be filled)
*/
class SteppableArray extends LoopableArray {
static stepFunction(value) {
value.step();
}
/**
* Steps all child elements.
*/
step() {
this.loop(SteppableArray.stepFunction);
}
}
/**
* (To be filled)
*/
class SpriteArray extends LoopableArray {
}
SpriteArray.prototype.step = SteppableArray.prototype.step;
SpriteArray.prototype.draw = DrawableArray.prototype.draw;
/**
* (To be filled)
*/
class CleanableArray extends LoopableArray {
/**
*
* @param initialCapacity
*/
constructor(initialCapacity) {
super(initialCapacity);
this.recentRemovedElements = new LoopableArray(initialCapacity);
}
/**
* Updates the variable 'isToBeRemoved'.
* If it has cleanable child elements, calls clean() recursively and
* removes the child elements which are to be removed.
*/
clean() {
this.recentRemovedElements.clear();
let validElementCount = 0;
for (let i = 0; i < this.length; i += 1) {
this.array[i].clean();
if (this.array[i].isToBeRemoved) {
this.recentRemovedElements.push(this.array[i]);
continue;
}
this.array[validElementCount] = this.array[i];
validElementCount += 1;
}
this.length = validElementCount;
}
}
/**
* (To be filled)
*/
class CleanableSpriteArray extends CleanableArray {
}
CleanableSpriteArray.prototype.draw = SpriteArray.prototype.draw;
CleanableSpriteArray.prototype.step = SpriteArray.prototype.step;
/**
* Object pool which calls the provided function for every element when using & recyling.
* Intended to use with the library deePool, but can also be used with another implementation.
*/
class ObjectPool {
/**
*
* @param naivePool - The pool object with use() and recycle(obj) methods.
* @param useProcess - The callback function which will be called in use().
* @param recycleProcess - The callback function which will be called in recycle().
*/
constructor(naivePool, useProcess, recycleProcess) {
this.naivePool = naivePool;
this.useProcess = useProcess || ((object) => { });
this.recycleProcess = recycleProcess || ((object) => { });
this.recycle = (usedObject) => {
this.recycleProcess(usedObject);
this.naivePool.recycle(usedObject);
};
}
/**
* Returns an object which is currently not in use.
*/
use() {
const newObject = this.naivePool.use();
this.useProcess(newObject);
return newObject;
}
/**
* Recycles all elements of the provided array.
* @param array
*/
recycleAll(array) {
array.loop(this.recycle);
}
}
/**
* Array of pooled objects. Recycles every removing object when clean() has been called.
*/
class PoolableArray extends CleanableArray {
constructor(pool, initialCapacity) {
super(initialCapacity);
this.pool = pool;
}
clean() {
super.clean();
this.recentRemovedElements.loop(this.pool.recycle);
this.recentRemovedElements.clear();
}
}
/**
* (To be filled)
*/
class ScaleFactor {
/**
*
* @param p - p5ex instance.
* @param { number } [value = 1]
*/
constructor(p, value = 1) {
this.p = p;
this.internalValue = value;
this.internalReciprocalValue = 1 / value;
}
/**
* The numeric value of the scale factor.
*/
get value() {
return this.internalValue;
}
set value(v) {
if (v === 0) {
this.internalValue = 0.0001;
this.internalReciprocalValue = 10000;
return;
}
this.internalValue = v;
this.internalReciprocalValue = 1 / v;
}
/**
* The reciprocal value of the scale factor.
*/
get reciprocalValue() {
return this.internalReciprocalValue;
}
/**
* Calls scale().
*/
applyScale() {
this.p.currentRenderer.scale(this.internalValue);
}
/**
* Calls scale() with the reciprocal value.
*/
cancel() {
this.p.currentRenderer.scale(this.internalReciprocalValue);
}
}
/**
* (To be filled)
*/
class DrawerBuilder {
/**
*
* @param p
*/
constructor(p) {
/**
* Parameter for drawing.
*/
this.drawParam = {};
this.p = p;
}
/**
* @param element
* @chainable
*/
setElement(element) {
this.element = element;
return this;
}
/**
* @param positionRef
* @chainable
*/
setPositionRef(positionRef) {
this.drawParam.positionRef = positionRef;
return this;
}
/**
* @param offsetPositionRef
* @chainable
*/
setOffsetPositionRef(offsetPositionRef) {
this.drawParam.offsetPositionRef = offsetPositionRef;
return this;
}
/**
* @param rotationAngleRef
* @chainable
*/
setRotationAngleRef(rotationAngleRef) {
this.drawParam.rotationAngleRef = rotationAngleRef;
return this;
}
/**
* @param scaleFactorRef
* @chainable
*/
setScaleFactorRef(scaleFactorRef) {
this.drawParam.scaleFactorRef = scaleFactorRef;
return this;
}
/**
* @param shapeColorRef
* @chainable
*/
setShapeColorRef(shapeColorRef) {
this.drawParam.shapeColorRef = shapeColorRef;
return this;
}
/**
* @param alphaChannelRef
* @chainable
*/
setAlphaChannelRef(alphaChannelRef) {
this.drawParam.alphaChannelRef = alphaChannelRef;
return this;
}
/**
* @param strokeWeightRef
* @chainable
*/
setStrokeWeightRef(strokeWeightRef) {
this.drawParam.strokeWeightRef = strokeWeightRef;
return this;
}
/**
* @param textSizeRef
* @chainable
*/
setTextSizeRef(textSizeRef) {
this.drawParam.textSizeRef = textSizeRef;
return this;
}
/**
* Builds a p5ex.Drawer instance.
*/
build() {
return new Drawer(this.p, this.element, this.drawParam);
}
}
/**
* (To be filled)
*/
class Drawer {
/**
*
* @param p
* @param element
* @param drawParam
*/
constructor(p, element, drawParam) {
this.p = p;
this.set(element, drawParam);
}
/**
* (To be filled)
* @param element
* @param drawParam
*/
set(element, drawParam) {
this.element = element;
this.position = drawParam.positionRef || this.p.createVector();
this.offsetPosition = drawParam.offsetPositionRef || this.p.createVector();
this.rotation = drawParam.rotationAngleRef || NumberContainer.NULL;
this.scaleFactor = drawParam.scaleFactorRef || new ScaleFactor(this.p);
this.shapeColor = drawParam.shapeColorRef || ShapeColor.UNDEFINED;
this.alphaChannel = drawParam.alphaChannelRef || NumberContainer.NULL;
this.strokeWeight = drawParam.strokeWeightRef || NumberContainer.NULL;
this.textSize = drawParam.textSizeRef || NumberContainer.NULL;
this.procedureList = this.createProcedureList(drawParam);
this.procedureListLength = this.procedureList.length;
}
/**
* Draws the content.
*/
draw() {
for (let i = 0, len = this.procedureListLength; i < len; i += 1) {
this.procedureList[i](this);
}
}
drawElement(drawer) {
drawer.element.draw();
}
createProcedureList(drawParam) {
const procedureList = [];
if (drawParam.shapeColorRef) {
if (drawParam.alphaChannelRef)
procedureList.push(this.alphaColor);
else
procedureList.push(this.color);
}
if (drawParam.textSizeRef)
procedureList.push(this.applyTextSize);
if (drawParam.strokeWeightRef)
procedureList.push(this.applyStrokeWeight);
if (drawParam.positionRef) {
if (drawParam.offsetPositionRef)
procedureList.push(this.translateWithOffset);
else
procedureList.push(this.translate);
}
else if (drawParam.offsetPositionRef)
procedureList.push(this.translateOnlyOffset);
if (drawParam.scaleFactorRef)
procedureList.push(this.scale);
if (drawParam.rotationAngleRef)
procedureList.push(this.rotate);
procedureList.push(this.drawElement);
if (drawParam.rotationAngleRef)
procedureList.push(this.cancelRotate);
if (drawParam.scaleFactorRef)
procedureList.push(this.cancelScale);
if (drawParam.positionRef) {
if (drawParam.offsetPositionRef)
procedureList.push(this.cancelTranslateWithOffset);
else
procedureList.push(this.cancelTranslate);
}
else if (drawParam.offsetPositionRef)
procedureList.push(this.cancelTranslateOnlyOffset);
return procedureList;
}
translate(drawer) {
drawer.p.currentRenderer.translate(drawer.position.x, drawer.position.y);
}
cancelTranslate(drawer) {
drawer.p.currentRenderer.translate(-drawer.position.x, -drawer.position.y);
}
translateOnlyOffset(drawer) {
drawer.p.currentRenderer.translate(drawer.offsetPosition.x, drawer.offsetPosition.y);
}
cancelTranslateOnlyOffset(drawer) {
drawer.p.currentRenderer.translate(-drawer.offsetPosition.x, -drawer.offsetPosition.y);
}
translateWithOffset(drawer) {
drawer.p.currentRenderer.translate(drawer.position.x + drawer.offsetPosition.x, drawer.position.y + drawer.offsetPosition.y);
}
cancelTranslateWithOffset(drawer) {
drawer.p.currentRenderer.translate(-(drawer.position.x + drawer.offsetPosition.x), -(drawer.position.y + drawer.offsetPosition.y));
}
rotate(drawer) {
drawer.p.currentRenderer.rotate(drawer.rotation.value);
}
cancelRotate(drawer) {
drawer.p.currentRenderer.rotate(-drawer.rotation.value);
}
scale(drawer) {
if (drawer.scaleFactor.value === 1)
return;
drawer.scaleFactor.applyScale();
}
cancelScale(drawer) {
if (drawer.scaleFactor.value === 1)
return;
drawer.scaleFactor.cancel();
}
color(drawer) {
drawer.shapeColor.applyColor();
}
alphaColor(drawer) {
drawer.shapeColor.applyColor(drawer.alphaChannel.value);
}
applyStrokeWeight(drawer) {
drawer.p.currentRenderer.strokeWeight(drawer.strokeWeight.value);
}
applyTextSize(drawer) {
drawer.p.currentRenderer.textSize(drawer.textSize.value);
}
}
/**
* (To be filled)
*/
class ShapeType {
/**
* @param drawShape
*/
constructor(drawShape) {
this.drawShape = drawShape;
}
}
const COS60 = 1 / 2;
const SIN60 = Math.sqrt(3) / 2;
/**
* Set of shape types.
*/
const ShapeTypes = {
CIRCLE: new ShapeType((renderer, size) => { renderer.ellipse(0, 0, size, size); }),
SQUARE: new ShapeType((renderer, size) => { renderer.rect(0, 0, size, size); }),
REGULAR_TRIANGLE: new ShapeType((renderer, size) => {
const radius = 0.5 * size;
renderer.triangle(radius, 0, -COS60 * radius, -SIN60 * radius, -COS60 * radius, +SIN60 * radius);
}),
REGULAR_TRIANGLE_UPWARD: new ShapeType((renderer, size) => {
const radius = 0.5 * size;
renderer.triangle(0, radius, -SIN60 * radius, -COS60 * radius, +SIN60 * radius, -COS60 * radius);
}),
};
/**
* (To be filled)
*/
class ScalableShape {
/**
*
* @param p5exInstance
* @param shapeType - type chosen from p5ex.ShapeTypes
* @param {number} baseShapeSize
* @param {NumberContainer} [scaleFactorRef]
*/
constructor(p5exInstance, shapeType, baseShapeSize, scaleFactorRef = new NumberContainer(1)) {
this.p = p5exInstance;
this.shapeType = shapeType;
this.baseShapeSize = baseShapeSize;
this.scaleFactorRef = scaleFactorRef;
}
/**
* Draws the shape.
*/
draw() {
this.shapeType.drawShape(this.p, this.scaleFactorRef.value * this.baseShapeSize);
}
}
/**
* (To be filled)
*/
class LineSegment {
constructor(p, x1, y1, x2, y2) {
this.p = p;
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
draw() {
this.p.currentRenderer.line(this.x1, this.y1, this.x2, this.y2);
}
}
/**
* (To be filled)
*/
class CircularArc {
constructor(p, centerPosition, diameter, startAngle, endAngle, isClockwise, startRatio, endRatio) {
this.p = p;
this.centerPosition = centerPosition;
this.diameter = diameter;
this.startAngle = startAngle;
this.endAngle = endAngle;
this.startRatio = startRatio;
this.endRatio = endRatio;
this.isClockwise = isClockwise;
}
get isClockwise() { return this._isClockwise; }
set isClockwise(flag) {
this._isClockwise = flag;
this.validate = flag ? validateClockwise : validateAntiClockwise;
this.drawTrimmedArc = flag ? drawClockwise : drawAntiClockwise;
}
draw() {
this.validate(this);
const angleDifference = this.endAngle.value - this.startAngle.value;
const start = this.startAngle.value +
this.startRatio.value * angleDifference;
const end = this.startAngle.value +
this.endRatio.value * angleDifference;
this.drawTrimmedArc(this.p, this.centerPosition, this.diameter.value, start, end);
}
}
function validateClockwise(arc) {
if (arc.startAngle.value > arc.endAngle.value)
arc.endAngle.value += arc.p.TWO_PI;
}
function validateAntiClockwise(arc) {
if (arc.startAngle.value < arc.endAngle.value)
arc.endAngle.value -= arc.p.TWO_PI;
}
function drawClockwise(p, centerPosition, diameter, trimmedStartAngle, trimmedEndAngle) {
p.currentRenderer.arc(centerPosition.x, centerPosition.y, diameter, diameter, trimmedStartAngle, trimmedEndAngle);
}
function drawAntiClockwise(p, centerPosition, diameter, trimmedStartAngle, trimmedEndAngle) {
p.currentRenderer.arc(centerPosition.x, centerPosition.y, diameter, diameter, trimmedEndAngle, trimmedStartAngle);
}
// temporal vectors for use in QuadraticBezierCurve.
const tmpMidPoint1 = dummyP5.createVector();
const tmpMidPoint2 = dummyP5.createVector();
/**
* Trimmable quadratic bezier curve.
*/
class QuadraticBezierCurve {
/**
*
* @param p
* @param startPoint
* @param controlPoint
* @param endPoint
* @param resolution
* @param startRatioRef
* @param endRatioRef
*/
constructor(p, startPoint, controlPoint, endPoint, resolution, startRatioRef, endRatioRef) {
this.pointList = new Array(resolution + 1);
this.resolution = resolution;
this.startRatio = startRatioRef;
this.endRatio = endRatioRef;
this.p = p;
for (let i = 0; i <= resolution; i += 1) {
const ratio2 = i / resolution;
const ratio1 = 1 - ratio2;
tmpMidPoint1.set(ratio1 * startPoint.x + ratio2 * controlPoint.x, ratio1 * startPoint.y + ratio2 * controlPoint.y);
tmpMidPoint2.set(ratio1 * controlPoint.x + ratio2 * endPoint.x, ratio1 * controlPoint.y + ratio2 * endPoint.y);
this.pointList[i] = p.createVector(ratio1 * tmpMidPoint1.x + ratio2 * tmpMidPoint2.x, ratio1 * tmpMidPoint1.y + ratio2 * tmpMidPoint2.y);
}
}
/**
* Returns true if the provided control point candidate is valid.
* @param controlPoint - The control point candidate to be checked.
* @param startPoint - The start point of the bezier curve.
* @param endPoint - The start point of the bezier curve.
* @param minDistance - Minimum distance between the control point and the start/end point.
* @param minAngle - Minimum angle of the control point.
* @param maxAngle - Maximum angle of the control point.
* @static
*/
static checkControlPoint(controlPoint, startPoint, endPoint, minDistance, minAngle, maxAngle) {
const minDistanceSquared = minDistance * minDistance;
if (distSq(controlPoint, startPoint) < minDistanceSquared)
return false;
if (distSq(controlPoint, endPoint) < minDistanceSquared)
return false;
const angle = Math.abs(angleDifference(getDirectionAngle(controlPoint, startPoint), getDirectionAngle(controlPoint, endPoint)));
if (angle < minAngle)
return false;
if (angle > maxAngle)
return false;
return true;
}
draw() {
const startIndex = Math.floor(this.startRatio.value * this.resolution);
const endIndex = Math.floor(this.endRatio.value * this.resolution);
const indexRemainder = this.endRatio.value * this.resolution - endIndex;
const renderer = this.p.currentRenderer;
const points = this.pointList;
renderer.beginShape();
for (let i = startIndex; i <= endIndex; i += 1) {
renderer.vertex(points[i].x, points[i].y);
}
if (indexRemainder > 0) {
renderer.vertex(points[endIndex].x + indexRemainder * (points[endIndex + 1].x - points[endIndex].x), points[endIndex].y + indexRemainder * (points[endIndex + 1].y - points[endIndex].y));
}
renderer.endShape();
}
}
/**
* Draws a sine wave.
* @param p
* @param drawingLength
* @param peakToPeakAmplitude
* @param waveLength
*/
function drawSineWave(p, drawingLength, peakToPeakAmplitude, waveLength) {
const renderer = p.currentRenderer;
const peakAmplitude = 0.5 * peakToPeakAmplitude;
let reachedEnd = false;
renderer.beginShape();
for (let x = 0; x <= drawingLength; x += 1) {
if (x > drawingLength)
reachedEnd = true;
renderer.vertex(reachedEnd ? drawingLength : x, -peakAmplitude * Math.sin(p.TWO_PI * x / waveLength));
if (reachedEnd)
break;
}
renderer.endShape();
}
/**
* Set color to the specified pixel.
* Should be used in conjunction with loadPixels() and updatePixels().
* @param renderer - Instance of either p5 or p5.Graphics.
* @param x - The x index of the pixel.
* @param y - The y index of the pixel.
* @param red - The red value (0 - 255).
* @param green - The green value (0 - 255).
* @param blue - The blue value (0 - 255).
* @param pixelDensity - If not specified, renderer.pixelDensity() will be called.
*/
function setPixel(renderer, x, y, red, green, blue, alpha, pixelDensity) {
const g = renderer;
const d = pixelDensity || g.pixelDensity();
for (let i = 0; i < d; i += 1) {
for (let j = 0; j < d; j += 1) {
const idx = 4 * ((y * d + j) * g.width * d + (x * d + i));
g.pixels[idx] = red;
g.pixels[idx + 1] = green;
g.pixels[idx + 2] = blue;
g.pixels[idx + 3] = alpha;
}
}
}
/**
* Lerp color to the specified pixel. The alpha channel remains unchanged.
* Should be used in conjunction with loadPixels() and updatePixels().
* @param renderer - Instance of either p5 or p5.Graphics.
* @param x - The x index of the pixel.
* @param y - The y index of the pixel.
* @param red - The red value (0 - 255).
* @param green - The green value (0 - 255).
* @param blue - The blue value (0 - 255).
* @param pixelDensity - If not specified, renderer.pixelDensity() will be called.
* @param lerpRatio - The lerp ratio (0 - 1). If 1, the color will be replaced.
*/
function lerpPixel(renderer, x, y, red, green, blue, pixelDensity, lerpRatio = 1) {
const g = renderer;
const d = pixelDensity || g.pixelDensity();
for (let i = 0; i < d; i += 1) {
for (let j = 0; j < d; j += 1) {
const idx = 4 * ((y * d + j) * g.width * d + (x * d + i));
g.pixels[idx] = lerp(g.pixels[idx], red, lerpRatio);
g.pixels[idx + 1] = lerp(g.pixels[idx + 1], green, lerpRatio);
g.pixels[idx + 2] = lerp(g.pixels[idx + 2], blue, lerpRatio);
// g.pixels[idx + 3] = 255;
}
}
}
/**
* Fill the canvas or graphics (according to p.currentRenderer) with gradation.
* @param p
* @param backgroundColor
* @param fromColor
* @param toColor
* @param lerpRatioExponent
*/
function gradationBackground(p, backgroundColor, fromColor, toColor, lerpRatioExponent = 1) {
const g = p.currentRenderer;
g.background(backgroundColor);
g.strokeWeight(2);
for (let y = 0; y < g.width; y += 1) {
const lerpRatio = Math.pow(y / (g.height - 1), lerpRatioExponent);
g.stroke(p.lerpColor(fromColor, toColor, lerpRatio));
g.line(0, y, g.width - 1, y);
}
}
function lerpPixelForRandomTexture(renderer, x, y, red, green, blue, alpha) {
lerpPixel(renderer, x, y, red, green, blue, undefined, alpha / 255);
}
/**
* Sets the specified color (default: black) to each pixel with a random alpha value.
* @param renderer - Instance of either p5 or p5.Graphics.
* @param {number} maxAlpha - The max value of alpha channel (1 - 255).
* @param {boolean} [blend] - Set true for blending, false for replacing.
* @param {number} [red]
* @param {number} [green]
* @param {number} [blue]
*/
function applyRandomTexture(renderer, maxAlpha, blend = true, red = 0, green = 0, blue = 0) {
const g = renderer;
const width = g.width;
const height = g.height;
const operatePixel = blend ? lerpPixelForRandomTexture : setPixel;
g.loadPixels();
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
operatePixel(renderer, x, y, red, green, blue, Math.random() * maxAlpha);
}
}
g.updatePixels();
return g;
}
/**
* Font class.
*/
class FontUnit {
/**
*
* @param p - p5ex instance.
* @param {string} name - The font name.
* @param {string} [filePath] - The file path of the font.
* Not required if the font is already loaded (e.g. as a web font).
*/
constructor(p, name, filePath) {
this.p = p;
this.filePath = filePath || null;
this.textFontArgument = name;
}
/**
* Loads the font file if the file path has been specified.
*/
loadFile() {
if (this.filePath)
this.textFontArgument = this.p.loadFont(this.filePath);
}
/**
* Applies the font to the current renderer.
*/
applyFont() {
this.p.currentRenderer.textFont(this.textFontArgument);
}
}
/**
* Manager class of FontUnit.
*/
class FontManager {
/**
*
* @param p - p5ex instance.
*/
constructor(p) {
this.p = p;
this.fontMap = new Map();
}
/**
* Registers a new font.
* @param p
* @param name
* @param filePath
* @chainable
*/
register(name, filePath) {
this.fontMap.set(name, new FontUnit(this.p, name, filePath));
return this;
}
/**
* Calls loadFile() for each registered font. Should be called in preload().
*/
loadAll() {
for (const font of this.fontMap.values()) {
font.loadFile();
}
}
/**
* Applies the specified font to the current renderer.
* @param {string} name - The font name.
*/
applyFont(name) {
const font = this.fontMap.get(name);
if (font)
font.applyFont();
}
}
/**
* (To be filled)
*/
class AngleQuantity {
/**
* Null object of AngleQuantity.
* @static
*/
static get NULL() { return NULL$1; }
/**
*
* @param angle
* @param angleVelocity
*/
constructor(angle = 0, angleVelocity = 0) {
this.angleReference = new NumberContainer(angle);
this.angleVelocityReference = new NumberContainer(angleVelocity);
}
/**
* Current angle value.
*/
get angle() { return this.angleReference.value; }
set angle(v) { this.angleReference.value = v; }
/**
* Current anglular velocity value.
*/
get angleVelocity() { return this.angleVelocityReference.value; }
set angleVelocity(v) { this.angleVelocityReference.value = v; }
/**
* Updates the angle.
*/
step() {
this.angle += this.angleVelocity;
}
}
class NullAngleQuantity extends AngleQuantity {
get angle() { return 0; }
set angle(v) { }
get angleVelocity() { return 0; }
set angleVelocity(v) { }
step() { }
}
const NULL$1 = new NullAngleQuantity();
/**
* (To be filled)
*/
class KinematicQuantity {
constructor() {
this.position = new p5.Vector();
this.velocity = new p5.Vector();
}
/**
* Updates the position.
*/
step() {
this.position.add(this.velocity);
}
/**
* Returns the current speed.
*/
getSpeed() {
return this.velocity.mag();
}
/**
* Returns the current direction angle.
*/
getDirection() {
return this.velocity.heading();
}
/**
* Adds the given value to the current speed.
* @param speedChange
*/
addSpeed(speedChange) {
this.velocity.setMag(Math.max(0, this.velocity.mag() + speedChange));
}
}
const temporalVector = dummyP5.createVector();
/**
* (To be filled)
*/
class PhysicsBody {
constructor() {
this.kinematicQuantity = new KinematicQuantity();
this.position = this.kinematicQuantity.position;
this.velocity = this.kinematicQuantity.velocity;
this.mass = 1;
this.collisionRadius = 0;
this.hasFriction = false;
this.decelerationFactor = 1;
}
/**
* X position.
*/
get x() {
return this.position.x;
}
/**
* Y position.
*/
get y() {
return this.position.y;
}
/**
* Z position.
*/
get z() {
return this.position.z;
}
/**
* X velocity.
*/
get vx() {
return this.velocity.x;
}
/**
* Y velocity.
*/
get vy() {
return this.velocity.y;
}
/**
* Z velocity.
*/
get vz() {
return this.velocity.z;
}
/**
* Returns the current speed.
*/
getSpeed() {
return this.kinematicQuantity.getSpeed();
}
/**
* Returns the current direction angle.
*/
getDirection() {
return this.kinematicQuantity.getDirection();
}
/**
* Sets the friction of the body.
* @param constant
*/
setFriction(constant) {
if (constant === 0) {
this.hasFriction = false;
return;
}
this.hasFriction = true;
this.decelerationFactor = 1 - constant;
}
/**
* Constrains the current speed. Should be called every time if needed.
* @param maxSpeed
*/
constrainSpeed(maxSpeed) {
if (this.velocity.magSq() > maxSpeed * maxSpeed)
this.velocity.setMag(maxSpeed);
}
/**
* Updates the body.
*/
step() {
this.kinematicQuantity.step();
if (this.hasFriction) {
this.kinematicQuantity.velocity.mult(this.decelerationFactor);
}
}
/**
* Accelerates the body.
* @param x
* @param y
* @param z
*/
accelerate(x, y, z) {
this.kinematicQuantity.velocity.add(x, y, z);
}
/**
* Apply the provided force to the body.
* @param force
*/
applyForce(force) {
this.accelerate(force.x / this.mass, force.y / this.mass, force.z / this.mass);
}
/**
* Add the provided value to the speed of the body.
* @param speedChange
*/
addSpeed(speedChange) {
this.kinematicQuantity.addSpeed(speedChange);
}
/**
* Returns true if the body collides the provided body.
* @param other
*/
collides(other) {
return (distSq(this.position, other.position) <
this.collisionRadius * this.collisionRadius + other.collisionRadius * other.collisionRadius);
}
/**
* (To be filled)
* @param normalUnitVector
* @param restitution
*/
bounce(normalUnitVector, restitution = 1) {
this.velocity.add(p5.Vector.mult(normalUnitVector, (1 + restitution) * p5.Vector.dot(this.velocity, p5.Vector.mult(normalUnitVector, -1))));
}
/**
* Applies attraction force to both this and the target body.
* @param {PhysicsBody} other - the other body to interact with
* @param {number} magnitudeFactor - the factor of magnitude other than the distance
* @param {number} minMag - the minimum magnitude
* @param {number} maxMag - the maximum magnitude
* @param {number} cutoffMag - does not apply force if magnitude is smaller than this
*/
attractEachOther(other, magnitudeFactor, minMag = 0, maxMag, cutoffMag) {
const force = this.calculateAttractionForce(other.position, magnitudeFactor, minMag, maxMag, cutoffMag);
if (!force)
return;
this.applyForce(force);
force.mult(-1);
other.applyForce(force);
}
/**
* Applies attraction force to this body.
* @param {p5.Vector} targetPosition - the target position
* @param {number} magnitudeFactor - the factor of magnitude other than the distance
* @param {number} minMag - the minimum magnitude
* @param {number} maxMag - the maximum magnitude
* @param {number} cutoffMag - does not apply force if magnitude is smaller than this
*/
attractToPoint(targetPosition, magnitudeFactor, minMag = 0, maxMag, cutoffMag) {
const force = this.calculateAttractionForce(targetPosition, magnitudeFactor, minMag, maxMag, cutoffMag);
if (!force)
return;
this.applyForce(force);
}
calculateAttractionForce(targetPosition, magnitudeFactor, minMag = 0, maxMag, cutoffMag) {
const tmpVec = temporalVector;
p5.Vector.sub(targetPosition, this.position, tmpVec); // set relative position
const distanceSquared = tmpVec.magSq();
let magnitude = Math.abs(magnitudeFactor) / distanceSquared;
if (cutoffMag && magnitude < cutoffMag)
return null;
if (maxMag)
magnitude = Math.min(Math.max(magnitude, minMag), maxMag);
else
magnitude = Math.max(magnitude, minMag);
tmpVec.setMag(magnitude); // set force
if (magnitudeFactor < 0)
tmpVec.mult(-1);
return tmpVec;
}
}
/**
* Returns the 2D force vector which is to be applied to the load.
* @param loadDirectionAngle - the direction angle from the fulcrum to the load
* @param loadDistance - the distance between the fulcrum and the load
* @param effortDistance - the distance between the fulcrum and the effort
* @param effortForceMagnitude - the effort force magnitude
* @param rotateClockwise - true if the load is to be rotated clockwise, otherwise false
* @param target - the vector to receive the result. Will be newly created if not specified
*/
function calculateLeverageForce(loadDirectionAngle, loadDistance, effortDistance, effortForceMagnitude, rotateClockwise, target) {
const force = target || dummyP5.createVector();
const forceDirectionAngle = loadDirectionAngle + (rotateClockwise ? -dummyP5.HALF_PI : dummyP5.HALF_PI);
force.set(Math.cos(forceDirectionAngle), Math.sin(forceDirectionAngle));
force.setMag(effortForceMagnitude * effortDistance / loadDistance); // load force
return force;
}
/**
* (To be filled)
*/
class FrameCounter {
constructor() {
this.count = 0;
}
/**
* Resets the counter.
* @param count
*/
resetCount(count = 0) {
this.count = count;
return this;
}
/**
* Increments the frame count.
*/
step() {
this.count += 1;
}
/**
* Returns the mod.
* @param divisor
*/
mod(divisor) {
return this.count % divisor;
}
}
/**
* (To be filled)
*/
class TimedFrameCounter extends FrameCounter {
/**
* True if this counter is activated.
*/
get isOn() { return this._isOn; }
/**
*
* @param durationFrameCount
* @param completeBehavior
*/
constructor(durationFrameCount, completeBehavior = EMPTY_FUNCTION) {
super();
this._isOn = true;
this.completeBehavior = completeBehavior;
this.durationFrameCount = durationFrameCount;
}
/**
* Activate this counter.
* @param duration
* @chainable
*/
on(duration) {
this._isOn = true;
if (duration)
this.durationFrameCount = duration;
return this;
}
/**
* Deactivate this counter.
* @chainable
*/
off() {
this._isOn = false;
return this;
}
/**
* @override
*/
step() {
if (!this._isOn)
return;
this.count += 1;
if (this.count > this.durationFrameCount) {
this.completeCycle();
}
}
}
/**
* (To be filled)
*/
class LoopedFrameCounter extends TimedFrameCounter {
/**
*
* @param duration
* @param cycleCompleteBehavior
*/
constructor(duration, cycleCompleteBehavior) {
super(duration, cycleCompleteBehavior);
}
/**
* @override
* @chainable
*/
on(duration) {
super.on(duration);
return this;
}
/**
* @override
* @chainable
*/
off() {
super.off();
return this;
}
/**
* @override
*/
getProgressRatio() {
return this.count / this.durationFrameCount;
}
/**
* @override
*/
completeCycle() {
this.completeBehavior();
this.count = 0;
}
}
/**
* (To be filled)
*/
class NonLoopedFrameCounter extends TimedFrameCounter {
/**
* True if the given frame count duration has ellapsed already.
*/
get isCompleted() { return this._isCompleted; }
/**
*
* @param durationFrameCount
* @param completeBehavior
*/
constructor(durationFrameCount, completeBehavior) {
super(durationFrameCount, completeBehavior);
this._isCompleted = false;
}
/**
* @override
* @chainable
*/
on(duration) {
super.on(duration);
return this;
}
/**
* @override
* @chainable
*/
off() {
super.off();
return this;
}
/**
* @override
*/
resetCount() {
super.resetCount();
this._isCompleted = false;
return this;
}
/**
* @override
*/
getProgressRatio() {
return this._isCompleted ? 1 : this.count / this.durationFrameCount;
}
/**
* @override
*/
completeCycle() {
this._isCompleted = true;
this._isOn = false;
this.completeBehavior();
}
}
/**
* Holds a boolean value for each key which indicates if the key is currently down.
*/
const keyDown = new Map();
/**
* Begins to listen key events. Default behaviors for arrow keys will be prevented.
*/
function listenKey() {
window.addEventListener('keydown', (event) => {
keyDown.set(event.key, true);
keyDown.set(event.code, true);
switch (event.key) {
case 'ArrowDown':
case 'ArrowUp':
case 'ArrowLeft':
case 'ArrowRight':
return false;
default:
return;
}
});
window.addEventListener('keyup', (event) => {
keyDown.set(event.key, false);
keyDown.set(event.code, false);
switch (event.key) {
case 'ArrowDown':
case 'ArrowUp':
case 'ArrowLeft':
case 'ArrowRight':
return false;
default:
return;
}
});
}
/**
* Returns true if the mouse cursor is on the canvas.
* @param p - p5ex instance.
*/
function mouseOnCanvas(p) {
if (p.mouseX < 0)
return false;
if (p.mouseX > p.width)
return false;
if (p.mouseY < 0)
return false;
if (p.mouseY > p.height)
return false;
return true;
}
/**
* Extension of p5 class.
*/
class p5exClass extends p5 {
/**
* Sets the current renderer object.
* @param renderer
*/
setCurrentRenderer(renderer) {
this.currentRenderer = renderer;
}
/**
* The non-scaled width of the canvas.
*/
get nonScaledWidth() {
return this.scalableCanvas.nonScaledWidth;
}
/**
* The non-scaled height of the canvas.
*/
get nonScaledHeight() {
return this.scalableCanvas.nonScaledHeight;
}
/**
* The ideal frame rate which was set by setFrameRate().
*/
get idealFrameRate() { return this._idealFrameRate; }
/**
* Anglular displacement in radians per frame which corresponds to 1 cycle per second.
* Set by setFrameRate().
*/
get unitAngleSpeed() { return this._unitAngleSpeed; }
/**
* Positional displacement per frame which corresponds to 1 unit length per second.
* Set by setFrameRate().
*/
get unitSpeed() { return this._unitSpeed; }
/**
* Change of speed per frame which corresponds to 1 unit speed per second.
* Set by setFrameRate().
*/
get unitAccelerationMagnitude() { return this._unitAccelerationMagnitude; }
/**
* Constructor of class p5ex.
* @param sketch
* @param node
* @param sync
*/
constructor(sketch, node, sync) {
super(sketch, typeof node === 'string' ? document.getElementById(node) || undefined : node, sync);
if (!node || typeof node === 'boolean') {
this.node = document.body;
}
else {
this.node = typeof node === 'string' ? document.getElementById(node) || document.body : node;
}
this.currentRenderer = this;
this.maxCanvasRegion = {
width: 0,
height: 0,
getShortSideLength() { return Math.min(this.width, this.height); },
};
this.updateMaxCanvasRegion();
this.setFrameRate();
}
/**
* Calls frameRate() and sets variables related to the frame rate.
* @param {number} [fps=60] - The ideal frame rate per second.
*/
setFrameRate(fps = 60) {
this.frameRate(fps);
if (fps) {
this._idealFrameRate = fps;
this._unitAngleSpeed = 2 * Math.PI / this._idealFrameRate;
this._unitSpeed = 1 / this._idealFrameRate;
this._unitAccelerationMagnitude = this._unitSpeed / this._idealFrameRate;
}
return this;
}
/**
* Updates the value of the variable maxCanvasRegion.
*/
updateMaxCanvasRegion() {
this.maxCanvasRegion.width = this.windowWidth;
this.maxCanvasRegion.height = this.windowHeight;
if (this.node === document.body)
return;
const containerRect = this.node.getBoundingClientRect();
this.maxCanvasRegion.width = containerRect.width;
this.maxCanvasRegion.height = containerRect.height;
}
/**
* Create an instance of ScalableCanvas. This includes calling of createCanvas().
* @param {ScalableCanvasType} type - Type chosen from p5ex.ScalableCanvasTypes.
* @param {ScalableCanvasParameters} [parameters] - Parameters for type CUSTOM.
* @param {string} [rendererType] - Either P2D or WEBGL.
*/
createScalableCanvas(type, parameters, rendererType) {
this.scalableCanvasType = type;
this.scalableCanvas = new ScalableCanvas(this, this.createScalableCanvasParameter(type, parameters), this.node, rendererType);
}
/**
* Resizes the ScalableCanvas. Does not work on OpenProcessing.
* @param {ScalableCanvasType} [type] - Type chosen from p5ex.ScalableCanvasTypes.
* If undefined, the last used type will be used again.
* @param {ScalableCanvasParameters} [parameters] - Parameters for type CUSTOM.
*/
resizeScalableCanvas(type, parameters) {
this.scalableCanvas.resize(this.createScalableCanvasParameter(type || this.scalableCanvasType, parameters));
}
createScalableCanvasParameter(type, parameters) {
this.updateMaxCanvasRegion();
const maxShortSide = this.maxCanvasRegion.getShortSideLength();
switch (type) {
case ScalableCanvasTypes.SQUARE640x640:
return {
scaledWidth: maxShortSide,
scaledHeight: maxShortSide,
nonScaledShortSideLength: 640,
};
case ScalableCanvasTypes.RECT640x480:
return {
scaledWidth: maxShortSide,
scaledHeight: 0.75 * maxShortSide,
nonScaledShortSideLength: 480,
};
case ScalableCanvasTypes.FULL:
return {
scaledWidth: this.maxCanvasRegion.width,
scaledHeight: this.maxCanvasRegion.height,
nonScaledShortSideLength: 640,
};
default:
return parameters || ScalableCanvas.DUMMY_PARAMETERS;
}
}
}
export { p5exClass, loopArray, loopArrayBackwards, roundRobin, nestedLoopJoin, LoopableArray, EMPTY_FUNCTION, distSq, multiplyMatrixAndArray, angleDifference, getDirectionAngle, lerp, randomBetween, randomInt, randomIntBetween, getRandom, popRandom, randomSign, NumberContainer, WeightedRandomSelector, easeLinear, easeOutQuad, easeOutCubic, easeOutQuart, easeOutBack, getEasingFunction, dummyP5, Region, RectangleRegion, ScalableCanvas, ScalableCanvasTypes, ScreenEffect, ScreenShake, ScreenFlash, AlphaBackground, mouseIsInCanvas, TwoDimensionalArray, NaiveEdge, NaiveCell, NullCell, Grid, DrawableArray, SteppableArray, SpriteArray, CleanableArray, CleanableSpriteArray, ObjectPool, PoolableArray, ShapeColor, RandomShapeColor, setIlluminant, cielabColor, cielchColor, Illuminants, degamma, subtractColor, hsbColor, Drawer, DrawerBuilder, ShapeType, ShapeTypes, ScalableShape, LineSegment, CircularArc, QuadraticBezierCurve, drawSineWave, setPixel, lerpPixel, gradationBackground, applyRandomTexture, FontUnit, FontManager, ScaleFactor, AngleQuantity, KinematicQuantity, PhysicsBody, calculateLeverageForce, FrameCounter, LoopedFrameCounter, NonLoopedFrameCounter, keyDown, listenKey, mouseOnCanvas };