diff --git a/Cubes.js b/Cubes.js new file mode 100644 index 0000000..e2aa251 --- /dev/null +++ b/Cubes.js @@ -0,0 +1,18 @@ + + +function setup() { + createCanvas(500, 500); + background(255,255,255) + noFill(); + for (i = 0; i < 30; i++){ + circle(width/2 +random(-width/10, width/10), height/2 +random(-height/10, height/10), random(15)*i); + } +} + + +function draw() { + +} + +function mousePressed(){ +} \ No newline at end of file diff --git a/RecursiveCubes.js b/RecursiveCubes.js new file mode 100644 index 0000000..e8dce14 --- /dev/null +++ b/RecursiveCubes.js @@ -0,0 +1,11 @@ +var numberOfSquaresX = 4; +var numberOfSquaresY = 4; + +function setup() { + createCanvas(500, 500); + background(255,255,255); +} + + +function draw() { +} diff --git a/calmCircles.js b/calmCircles.js new file mode 100644 index 0000000..e3170cc --- /dev/null +++ b/calmCircles.js @@ -0,0 +1,33 @@ +function setup() { + createCanvas(1000, 1000); +} + +function draw() { + background(random(100,200), 0, random(100,200)); + + for (let i = 0; i <= width * sqrt(2); i += 10){ + stroke(random(255),random(255),random(255)) + strokeWeight(2) + noFill() + circle(width/2 + random(0,30),height/2 + random(0,30),i) + + const strokeValue = i%20 ? 100 : 0; + stroke(strokeValue) + } + + for (let i = 0; i < 50; i++){ + stroke(random(255),random(255),random(255)) + let x = random(0, width) + let y = random(0, height) + line(width/2,height/2,x,y) + } +} + +function mousePressed(){ + for (let i = 0; i < 50; i++){ + stroke(random(255),random(255),random(255)) + let x = random(0, width) + let y = random(0, height) + line(mouseX,mouseY,x,y) + } +} \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..bc6840f --- /dev/null +++ b/css/style.css @@ -0,0 +1,13 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: #f0f0f0; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..b096665 --- /dev/null +++ b/index.html @@ -0,0 +1,143 @@ + + +
+ + +
+ *
+ * The numbers can be passed individually, as in
+ * `applyMatrix(2, 0, 0, 0, 2, 0)`. They can also be passed in an array, as in
+ * `applyMatrix([2, 0, 0, 0, 2, 0])`.
+ *
+ * In 3D mode, the parameters `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h`, `i`,
+ * `j`, `k`, `l`, `m`, `n`, `o`, and `p` correspond to elements in the
+ * following transformation matrix:
+ *
+ *
+ *
+ * The numbers can be passed individually, as in
+ * `applyMatrix(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1)`. They can
+ * also be passed in an array, as in
+ * `applyMatrix([2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1])`.
+ *
+ * By default, transformations accumulate. The
+ * push() and pop() functions
+ * can be used to isolate transformations within distinct drawing groups.
+ *
+ * Note: Transformations are reset at the beginning of the draw loop. Calling
+ * `applyMatrix()` inside the draw() function won't
+ * cause shapes to transform continuously.
+ *
+ * @method applyMatrix
+ * @param {Array
+ *
+ *
+ * Passing in `CLOSE` to `endShape()` closes the spline smoothly.
+ * ```js
+ * beginShape();
+ *
+ * // Add the first point.
+ * splineVertex(25, 80);
+ *
+ * // Add the second point.
+ * splineVertex(20, 30);
+ *
+ * // Add the second point.
+ * splineVertex(85, 60);
+ *
+ * endShape(CLOSE);
+ * ```
+ *
+ *
+ *
+ *
+ * By default (`ends: INCLUDE`), the curve passes through
+ * all the points you add with `splineVertex()`, similar to
+ * the spline() function. To draw only
+ * the middle span p1->p2 (skipping p0->p1 and p2->p3), set
+ * `splineProperty('ends', EXCLUDE)`. You don’t need to duplicate
+ * vertices to draw those spans.
+ *
+ * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of
+ * `splineVertex()` has three arguments because each point has x-, y-, and
+ * z-coordinates. By default, the vertex’s z-coordinate is set to 0.
+ *
+ * Note: `splineVertex()` won’t work when an argument is passed to
+ * beginShape().
+ *
+ * @method splineVertex
+ * @param {Number} x x-coordinate of the vertex
+ * @param {Number} y y-coordinate of the vertex
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(220);
+ * noFill();
+ * strokeWeight(1);
+ *
+ * beginShape();
+ * splineVertex(25, 80);
+ * splineVertex(20, 30);
+ * splineVertex(85, 60);
+ * endShape();
+ *
+ * strokeWeight(5);
+ * stroke(0);
+ *
+ * point(25, 80);
+ * point(20, 30);
+ * point(85, 60);
+ *
+ * describe(
+ * 'On a gray background, a black spline passes through three marked points.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ * background(220);
+ *
+ * beginShape();
+ * splineVertex(25, 80);
+ * splineVertex(20, 30);
+ * splineVertex(85, 60);
+ * endShape(CLOSE);
+ *
+ * describe(
+ * 'On a gray background, a closed black spline with a white interior forms a triangular shape with smooth corners.'
+ * );
+ * }
+ *
+ * @example
+ * let ringInnerRadius, ringWidth;
+ * let radius, dRadius;
+ * let theta, dTheta;
+ * let time, dTime;
+ * let vertexCount, unit, offset;
+ *
+ * function setup() {
+ * createCanvas(400, 400);
+ *
+ * vertexCount = 15;
+ * unit = createVector(1, 0);
+ * dTheta = TAU / vertexCount;
+ * dTime = 0.004;
+ *
+ * ringInnerRadius = 25;
+ * ringWidth = 5 * ringInnerRadius;
+ *
+ * offset = width;
+ *
+ * describe(
+ * 'A white blob with a black outline changes its shape over time.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(220);
+ * strokeWeight(2);
+ * translate(width / 2, height / 2);
+ *
+ * time = dTime * frameCount;
+ *
+ * beginShape();
+ * for (let i = 0; i < vertexCount; i++) {
+ * unit.rotate(dTheta);
+ * dRadius = noise(offset + unit.x, offset + unit.y, time) * ringWidth;
+ * radius = ringInnerRadius + dRadius;
+ * splineVertex(radius * unit.x, radius * unit.y);
+ * }
+ * endShape(CLOSE);
+ * }
+ *
+ * @example
+ * let vertexA;
+ * let vertexB;
+ * let vertexC;
+ * let vertexD;
+ * let vertexE;
+ * let vertexF;
+ *
+ * let markerRadius;
+ *
+ * let vectorAB;
+ * let vectorFE;
+ *
+ * let endOfTangentB;
+ * let endOfTangentE;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Initialize variables
+ * // Adjusting vertices A and F affects the slopes at B and E
+ *
+ * vertexA = createVector(35, 85);
+ * vertexB = createVector(25, 70);
+ * vertexC = createVector(30, 30);
+ * vertexD = createVector(70, 30);
+ * vertexE = createVector(75, 70);
+ * vertexF = createVector(65, 85);
+ *
+ * markerRadius = 4;
+ *
+ * vectorAB = p5.Vector.sub(vertexB, vertexA);
+ * vectorFE = p5.Vector.sub(vertexE, vertexF);
+ *
+ * endOfTangentB = p5.Vector.add(vertexC, vectorAB);
+ * endOfTangentE = p5.Vector.add(vertexD, vectorFE);
+ *
+ * splineProperty(`ends`, EXCLUDE);
+ *
+ * // Draw figure
+ *
+ * background(220);
+ *
+ * noFill();
+ *
+ * beginShape();
+ * splineVertex(vertexA.x, vertexA.y);
+ * splineVertex(vertexB.x, vertexB.y);
+ * splineVertex(vertexC.x, vertexC.y);
+ * splineVertex(vertexD.x, vertexD.y);
+ * splineVertex(vertexE.x, vertexE.y);
+ * splineVertex(vertexF.x, vertexF.y);
+ * endShape();
+ *
+ * stroke('red');
+ * line(vertexA.x, vertexA.y, vertexC.x, vertexC.y);
+ * line(vertexB.x, vertexB.y, endOfTangentB.x, endOfTangentB.y);
+ *
+ * stroke('blue');
+ * line(vertexD.x, vertexD.y, vertexF.x, vertexF.y);
+ * line(vertexE.x, vertexE.y, endOfTangentE.x, endOfTangentE.y);
+ *
+ * fill('white');
+ * stroke('black');
+ * circle(vertexA.x, vertexA.y, markerRadius);
+ * circle(vertexB.x, vertexB.y, markerRadius);
+ * circle(vertexC.x, vertexC.y, markerRadius);
+ * circle(vertexD.x, vertexD.y, markerRadius);
+ * circle(vertexE.x, vertexE.y, markerRadius);
+ * circle(vertexF.x, vertexF.y, markerRadius);
+ *
+ * fill('black');
+ * noStroke();
+ * text('A', vertexA.x - 15, vertexA.y + 5);
+ * text('B', vertexB.x - 15, vertexB.y + 5);
+ * text('C', vertexC.x - 5, vertexC.y - 5);
+ * text('D', vertexD.x - 5, vertexD.y - 5);
+ * text('E', vertexE.x + 5, vertexE.y + 5);
+ * text('F', vertexF.x + 5, vertexF.y + 5);
+ *
+ * describe('On a gray background, a black spline passes through vertices A, B, C, D, E, and F, shown as white circles. A red line segment joining vertices A and C has the same slope as the red tangent segment at B. Similarly, the blue line segment joining vertices D and F has the same slope as the blue tangent at E.');
+ * }
+ */
+ /**
+ * @method splineVertex
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} [z] z-coordinate of the vertex.
+ * @chainable
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A ghost shape drawn in white on a blue background. When the user drags the mouse, the scene rotates to reveal the outline of a second ghost.');
+ * }
+ *
+ * function draw() {
+ * background('midnightblue');
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Draw the first ghost.
+ * noStroke();
+ * fill('ghostwhite');
+ *
+ * beginShape();
+ * splineVertex(-28, 41, 0);
+ * splineVertex(-28, 41, 0);
+ * splineVertex(-29, -33, 0);
+ * splineVertex(18, -31, 0);
+ * splineVertex(34, 41, 0);
+ * splineVertex(34, 41, 0);
+ * endShape();
+ *
+ * // Draw the second ghost.
+ * noFill();
+ * stroke('ghostwhite');
+ *
+ * beginShape();
+ * splineVertex(-28, 41, -20);
+ * splineVertex(-28, 41, -20);
+ * splineVertex(-29, -33, -20);
+ * splineVertex(18, -31, -20);
+ * splineVertex(34, 41, -20);
+ * splineVertex(34, 41, -20);
+ * endShape();
+ * }
+ */
+ /**
+ * @method splineVertex
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} [u=0]
+ * @param {Number} [v=0]
+ */
+ /**
+ * @method splineVertex
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} z
+ * @param {Number} [u=0]
+ * @param {Number} [v=0]
+ */
+ fn.splineVertex = function(...args) {
+ let x = 0, y = 0, z = 0, u = 0, v = 0;
+ if (args.length === 2) {
+ [x, y] = args;
+ } else if (args.length === 4) {
+ [x, y, u, v] = args;
+ } else if (args.length === 3) {
+ [x, y, z] = args;
+ } else if (args.length === 5) {
+ [x, y, z, u, v] = args;
+ }
+ this._renderer.splineVertex(x, y, z, u, v);
+ };
+
+ /**
+ * Gets or sets a given spline property.
+ *
+ * Use `splineProperty()` to adjust the behavior of splines
+ * created with `splineVertex()` or `spline()`. You can control
+ * two key aspects of a spline: its end behavior (`ends`) and
+ * its curvature (`tightness`).
+ *
+ * By default, the ends property is set to `INCLUDE`, which means
+ * the spline passes through every point, including the endpoints.
+ * You can also set it to `EXCLUDE` i.e. `splineProperty('ends', EXCLUDE)`,
+ * which makes the spline pass through all points except the endpoints.
+ *
+ * `INCLUDE` case will have the spline passing through
+ * all points, like this:
+ *
+ * ```js
+ * splineProperty('ends', INCLUDE); // no need to set this, as it is the default
+ * spline(25, 46, 93, 44, 93, 81, 35, 85);
+ *
+ * point(25, 46);
+ * point(93, 44);
+ * point(93, 81);
+ * point(35, 85);
+ * ```
+ *
+ *
+ *
+ *
+ * EXCLUDE case will have the spline passing through
+ * the middle points, like this:
+ *
+ *
+ * ```js
+ * splineProperty('ends', EXCLUDE);
+ * spline(25, 46, 93, 44, 93, 81, 35, 85);
+ *
+ * point(25, 46);
+ * point(93, 44);
+ * point(93, 81);
+ * point(35, 85);
+ * ```
+ *
+ *
+ *
+ * By default, the tightness property is set to `0`,
+ * producing a smooth curve that passes evenly through
+ * the vertices. Negative values make the curve looser,
+ * while positive values make it tighter. Common values
+ * range between -1 and 1, though values outside this
+ * range can also be used for different effects.
+ *
+ * For example, To set tightness, use `splineProperty('tightness', t)`,
+ * (default: t = 0).
+ *
+ * Here's the example showing negetive value of tightness,
+ * which creates a rounder bulge:
+ *
+ * ```js
+ * splineProperty('tightness', -5)
+ * stroke(0);
+ * strokeWeight(2);
+ * spline(25, 46, 93, 44, 93, 81, 35, 85);
+ * ```
+ *
+ * Here's the example showing positive value of tightness,
+ * which makes the curve tighter and more angular:
+ *
+ * ```js
+ * splineProperty('tightness', 5)
+ * stroke(0);
+ * strokeWeight(2);
+ * spline(25, 46, 93, 44, 93, 81, 35, 85);
+ * ```
+ *
+ *
+ * In all cases, the splines in p5.js are cardinal splines.
+ * When tightness is 0, these splines are often known as
+ * Catmull-Rom splines
+ *
+ * @method splineProperty
+ * @param {String} property
+ * @param value Value to set the given property to.
+ *
+ * @example
+ * // Move the mouse left and right to see the curve change.
+ *
+ * let t;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ * }
+ *
+ * function draw() {
+ * background(240);
+ *
+ * t = map(mouseX, 0, width, -5, 5, true);
+ * splineProperty('tightness', t);
+ *
+ * noFill();
+ * stroke(0);
+ * strokeWeight(2);
+ *
+ * beginShape();
+ * splineVertex(10, 26);
+ * splineVertex(83, 24);
+ *
+ * splineVertex(83, 61);
+ * splineVertex(25, 65);
+ * endShape();
+ *
+ * push();
+ * strokeWeight(5);
+ * point(10, 26);
+ * point(83, 24);
+ * point(83, 61);
+ * point(25, 65);
+ * pop();
+ *
+ * fill(0);
+ * noStroke();
+ * textSize(10);
+ * text(`tightness: ${round(t, 1)}`, 15, 90);
+ * describe('A black spline forms a sideways U shape through four points. The spline passes through the points more loosely as the mouse moves left of center (negative tightness), and more tightly as it moves right of center (positive tightness). The tightness is displayed at the bottom.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(360, 140);
+ * background(240);
+ * noFill();
+ *
+ * // Right panel: ends = INCLUDE (all spans).
+ * push();
+ * translate(10, 10);
+ * stroke(220);
+ * rect(0, 0, 160, 120);
+ * fill(30);
+ * textSize(11);
+ * text('ends: INCLUDE (all spans)', 8, 16);
+ * noFill();
+ *
+ * splineProperty('ends', INCLUDE);
+ * stroke(0);
+ * strokeWeight(2);
+ * spline(25, 46, 93, 44, 93, 81, 35, 85);
+ *
+ * // vertices
+ * strokeWeight(5);
+ * stroke(0);
+ * point(25, 46);
+ * point(93, 44);
+ * point(93, 81);
+ * point(35, 85);
+ * pop();
+ *
+ * // Right panel: ends = EXCLUDE (middle only).
+ * push();
+ * translate(190, 10);
+ * stroke(220);
+ * rect(0, 0, 160, 120);
+ * noStroke();
+ * fill(30);
+ * text('ends: EXCLUDE ', 18, 16);
+ * noFill();
+ *
+ * splineProperty('ends', EXCLUDE);
+ * stroke(0);
+ * strokeWeight(2);
+ * spline(25, 46, 93, 44, 93, 81, 35, 85);
+ *
+ * // vertices
+ * strokeWeight(5);
+ * stroke(0);
+ * point(25, 46);
+ * point(93, 44);
+ * point(93, 81);
+ * point(35, 85);
+ * pop();
+ *
+ * describe('Left panel shows spline with ends INCLUDE (three spans). Right panel shows EXCLUDE (only the middle span). Four black points mark the vertices.');
+ * }
+ *
+ * @example
+ * let vertexA;
+ * let vertexB;
+ * let vertexC;
+ * let vertexD;
+ * let vertexE;
+ * let vertexF;
+ *
+ * let markerRadius;
+ *
+ * let vectorAB;
+ * let vectorFE;
+ *
+ * let endOfTangentB;
+ * let endOfTangentE;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Initialize variables
+ * // Adjusting vertices A and F affects the slopes at B and E
+ *
+ * vertexA = createVector(35, 85);
+ * vertexB = createVector(25, 70);
+ * vertexC = createVector(30, 30);
+ * vertexD = createVector(70, 30);
+ * vertexE = createVector(75, 70);
+ * vertexF = createVector(65, 85);
+ *
+ * markerRadius = 4;
+ *
+ * vectorAB = p5.Vector.sub(vertexB, vertexA);
+ * vectorFE = p5.Vector.sub(vertexE, vertexF);
+ *
+ * endOfTangentB = p5.Vector.add(vertexC, vectorAB);
+ * endOfTangentE = p5.Vector.add(vertexD, vectorFE);
+ *
+ * splineProperty(`ends`, EXCLUDE);
+ *
+ * // Draw figure
+ *
+ * background(220);
+ *
+ * noFill();
+ *
+ * beginShape();
+ * splineVertex(vertexA.x, vertexA.y);
+ * splineVertex(vertexB.x, vertexB.y);
+ * splineVertex(vertexC.x, vertexC.y);
+ * splineVertex(vertexD.x, vertexD.y);
+ * splineVertex(vertexE.x, vertexE.y);
+ * splineVertex(vertexF.x, vertexF.y);
+ * endShape();
+ *
+ * stroke('red');
+ * line(vertexA.x, vertexA.y, vertexC.x, vertexC.y);
+ * line(vertexB.x, vertexB.y, endOfTangentB.x, endOfTangentB.y);
+ *
+ * stroke('blue');
+ * line(vertexD.x, vertexD.y, vertexF.x, vertexF.y);
+ * line(vertexE.x, vertexE.y, endOfTangentE.x, endOfTangentE.y);
+ *
+ * fill('white');
+ * stroke('black');
+ * circle(vertexA.x, vertexA.y, markerRadius);
+ * circle(vertexB.x, vertexB.y, markerRadius);
+ * circle(vertexC.x, vertexC.y, markerRadius);
+ * circle(vertexD.x, vertexD.y, markerRadius);
+ * circle(vertexE.x, vertexE.y, markerRadius);
+ * circle(vertexF.x, vertexF.y, markerRadius);
+ *
+ * fill('black');
+ * noStroke();
+ * text('A', vertexA.x - 15, vertexA.y + 5);
+ * text('B', vertexB.x - 15, vertexB.y + 5);
+ * text('C', vertexC.x - 5, vertexC.y - 5);
+ * text('D', vertexD.x - 5, vertexD.y - 5);
+ * text('E', vertexE.x + 5, vertexE.y + 5);
+ * text('F', vertexF.x + 5, vertexF.y + 5);
+ *
+ * describe('On a gray background, a black spline passes through vertices A, B, C, D, E, and F, shown as white circles. A red line segment joining vertices A and C has the same slope as the red tangent segment at B. Similarly, the blue line segment joining vertices D and F has the same slope as the blue tangent at E.');
+ * }
+ */
+ /**
+ * @method splineProperty
+ * @param {String} property
+ * @returns The current value for the given property.
+ */
+ fn.splineProperty = function(property, value) {
+ return this._renderer.splineProperty(property, value);
+ };
+
+ /**
+ * Sets multiple properties for spline curves at once.
+ *
+ * `splineProperties()` accepts an object with key-value pairs to configure
+ * how spline curves are drawn. This is a convenient way to set multiple
+ * spline properties with a single function call, rather than calling
+ * splineProperty() multiple times.
+ *
+ * The properties object can include:
+ * - `tightness`: A number that controls how tightly the curve fits to the
+ * vertex points. The default value is 0. Positive values make the curve
+ * tighter (straighter), while negative values make it looser. Values
+ * between -5 and 5 work best.
+ * - `ends`: Controls whether to draw the end segments of the spline. Set to
+ * `EXCLUDE` to skip drawing the segments between the first and second
+ * points and between the second-to-last and last points. This is useful
+ * when you want to use the first and last points as control points only.
+ *
+ * `splineProperties()` affects curves drawn with
+ * splineVertex() within
+ * beginShape() and
+ * endShape(), as well as curves drawn with
+ * spline(). The properties remain active until
+ * changed by another call to `splineProperties()` or
+ * splineProperty().
+ *
+ * @method splineProperties
+ * @param {Object} values an object containing spline property key-value pairs
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ * background(220);
+ *
+ * // Set spline tightness using splineProperties
+ * splineProperties({
+ * tightness: 0.5
+ * });
+ *
+ * // Draw a spline curve
+ * noFill();
+ * stroke(0);
+ * strokeWeight(2);
+ *
+ * beginShape();
+ * splineVertex(20, 80);
+ * splineVertex(30, 30);
+ * splineVertex(70, 30);
+ * splineVertex(80, 80);
+ * endShape();
+ *
+ * // Show vertex points
+ * fill(255, 0, 0);
+ * noStroke();
+ * circle(20, 80, 6);
+ * circle(30, 30, 6);
+ * circle(70, 30, 6);
+ * circle(80, 80, 6);
+ *
+ * describe('A smooth curved line with tightness 0.5 connecting four red points.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ * background(220);
+ *
+ * // Exclude end segments - first and last points become control points
+ * splineProperties({
+ * tightness: 0,
+ * ends: EXCLUDE
+ * });
+ *
+ * // Draw curve only between middle points
+ * noFill();
+ * stroke(0);
+ * strokeWeight(2);
+ *
+ * beginShape();
+ * splineVertex(10, 50); // Control point (affects curve but not drawn to)
+ * splineVertex(30, 20); // Start of visible curve
+ * splineVertex(70, 80); // End of visible curve
+ * splineVertex(90, 50); // Control point (affects curve but not drawn to)
+ * endShape();
+ *
+ * // Show all points
+ * fill(200, 0, 0);
+ * noStroke();
+ * circle(10, 50, 6); // Control point
+ * circle(90, 50, 6); // Control point
+ *
+ * fill(0, 0, 255);
+ * circle(30, 20, 6); // Visible curve point
+ * circle(70, 80, 6); // Visible curve point
+ *
+ * describe('A curved line between two blue points, with red control points at the ends.');
+ * }
+ *
+ * @method splineProperties
+ * @return {Object}
+ */
+ fn.splineProperties = function(values) {
+ return this._renderer.splineProperties(values);
+ };
+
+ /**
+ * Adds a vertex to a custom shape.
+ *
+ * `vertex()` sets the coordinates of vertices drawn between the
+ * beginShape() and
+ * endShape() functions.
+ *
+ * The first two parameters, `x` and `y`, set the x- and y-coordinates of the
+ * vertex.
+ *
+ * The third parameter, `z`, is optional. It sets the z-coordinate of the
+ * vertex in WebGL mode. By default, `z` is 0.
+ *
+ * The fourth and fifth parameters, `u` and `v`, are also optional. They set
+ * the u- and v-coordinates for the vertex’s texture when used with
+ * endShape(). By default, `u` and `v` are both 0.
+ *
+ * @method vertex
+ * @param {Number} x x-coordinate of the vertex.
+ * @param {Number} y y-coordinate of the vertex.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the shape.
+ * strokeWeight(3);
+ *
+ * // Start drawing the shape.
+ * // Only draw the vertices.
+ * beginShape(POINTS);
+ *
+ * // Add the vertices.
+ * vertex(30, 20);
+ * vertex(85, 20);
+ * vertex(85, 75);
+ * vertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('Four black dots that form a square are drawn on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(30, 20);
+ * vertex(85, 20);
+ * vertex(85, 75);
+ * vertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ *
+ * describe('A white square on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(-20, -30, 0);
+ * vertex(35, -30, 0);
+ * vertex(35, 25, 0);
+ * vertex(-20, 25, 0);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ *
+ * describe('A white square on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white square spins around slowly on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(-20, -30, 0);
+ * vertex(35, -30, 0);
+ * vertex(35, 25, 0);
+ * vertex(-20, 25, 0);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load an image to apply as a texture.
+ * img = await loadImage('assets/laDefense.jpg');
+ *
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A photograph of a ceiling rotates slowly against a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Style the shape.
+ * noStroke();
+ *
+ * // Apply the texture.
+ * texture(img);
+ * textureMode(NORMAL);
+ *
+ * // Start drawing the shape
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(-20, -30, 0, 0, 0);
+ * vertex(35, -30, 0, 1, 0);
+ * vertex(35, 25, 0, 1, 1);
+ * vertex(-20, 25, 0, 0, 1);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ * }
+ *
+ * @example
+ * let vid;
+ * function setup() {
+ * // Load a video and create a p5.MediaElement object.
+ * vid = createVideo('/assets/fingers.mov');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Hide the video.
+ * vid.hide();
+ *
+ * // Set the video to loop.
+ * vid.loop();
+ *
+ * describe('A rectangle with video as texture');
+ * }
+ *
+ * function draw() {
+ * background(0);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Set the texture mode.
+ * textureMode(NORMAL);
+ *
+ * // Apply the video as a texture.
+ * texture(vid);
+ *
+ * // Draw a custom shape using uv coordinates.
+ * beginShape();
+ * vertex(-40, -40, 0, 0);
+ * vertex(40, -40, 1, 0);
+ * vertex(40, 40, 1, 1);
+ * vertex(-40, 40, 0, 1);
+ * endShape();
+ * }
+ */
+ /**
+ * @method vertex
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} [u=0] u-coordinate of the vertex's texture.
+ * @param {Number} [v=0] v-coordinate of the vertex's texture.
+ */
+ /**
+ * @method vertex
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} z
+ * @param {Number} [u=0] u-coordinate of the vertex's texture.
+ * @param {Number} [v=0] v-coordinate of the vertex's texture.
+ */
+ fn.vertex = function(x, y) {
+ let z, u, v;
+
+ // default to (x, y) mode: all other arguments assumed to be 0.
+ z = u = v = 0;
+
+ if (arguments.length === 3) {
+ // (x, y, z) mode: (u, v) assumed to be 0.
+ z = arguments[2];
+ } else if (arguments.length === 4) {
+ // (x, y, u, v) mode: z assumed to be 0.
+ u = arguments[2];
+ v = arguments[3];
+ } else if (arguments.length === 5) {
+ // (x, y, z, u, v) mode
+ z = arguments[2];
+ u = arguments[3];
+ v = arguments[4];
+ }
+ this._renderer.vertex(x, y, z, u, v);
+ return;
+ };
+
+ /**
+ * Begins creating a hole within a flat shape.
+ *
+ * The `beginContour()` and endContour()
+ * functions allow for creating negative space within custom shapes that are
+ * flat. `beginContour()` begins adding vertices to a negative space and
+ * endContour() stops adding them.
+ * `beginContour()` and endContour() must be
+ * called between beginShape() and
+ * endShape().
+ *
+ * Transformations such as translate(),
+ * rotate(), and scale()
+ * don't work between `beginContour()` and
+ * endContour(). It's also not possible to use
+ * other shapes, such as ellipse() or
+ * rect(), between `beginContour()` and
+ * endContour().
+ *
+ * Note: The vertices that define a negative space must "wind" in the opposite
+ * direction from the outer shape. First, draw vertices for the outer shape
+ * clockwise order. Then, draw vertices for the negative space in
+ * counter-clockwise order.
+ *
+ * @method beginContour
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Exterior vertices, clockwise winding.
+ * vertex(10, 10);
+ * vertex(90, 10);
+ * vertex(90, 90);
+ * vertex(10, 90);
+ *
+ * // Interior vertices, counter-clockwise winding.
+ * beginContour();
+ * vertex(30, 30);
+ * vertex(30, 70);
+ * vertex(70, 70);
+ * vertex(70, 30);
+ * endContour(CLOSE);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ *
+ * describe('A white square with a square hole in its center drawn on a gray background.');
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white square with a square hole in its center drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Exterior vertices, clockwise winding.
+ * vertex(-40, -40);
+ * vertex(40, -40);
+ * vertex(40, 40);
+ * vertex(-40, 40);
+ *
+ * // Interior vertices, counter-clockwise winding.
+ * beginContour();
+ * vertex(-20, -20);
+ * vertex(-20, 20);
+ * vertex(20, 20);
+ * vertex(20, -20);
+ * endContour(CLOSE);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ * }
+ */
+ fn.beginContour = function(kind) {
+ this._renderer.beginContour(kind);
+ };
+
+ /**
+ * Stops creating a hole within a flat shape.
+ *
+ * The beginContour() and `endContour()`
+ * functions allow for creating negative space within custom shapes that are
+ * flat. beginContour() begins adding vertices
+ * to a negative space and `endContour()` stops adding them.
+ * beginContour() and `endContour()` must be
+ * called between beginShape() and
+ * endShape().
+ *
+ * By default,
+ * the controur has an `OPEN` end, and to close it,
+ * call `endContour(CLOSE)`. The CLOSE contour mode closes splines smoothly.
+ *
+ * Transformations such as translate(),
+ * rotate(), and scale()
+ * don't work between beginContour() and
+ * `endContour()`. It's also not possible to use other shapes, such as
+ * ellipse() or rect(),
+ * between beginContour() and `endContour()`.
+ *
+ * Note: The vertices that define a negative space must "wind" in the opposite
+ * direction from the outer shape. First, draw vertices for the outer shape
+ * clockwise order. Then, draw vertices for the negative space in
+ * counter-clockwise order.
+ *
+ * @method endContour
+ * @param {OPEN|CLOSE} [mode=OPEN] By default, the value is OPEN
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Exterior vertices, clockwise winding.
+ * vertex(10, 10);
+ * vertex(90, 10);
+ * vertex(90, 90);
+ * vertex(10, 90);
+ *
+ * // Interior vertices, counter-clockwise winding.
+ * beginContour();
+ * vertex(30, 30);
+ * vertex(30, 70);
+ * vertex(70, 70);
+ * vertex(70, 30);
+ * endContour(CLOSE);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ *
+ * describe('A white square with a square hole in its center drawn on a gray background.');
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white square with a square hole in its center drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Exterior vertices, clockwise winding.
+ * vertex(-40, -40);
+ * vertex(40, -40);
+ * vertex(40, 40);
+ * vertex(-40, 40);
+ *
+ * // Interior vertices, counter-clockwise winding.
+ * beginContour();
+ * vertex(-20, -20);
+ * vertex(-20, 20);
+ * vertex(20, 20);
+ * vertex(20, -20);
+ * endContour(CLOSE);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ * }
+ */
+ fn.endContour = function(mode = OPEN) {
+ this._renderer.endContour(mode);
+ };
+ }
+
+ if (typeof p5 !== 'undefined') {
+ customShapes(p5, p5.prototype);
+ }
+
+ class States {
+ #modified = {};
+
+ constructor(initialState) {
+ for (const key in initialState) {
+ this[key] = initialState[key];
+ }
+ }
+
+ setValue(key, value) {
+ if (!(key in this.#modified)) {
+ this.#modified[key] = this[key];
+ }
+ this[key] = value;
+ }
+
+ getDiff() {
+ const diff = this.#modified;
+ this.#modified = {};
+ return diff;
+ }
+
+ getModified() {
+ return this.#modified;
+ }
+
+ applyDiff(prevModified) {
+ for (const key in this.#modified) {
+ this[key] = this.#modified[key];
+ }
+ this.#modified = prevModified;
+ }
+ }
+
+ /**
+ * @module Rendering
+ * @submodule Rendering
+ * @for p5
+ */
+
+
+ class ClonableObject {
+ constructor(obj = {}) {
+ for (const key in obj) {
+ this[key] = obj[key];
+ }
+ }
+
+ clone() {
+ return new ClonableObject(this);
+ }
+ }
+ class Renderer {
+ static states = {
+ strokeColor: null,
+ strokeSet: false,
+ fillColor: null,
+ fillSet: false,
+ tint: null,
+
+ imageMode: CORNER,
+ rectMode: CORNER,
+ ellipseMode: CENTER,
+ strokeWeight: 1,
+
+ textFont: { family: 'sans-serif' },
+ textLeading: 15,
+ leadingSet: false,
+ textSize: 12,
+ textAlign: LEFT,
+ textBaseline: BASELINE,
+ bezierOrder: 3,
+ splineProperties: new ClonableObject({
+ ends: INCLUDE,
+ tightness: 0
+ }),
+ textWrap: WORD,
+
+ // added v2.0
+ fontStyle: NORMAL, // v1: textStyle
+ fontStretch: NORMAL,
+ fontWeight: NORMAL,
+ lineHeight: NORMAL,
+ fontVariant: NORMAL,
+ direction: 'inherit'
+ };
+
+ constructor(pInst, w, h, isMainCanvas) {
+ this._pInst = pInst;
+ this._isMainCanvas = isMainCanvas;
+ this.pixels = [];
+ this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1;
+
+ this.width = w;
+ this.height = h;
+
+ this._events = {};
+
+ if (isMainCanvas) {
+ this._isMainCanvas = true;
+ }
+
+ // Renderer state machine
+ this.states = new States(Renderer.states);
+
+ this.states.strokeColor = new Color([0, 0, 0]);
+ this.states.fillColor = new Color([1, 1, 1]);
+
+ this._pushPopStack = [];
+ // NOTE: can use the length of the push pop stack instead
+ this._pushPopDepth = 0;
+
+ this._clipping = false;
+ this._clipInvert = false;
+
+ this._currentShape = undefined; // Lazily generate current shape
+ }
+
+ get currentShape() {
+ if (!this._currentShape) {
+ this._currentShape = new Shape(this.getCommonVertexProperties());
+ }
+ return this._currentShape;
+ }
+
+ remove() {
+
+ }
+
+ pixelDensity(val){
+ let returnValue;
+ if (typeof val === 'number') {
+ if (val !== this._pixelDensity) {
+ this._pixelDensity = val;
+ }
+ returnValue = this;
+ this.resize(this.width, this.height);
+ } else {
+ returnValue = this._pixelDensity;
+ }
+ return returnValue;
+ }
+
+ // Makes a shallow copy of the current states
+ // and push it into the push pop stack
+ push() {
+ this._pushPopDepth++;
+ this._pushPopStack.push(this.states.getDiff());
+ }
+
+ // Pop the previous states out of the push pop stack and
+ // assign it back to the current state
+ pop() {
+ this._pushPopDepth--;
+ const diff = this._pushPopStack.pop() || {};
+ const modified = this.states.getModified();
+ this.states.applyDiff(diff);
+ this.updateShapeVertexProperties(modified);
+ this.updateShapeProperties(modified);
+ }
+
+ bezierOrder(order) {
+ if (order === undefined) {
+ return this.states.bezierOrder;
+ } else {
+ this.states.setValue('bezierOrder', order);
+ this.updateShapeProperties();
+ }
+ }
+
+ bezierVertex(x, y, z = 0, u = 0, v = 0) {
+ const position = new Vector(x, y, z);
+ const textureCoordinates = this.getSupportedIndividualVertexProperties()
+ .textureCoordinates
+ ? new Vector(u, v)
+ : undefined;
+ this.currentShape.bezierVertex(position, textureCoordinates);
+ }
+
+ splineProperty(key, value) {
+ if (value === undefined) {
+ return this.states.splineProperties[key];
+ } else {
+ this.states.setValue('splineProperties', this.states.splineProperties.clone());
+ this.states.splineProperties[key] = value;
+ }
+ this.updateShapeProperties();
+ }
+
+ splineProperties(values) {
+ if (values) {
+ for (const key in values) {
+ this.splineProperty(key, values[key]);
+ }
+ } else {
+ return { ...this.states.splineProperties };
+ }
+ }
+
+ splineVertex(x, y, z = 0, u = 0, v = 0) {
+ const position = new Vector(x, y, z);
+ const textureCoordinates = this.getSupportedIndividualVertexProperties()
+ .textureCoordinates
+ ? new Vector(u, v)
+ : undefined;
+ this.currentShape.splineVertex(position, textureCoordinates);
+ }
+
+ curveDetail(d) {
+ if (d === undefined) {
+ return this.states.curveDetail;
+ } else {
+ this.states.setValue('curveDetail', d);
+ }
+ }
+
+ beginShape(...args) {
+ this.currentShape.reset();
+ this.updateShapeVertexProperties();
+ this.currentShape.beginShape(...args);
+ }
+
+ endShape(...args) {
+ this.currentShape.endShape(...args);
+ this.drawShape(this.currentShape);
+ }
+
+ beginContour(shapeKind) {
+ this.currentShape.beginContour(shapeKind);
+ }
+
+ endContour(mode) {
+ this.currentShape.endContour(mode);
+ }
+
+ drawShape(shape, count) {
+ throw new Error('Unimplemented');
+ }
+
+ vertex(x, y, z = 0, u = 0, v = 0) {
+ const position = new Vector(x, y, z);
+ const textureCoordinates = this.getSupportedIndividualVertexProperties()
+ .textureCoordinates
+ ? new Vector(u, v)
+ : undefined;
+ this.currentShape.vertex(position, textureCoordinates);
+ }
+
+ bezier(x1, y1, x2, y2, x3, y3, x4, y4) {
+ const oldOrder = this._pInst.bezierOrder();
+ this._pInst.bezierOrder(oldOrder);
+ this._pInst.beginShape();
+ this._pInst.bezierVertex(x1, y1);
+ this._pInst.bezierVertex(x2, y2);
+ this._pInst.bezierVertex(x3, y3);
+ this._pInst.bezierVertex(x4, y4);
+ this._pInst.endShape();
+ return this;
+ }
+
+ spline(...args) {
+ if (args.length === 2 * 4) {
+ const [x1, y1, x2, y2, x3, y3, x4, y4] = args;
+ this._pInst.beginShape();
+ this._pInst.splineVertex(x1, y1);
+ this._pInst.splineVertex(x2, y2);
+ this._pInst.splineVertex(x3, y3);
+ this._pInst.splineVertex(x4, y4);
+ this._pInst.endShape();
+ } else if (args.length === 3 * 4) {
+ const [x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4] = args;
+ this._pInst.beginShape();
+ this._pInst.splineVertex(x1, y1, z1);
+ this._pInst.splineVertex(x2, y2, z2);
+ this._pInst.splineVertex(x3, y3, z3);
+ this._pInst.splineVertex(x4, y4, z4);
+ this._pInst.endShape();
+ }
+ return this;
+ }
+
+ beginClip(options = {}) {
+ if (this._clipping) {
+ throw new Error("It looks like you're trying to clip while already in the middle of clipping. Did you forget to endClip()?");
+ }
+ this._clipping = true;
+ this._clipInvert = options.invert;
+ }
+
+ endClip() {
+ if (!this._clipping) {
+ throw new Error("It looks like you've called endClip() without beginClip(). Did you forget to call beginClip() first?");
+ }
+ this._clipping = false;
+ }
+
+ /**
+ * Resize our canvas element.
+ */
+ resize(w, h) {
+ this.width = w;
+ this.height = h;
+ }
+
+ get(x, y, w, h) {
+ const pd = this._pixelDensity;
+ const canvas = this.canvas;
+
+ if (typeof x === 'undefined' && typeof y === 'undefined') {
+ // get()
+ x = y = 0;
+ w = this.width;
+ h = this.height;
+ } else {
+ x *= pd;
+ y *= pd;
+
+ if (typeof w === 'undefined' && typeof h === 'undefined') {
+ // get(x,y)
+ if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) {
+ return [0, 0, 0, 0];
+ }
+
+ return this._getPixel(x, y);
+ }
+ // get(x,y,w,h)
+ }
+
+ const region = new Image(w*pd, h*pd);
+ region.pixelDensity(pd);
+ region.canvas
+ .getContext('2d')
+ .drawImage(canvas, x, y, w * pd, h * pd, 0, 0, w*pd, h*pd);
+
+ return region;
+ }
+
+ scale(x, y){
+
+ }
+
+ fill(...args) {
+ this.states.setValue('fillSet', true);
+ this.states.setValue('fillColor', this._pInst.color(...args));
+ this.updateShapeVertexProperties();
+ }
+
+ noFill() {
+ this.states.setValue('fillColor', null);
+ }
+
+ strokeWeight(w) {
+ if (w === undefined) {
+ return this.states.strokeWeight;
+ } else {
+ this.states.setValue('strokeWeight', w);
+ }
+ }
+
+ stroke(...args) {
+ this.states.setValue('strokeSet', true);
+ this.states.setValue('strokeColor', this._pInst.color(...args));
+ this.updateShapeVertexProperties();
+ }
+
+ noStroke() {
+ this.states.setValue('strokeColor', null);
+ }
+
+ getCommonVertexProperties() {
+ return {};
+ }
+
+ getSupportedIndividualVertexProperties() {
+ return {
+ textureCoordinates: false
+ };
+ }
+
+ updateShapeProperties(modified) {
+ if (!modified || modified.bezierOrder || modified.splineProperties) {
+ const shape = this.currentShape;
+ shape.bezierOrder(this.states.bezierOrder);
+ shape.splineProperty('ends', this.states.splineProperties.ends);
+ shape.splineProperty('tightness', this.states.splineProperties.tightness);
+ }
+ }
+
+ updateShapeVertexProperties(modified) {
+ const props = this.getCommonVertexProperties();
+ if (!modified || Object.keys(modified).some(k => k in props)) {
+ const shape = this.currentShape;
+ for (const key in props) {
+ shape[key](props[key]);
+ }
+ }
+ }
+
+ _applyDefaults() {
+ return this;
+ }
+
+ finishDraw() {
+ // Default no-op implementation
+ // Override in specific renderers as needed
+ }
+
+ ///////////////////////////////
+ //// TEXT SUPPORT METHODS
+ //////////////////////////////
+
+ _middleAlignOffset = function() {
+ const { textFont, textSize } = this.states;
+ const font = textFont?.font;
+ const ctx = this.textDrawingContext();
+ const metrics = ctx.measureText('X');
+ let sCapHeight = (font?.data || {})['OS/2']?.sCapHeight;
+ if (sCapHeight) {
+ const unitsPerEm = font.data.head.unitsPerEm;
+ sCapHeight *= textSize / unitsPerEm;
+ } else {
+ sCapHeight = metrics.fontBoundingBoxAscent;
+ }
+ return metrics.alphabeticBaseline + sCapHeight / 2;
+ };
+ }
+ function renderer(p5, fn){
+ /**
+ * Main graphics and rendering context, as well as the base API
+ * implementation for p5.js "core". To be used as the superclass for
+ * Renderer2D and Renderer3D classes, respectively.
+ *
+ * @class p5.Renderer
+ * @param {HTMLElement} elt DOM node that is wrapped
+ * @param {p5} [pInst] pointer to p5 instance
+ * @param {Boolean} [isMainCanvas] whether we're using it as main canvas
+ * @private
+ */
+ p5.Renderer = Renderer;
+ }
+
+ /**
+ * @module DOM
+ * @submodule DOM
+ */
+
+
+ /**
+ * @typedef {'video'} VIDEO
+ * @property {VIDEO} VIDEO
+ * @final
+ */
+ const VIDEO = 'video';
+
+ /**
+ * @typedef {'audio'} AUDIO
+ * @property {AUDIO} AUDIO
+ * @final
+ */
+ const AUDIO = 'audio';
+
+ class Cue {
+ constructor(callback, time, id, val) {
+ this.callback = callback;
+ this.time = time;
+ this.id = id;
+ this.val = val;
+ }
+ }
+
+ class MediaElement extends Element {
+ constructor(elt, pInst) {
+ super(elt, pInst);
+
+ const self = this;
+ this.elt.crossOrigin = 'anonymous';
+
+ this._prevTime = 0;
+ this._cueIDCounter = 0;
+ this._cues = [];
+ this.pixels = [];
+ this._pixelsState = this;
+ this._pixelDensity = 1;
+ this._modified = false;
+
+ // Media has an internal canvas that is used when drawing it to the main
+ // canvas. It will need to be updated each frame as the video itself plays.
+ // We don't want to update it every time we draw, however, in case the user
+ // has used load/updatePixels. To handle this, we record the frame drawn to
+ // the internal canvas so we only update it if the frame has changed.
+ this._frameOnCanvas = -1;
+
+ Object.defineProperty(self, 'src', {
+ get() {
+ const firstChildSrc = self.elt.children[0].src;
+ const srcVal = self.elt.src === window.location.href ? '' : self.elt.src;
+ const ret =
+ firstChildSrc === window.location.href ? srcVal : firstChildSrc;
+ return ret;
+ },
+ set(newValue) {
+ for (let i = 0; i < self.elt.children.length; i++) {
+ self.elt.removeChild(self.elt.children[i]);
+ }
+ const source = document.createElement('source');
+ source.src = newValue;
+ elt.appendChild(source);
+ self.elt.src = newValue;
+ self._modified = true;
+ }
+ });
+
+ // private _onended callback, set by the method: onended(callback)
+ self._onended = function () { };
+ self.elt.onended = function () {
+ self._onended(self);
+ };
+ }
+
+
+ /**
+ * Plays audio or video from a media element.
+ *
+ * @chainable
+ *
+ * @example
+ * let beat;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display a message.
+ * text('Click to play', 50, 50);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * beat = createAudio('assets/beat.mp3');
+ *
+ * describe('The text "Click to play" written in black on a gray background. A beat plays when the user clicks the square.');
+ * }
+ *
+ * // Play the beat when the user presses the mouse.
+ * function mousePressed() {
+ * beat.play();
+ * }
+ */
+ play() {
+ if (this.elt.currentTime === this.elt.duration) {
+ this.elt.currentTime = 0;
+ }
+ let promise;
+ if (this.elt.readyState > 1) {
+ promise = this.elt.play();
+ } else {
+ // in Chrome, playback cannot resume after being stopped and must reload
+ this.elt.load();
+ promise = this.elt.play();
+ }
+ if (promise && promise.catch) {
+ promise.catch(e => {
+ // if it's an autoplay failure error
+ if (e.name === 'NotAllowedError') {
+ if (typeof IS_MINIFIED === 'undefined') {
+ p5._friendlyAutoplayError(this.src);
+ } else {
+ console.error(e);
+ }
+ } else {
+ // any other kind of error
+ console.error('Media play method encountered an unexpected error', e);
+ }
+ });
+ }
+ return this;
+ }
+
+ /**
+ * Stops a media element and sets its current time to 0.
+ *
+ * Calling `media.play()` will restart playing audio/video from the beginning.
+ *
+ * @chainable
+ *
+ * @example
+ * let beat;
+ * let isStopped = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * beat = createAudio('assets/beat.mp3');
+ *
+ * describe('The text "Click to start" written in black on a gray background. The beat starts or stops when the user presses the mouse.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display different instructions based on playback.
+ * if (isStopped === true) {
+ * text('Click to start', 50, 50);
+ * } else {
+ * text('Click to stop', 50, 50);
+ * }
+ * }
+ *
+ * // Adjust playback when the user presses the mouse.
+ * function mousePressed() {
+ * if (isStopped === true) {
+ * // If the beat is stopped, play it.
+ * beat.play();
+ * isStopped = false;
+ * } else {
+ * // If the beat is playing, stop it.
+ * beat.stop();
+ * isStopped = true;
+ * }
+ * }
+ */
+ stop() {
+ this.elt.pause();
+ this.elt.currentTime = 0;
+ return this;
+ }
+
+ /**
+ * Pauses a media element.
+ *
+ * Calling `media.play()` will resume playing audio/video from the moment it paused.
+ *
+ * @chainable
+ *
+ * @example
+ * let beat;
+ * let isPaused = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * beat = createAudio('assets/beat.mp3');
+ *
+ * describe('The text "Click to play" written in black on a gray background. The beat plays or pauses when the user clicks the square.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display different instructions based on playback.
+ * if (isPaused === true) {
+ * text('Click to play', 50, 50);
+ * } else {
+ * text('Click to pause', 50, 50);
+ * }
+ * }
+ *
+ * // Adjust playback when the user presses the mouse.
+ * function mousePressed() {
+ * if (isPaused === true) {
+ * // If the beat is paused,
+ * // play it.
+ * beat.play();
+ * isPaused = false;
+ * } else {
+ * // If the beat is playing,
+ * // pause it.
+ * beat.pause();
+ * isPaused = true;
+ * }
+ * }
+ */
+ pause() {
+ this.elt.pause();
+ return this;
+ }
+
+ /**
+ * Plays the audio/video repeatedly in a loop.
+ *
+ * @chainable
+ *
+ * @example
+ * let beat;
+ * let isLooping = false;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * beat = createAudio('assets/beat.mp3');
+ *
+ * describe('The text "Click to loop" written in black on a gray background. A beat plays repeatedly in a loop when the user clicks. The beat stops when the user clicks again.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display different instructions based on playback.
+ * if (isLooping === true) {
+ * text('Click to stop', 50, 50);
+ * } else {
+ * text('Click to loop', 50, 50);
+ * }
+ * }
+ *
+ * // Adjust playback when the user presses the mouse.
+ * function mousePressed() {
+ * if (isLooping === true) {
+ * // If the beat is looping, stop it.
+ * beat.stop();
+ * isLooping = false;
+ * } else {
+ * // If the beat is stopped, loop it.
+ * beat.loop();
+ * isLooping = true;
+ * }
+ * }
+ */
+ loop() {
+ this.elt.setAttribute('loop', true);
+ this.play();
+ return this;
+ }
+ /**
+ * Stops the audio/video from playing in a loop.
+ *
+ * The media will stop when it finishes playing.
+ *
+ * @chainable
+ *
+ * @example
+ * let beat;
+ * let isPlaying = false;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * beat = createAudio('assets/beat.mp3');
+ *
+ * describe('The text "Click to play" written in black on a gray background. A beat plays when the user clicks. The beat stops when the user clicks again.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display different instructions based on playback.
+ * if (isPlaying === true) {
+ * text('Click to stop', 50, 50);
+ * } else {
+ * text('Click to play', 50, 50);
+ * }
+ * }
+ *
+ * // Adjust playback when the user presses the mouse.
+ * function mousePressed() {
+ * if (isPlaying === true) {
+ * // If the beat is playing, stop it.
+ * beat.stop();
+ * isPlaying = false;
+ * } else {
+ * // If the beat is stopped, play it.
+ * beat.play();
+ * isPlaying = true;
+ * }
+ * }
+ */
+ noLoop() {
+ this.elt.removeAttribute('loop');
+ return this;
+ }
+
+ /**
+ * Sets up logic to check that autoplay succeeded.
+ *
+ * @private
+ */
+ _setupAutoplayFailDetection() {
+ const timeout = setTimeout(() => {
+ if (typeof IS_MINIFIED === 'undefined') {
+ p5._friendlyAutoplayError(this.src);
+ } else {
+ console.error(e);
+ }
+ }, 500);
+ this.elt.addEventListener('play', () => clearTimeout(timeout), {
+ passive: true,
+ once: true
+ });
+ }
+
+ /**
+ * Sets the audio/video to play once it's loaded.
+ *
+ * The parameter, `shouldAutoplay`, is optional. Calling
+ * `media.autoplay()` without an argument causes the media to play
+ * automatically. If `true` is passed, as in `media.autoplay(true)`, the
+ * media will automatically play. If `false` is passed, as in
+ * `media.autoPlay(false)`, it won't play automatically.
+ *
+ * @param {Boolean} [shouldAutoplay] whether the element should autoplay.
+ * @chainable
+ *
+ * @example
+ * let video;
+ *
+ * function setup() {
+ * noCanvas();
+ *
+ * // Call handleVideo() once the video loads.
+ * video = createVideo('assets/fingers.mov', handleVideo);
+ *
+ * describe('A video of fingers walking on a treadmill.');
+ * }
+ *
+ * // Set the video's size and play it.
+ * function handleVideo() {
+ * video.size(100, 100);
+ * video.autoplay();
+ * }
+ *
+ * @example
+ * function setup() {
+ * noCanvas();
+ *
+ * // Load a video, but don't play it automatically.
+ * let video = createVideo('assets/fingers.mov', handleVideo);
+ *
+ * // Play the video when the user clicks on it.
+ * video.mousePressed(handlePress);
+ *
+ * describe('An image of fingers on a treadmill. They start walking when the user double-clicks on them.');
+ * }
+ *
+ * // Set the video's size and playback mode.
+ * function handleVideo() {
+ * video.size(100, 100);
+ * video.autoplay(false);
+ * }
+ *
+ * // Play the video.
+ * function handleClick() {
+ * video.play();
+ * }
+ */
+ autoplay(val) {
+ const oldVal = this.elt.getAttribute('autoplay');
+ this.elt.setAttribute('autoplay', val);
+ // if we turned on autoplay
+ if (val && !oldVal) {
+ // bind method to this scope
+ const setupAutoplayFailDetection =
+ () => this._setupAutoplayFailDetection();
+ // if media is ready to play, schedule check now
+ if (this.elt.readyState === 4) {
+ setupAutoplayFailDetection();
+ } else {
+ // otherwise, schedule check whenever it is ready
+ this.elt.addEventListener('canplay', setupAutoplayFailDetection, {
+ passive: true,
+ once: true
+ });
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Sets the audio/video volume.
+ *
+ * Calling `media.volume()` without an argument returns the current volume
+ * as a number in the range 0 (off) to 1 (maximum).
+ *
+ * The parameter, `val`, is optional. It's a number that sets the volume
+ * from 0 (off) to 1 (maximum). For example, calling `media.volume(0.5)`
+ * sets the volume to half of its maximum.
+ *
+ * @return {Number} current volume.
+ *
+ * @example
+ * let dragon;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * dragon = createAudio('assets/lucky_dragons.mp3');
+ *
+ * // Show the default media controls.
+ * dragon.showControls();
+ *
+ * describe('The text "Volume: V" on a gray square with media controls beneath it. The number "V" oscillates between 0 and 1 as the music plays.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Produce a number between 0 and 1.
+ * let n = 0.5 * sin(frameCount * 0.01) + 0.5;
+ *
+ * // Use n to set the volume.
+ * dragon.volume(n);
+ *
+ * // Get the current volume and display it.
+ * let v = dragon.volume();
+ *
+ * // Round v to 1 decimal place for display.
+ * v = round(v, 1);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display the volume.
+ * text(`Volume: ${v}`, 50, 50);
+ * }
+ */
+ /**
+ * @param {Number} val volume between 0.0 and 1.0.
+ * @chainable
+ */
+ volume(val) {
+ if (typeof val === 'undefined') {
+ return this.elt.volume;
+ } else {
+ this.elt.volume = val;
+ }
+ }
+
+ /**
+ * Sets the audio/video playback speed.
+ *
+ * The parameter, `val`, is optional. It's a number that sets the playback
+ * speed. 1 plays the media at normal speed, 0.5 plays it at half speed, 2
+ * plays it at double speed, and so on. -1 plays the media at normal speed
+ * in reverse.
+ *
+ * Calling `media.speed()` returns the current speed as a number.
+ *
+ * Note: Not all browsers support backward playback. Even if they do,
+ * playback might not be smooth.
+ *
+ * @return {Number} current playback speed.
+ *
+ * @example
+ * let dragon;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * dragon = createAudio('assets/lucky_dragons.mp3');
+ *
+ * // Show the default media controls.
+ * dragon.showControls();
+ *
+ * describe('The text "Speed: S" on a gray square with media controls beneath it. The number "S" oscillates between 0 and 1 as the music plays.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Produce a number between 0 and 2.
+ * let n = sin(frameCount * 0.01) + 1;
+ *
+ * // Use n to set the playback speed.
+ * dragon.speed(n);
+ *
+ * // Get the current speed and display it.
+ * let s = dragon.speed();
+ *
+ * // Round s to 1 decimal place for display.
+ * s = round(s, 1);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display the speed.
+ * text(`Speed: ${s}`, 50, 50);
+ * }
+ */
+ /**
+ * @param {Number} speed speed multiplier for playback.
+ * @chainable
+ */
+ speed(val) {
+ if (typeof val === 'undefined') {
+ return this.presetPlaybackRate || this.elt.playbackRate;
+ } else {
+ if (this.loadedmetadata) {
+ this.elt.playbackRate = val;
+ } else {
+ this.presetPlaybackRate = val;
+ }
+ }
+ }
+
+ /**
+ * Sets the media element's playback time.
+ *
+ * The parameter, `time`, is optional. It's a number that specifies the
+ * time, in seconds, to jump to when playback begins.
+ *
+ * Calling `media.time()` without an argument returns the number of seconds
+ * the audio/video has played.
+ *
+ * Note: Time resets to 0 when looping media restarts.
+ *
+ * @param {Number} [time] time to jump to (in seconds).
+ * @return {Number} current time (in seconds).
+ *
+ * @example
+ * let dragon;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * dragon = createAudio('assets/lucky_dragons.mp3');
+ *
+ * // Show the default media controls.
+ * dragon.showControls();
+ *
+ * describe('The text "S seconds" on a gray square with media controls beneath it. The number "S" increases as the song plays.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Get the current playback time.
+ * let s = dragon.time();
+ *
+ * // Round s to 1 decimal place for display.
+ * s = round(s, 1);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display the playback time.
+ * text(`${s} seconds`, 50, 50);
+ * }
+ *
+ * @example
+ * let dragon;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * dragon = createAudio('assets/lucky_dragons.mp3');
+ *
+ * // Show the default media controls.
+ * dragon.showControls();
+ *
+ * // Jump to 2 seconds to start.
+ * dragon.time(2);
+ *
+ * describe('The text "S seconds" on a gray square with media controls beneath it. The number "S" increases as the song plays.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Get the current playback time.
+ * let s = dragon.time();
+ *
+ * // Round s to 1 decimal place for display.
+ * s = round(s, 1);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display the playback time.
+ * text(`${s} seconds`, 50, 50);
+ * }
+ */
+ time(val) {
+ if (typeof val !== 'undefined') {
+ this.elt.currentTime = val;
+ }
+ return this.elt.currentTime;
+ }
+
+ /**
+ * Returns the audio/video's duration in seconds.
+ *
+ * @return {Number} duration (in seconds).
+ *
+ * @example
+ * let dragon;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * dragon = createAudio('assets/lucky_dragons.mp3');
+ *
+ * // Show the default media controls.
+ * dragon.showControls();
+ *
+ * describe('The text "S seconds left" on a gray square with media controls beneath it. The number "S" decreases as the song plays.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Calculate the time remaining.
+ * let s = dragon.duration() - dragon.time();
+ *
+ * // Round s to 1 decimal place for display.
+ * s = round(s, 1);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display the time remaining.
+ * text(`${s} seconds left`, 50, 50);
+ * }
+ */
+ duration() {
+ return this.elt.duration;
+ }
+ _ensureCanvas() {
+ if (!this.canvas) {
+ this.canvas = document.createElement('canvas');
+ this.drawingContext = this.canvas.getContext('2d');
+ this.setModified(true);
+ }
+
+ // Don't update the canvas again if we have already updated the canvas with
+ // the current frame
+ const needsRedraw = this._frameOnCanvas !== this._pInst.frameCount;
+ if (this.loadedmetadata && needsRedraw) {
+ // wait for metadata for w/h
+ if (this.canvas.width !== this.elt.width) {
+ this.canvas.width = this.elt.width;
+ this.canvas.height = this.elt.height;
+ this.width = this.canvas.width;
+ this.height = this.canvas.height;
+ }
+
+ this.drawingContext.clearRect(
+ 0, 0, this.canvas.width, this.canvas.height);
+
+ if (this.flipped === true) {
+ this.drawingContext.save();
+ this.drawingContext.scale(-1, 1);
+ this.drawingContext.translate(-this.canvas.width, 0);
+ }
+
+ this.drawingContext.drawImage(
+ this.elt,
+ 0,
+ 0,
+ this.canvas.width,
+ this.canvas.height
+ );
+
+ if (this.flipped === true) {
+ this.drawingContext.restore();
+ }
+
+ this.setModified(true);
+ this._frameOnCanvas = this._pInst.frameCount;
+ }
+ }
+ loadPixels(...args) {
+ this._ensureCanvas();
+ return p5.Renderer2D.prototype.loadPixels.apply(this, args);
+ }
+ updatePixels(x, y, w, h) {
+ if (this.loadedmetadata) {
+ // wait for metadata
+ this._ensureCanvas();
+ p5.Renderer2D.prototype.updatePixels.call(this, x, y, w, h);
+ }
+ this.setModified(true);
+ return this;
+ }
+ get(...args) {
+ this._ensureCanvas();
+ return p5.Renderer2D.prototype.get.apply(this, args);
+ }
+ _getPixel(...args) {
+ this.loadPixels();
+ return p5.Renderer2D.prototype._getPixel.apply(this, args);
+ }
+
+ set(x, y, imgOrCol) {
+ if (this.loadedmetadata) {
+ // wait for metadata
+ this._ensureCanvas();
+ p5.Renderer2D.prototype.set.call(this, x, y, imgOrCol);
+ this.setModified(true);
+ }
+ }
+ copy(...args) {
+ this._ensureCanvas();
+ p5.prototype.copy.apply(this, args);
+ }
+ mask(...args) {
+ this.loadPixels();
+ this.setModified(true);
+ p5.Image.prototype.mask.apply(this, args);
+ }
+ /**
+ * helper method for web GL mode to figure out if the element
+ * has been modified and might need to be re-uploaded to texture
+ * memory between frames.
+ * @private
+ * @return {boolean} a boolean indicating whether or not the
+ * image has been updated or modified since last texture upload.
+ */
+ isModified() {
+ return this._modified;
+ }
+ /**
+ * helper method for web GL mode to indicate that an element has been
+ * changed or unchanged since last upload. gl texture upload will
+ * set this value to false after uploading the texture; or might set
+ * it to true if metadata has become available but there is no actual
+ * texture data available yet..
+ * @param {Boolean} val sets whether or not the element has been
+ * modified.
+ * @private
+ */
+ setModified(value) {
+ this._modified = value;
+ }
+ /**
+ * Calls a function when the audio/video reaches the end of its playback.
+ *
+ * The element is passed as an argument to the callback function.
+ *
+ * Note: The function won't be called if the media is looping.
+ *
+ * @param {Function} callback function to call when playback ends.
+ * The `p5.MediaElement` is passed as
+ * the argument.
+ * @chainable
+ *
+ * @example
+ * let beat;
+ * let isPlaying = false;
+ * let isDone = false;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * beat = createAudio('assets/beat.mp3');
+ *
+ * // Call handleEnd() when the beat finishes.
+ * beat.onended(handleEnd);
+ *
+ * describe('The text "Click to play" written in black on a gray square. A beat plays when the user clicks. The text "Done!" appears when the beat finishes playing.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ *
+ * // Display different messages based on playback.
+ * if (isDone === true) {
+ * text('Done!', 50, 50);
+ * } else if (isPlaying === false) {
+ * text('Click to play', 50, 50);
+ * } else {
+ * text('Playing...', 50, 50);
+ * }
+ * }
+ *
+ * // Play the beat when the user presses the mouse.
+ * function mousePressed() {
+ * if (isPlaying === false) {
+ * isPlaying = true;
+ * beat.play();
+ * }
+ * }
+ *
+ * // Set isDone when playback ends.
+ * function handleEnd() {
+ * isDone = false;
+ * }
+ */
+ onended(callback) {
+ this._onended = callback;
+ return this;
+ }
+
+ /*** CONNECT TO WEB AUDIO API / p5.sound.js ***/
+
+ _getAudioContext() {
+ return undefined;
+ }
+ _getSoundOut() {
+ return undefined;
+ }
+
+ /**
+ * Sends the element's audio to an output.
+ *
+ * The parameter, `audioNode`, can be an `AudioNode` or an object from the
+ * `p5.sound` library.
+ *
+ * If no element is provided, as in `myElement.connect()`, the element
+ * connects to the main output. All connections are removed by the
+ * `.disconnect()` method.
+ *
+ * Note: This method is meant to be used with the p5.sound.js addon library.
+ *
+ * @param {AudioNode|Object} audioNode AudioNode from the Web Audio API,
+ * or an object from the p5.sound library
+ */
+ connect(obj) {
+ let audioContext, mainOutput;
+
+ // if p5.sound exists, same audio context
+ if (this._getAudioContext() && this._getSoundOut()) {
+ audioContext = this._getAudioContext();
+ mainOutput = this._getSoundOut().input;
+ } else {
+ try {
+ audioContext = obj.context;
+ mainOutput = audioContext.destination;
+ } catch (e) {
+ throw 'connect() is meant to be used with Web Audio API or p5.sound.js';
+ }
+ }
+
+ // create a Web Audio MediaElementAudioSourceNode if none already exists
+ if (!this.audioSourceNode) {
+ this.audioSourceNode = audioContext.createMediaElementSource(this.elt);
+
+ // connect to main output when this method is first called
+ this.audioSourceNode.connect(mainOutput);
+ }
+
+ // connect to object if provided
+ if (obj) {
+ if (obj.input) {
+ this.audioSourceNode.connect(obj.input);
+ } else {
+ this.audioSourceNode.connect(obj);
+ }
+ } else {
+ // otherwise connect to main output of p5.sound / AudioContext
+ this.audioSourceNode.connect(mainOutput);
+ }
+ }
+
+ /**
+ * Disconnect all Web Audio routing, including to the main output.
+ *
+ * This is useful if you want to re-route the output through audio effects,
+ * for example.
+ *
+ */
+ disconnect() {
+ if (this.audioSourceNode) {
+ this.audioSourceNode.disconnect();
+ } else {
+ throw 'nothing to disconnect';
+ }
+ }
+
+ /*** SHOW / HIDE CONTROLS ***/
+
+ /**
+ * Show the default
+ * HTMLMediaElement
+ * controls.
+ *
+ * Note: The controls vary between web browsers.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background('cornflowerblue');
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(50);
+ *
+ * // Display a dragon.
+ * text('🐉', 50, 50);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * let dragon = createAudio('assets/lucky_dragons.mp3');
+ *
+ * // Show the default media controls.
+ * dragon.showControls();
+ *
+ * describe('A dragon emoji, 🐉, drawn in the center of a blue square. A song plays in the background. Audio controls are displayed beneath the canvas.');
+ * }
+ */
+ showControls() {
+ // must set style for the element to show on the page
+ this.elt.style['text-align'] = 'inherit';
+ this.elt.controls = true;
+ }
+
+ /**
+ * Hide the default
+ * HTMLMediaElement
+ * controls.
+ *
+ * @example
+ * let dragon;
+ * let isHidden = false;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * dragon = createAudio('assets/lucky_dragons.mp3');
+ *
+ * // Show the default media controls.
+ * dragon.showControls();
+ *
+ * describe('The text "Double-click to hide controls" written in the middle of a gray square. A song plays in the background. Audio controls are displayed beneath the canvas. The controls appear/disappear when the user double-clicks the square.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ *
+ * // Display a different message when controls are hidden or shown.
+ * if (isHidden === true) {
+ * text('Double-click to show controls', 10, 20, 80, 80);
+ * } else {
+ * text('Double-click to hide controls', 10, 20, 80, 80);
+ * }
+ * }
+ *
+ * // Show/hide controls based on a double-click.
+ * function doubleClicked() {
+ * if (isHidden === true) {
+ * dragon.showControls();
+ * isHidden = false;
+ * } else {
+ * dragon.hideControls();
+ * isHidden = true;
+ * }
+ * }
+ */
+ hideControls() {
+ this.elt.controls = false;
+ }
+
+ /**
+ * Schedules a function to call when the audio/video reaches a specific time
+ * during its playback.
+ *
+ * The first parameter, `time`, is the time, in seconds, when the function
+ * should run. This value is passed to `callback` as its first argument.
+ *
+ * The second parameter, `callback`, is the function to call at the specified
+ * cue time.
+ *
+ * The third parameter, `value`, is optional and can be any type of value.
+ * `value` is passed to `callback`.
+ *
+ * Calling `media.addCue()` returns an ID as a string. This is useful for
+ * removing the cue later.
+ *
+ * @param {Number} time cue time to run the callback function.
+ * @param {Function} callback function to call at the cue time.
+ * @param {Object} [value] object to pass as the argument to
+ * `callback`.
+ * @return {Number} id ID of this cue,
+ * useful for `media.removeCue(id)`.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * let beat = createAudio('assets/beat.mp3');
+ *
+ * // Play the beat in a loop.
+ * beat.loop();
+ *
+ * // Schedule a few events.
+ * beat.addCue(0, changeBackground, 'red');
+ * beat.addCue(2, changeBackground, 'deeppink');
+ * beat.addCue(4, changeBackground, 'orchid');
+ * beat.addCue(6, changeBackground, 'lavender');
+ *
+ * describe('A red square with a beat playing in the background. Its color changes every 2 seconds while the audio plays.');
+ * }
+ *
+ * // Change the background color.
+ * function changeBackground(c) {
+ * background(c);
+ * }
+ */
+ addCue(time, callback, val) {
+ const id = this._cueIDCounter++;
+
+ const cue = new Cue(callback, time, id, val);
+ this._cues.push(cue);
+
+ if (!this.elt.ontimeupdate) {
+ this.elt.ontimeupdate = this._onTimeUpdate.bind(this);
+ }
+
+ return id;
+ }
+
+ /**
+ * Removes a callback based on its ID.
+ *
+ * @param {Number} id ID of the cue, created by `media.addCue()`.
+ *
+ * @example
+ * let lavenderID;
+ * let isRemoved = false;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * let beat = createAudio('assets/beat.mp3');
+ *
+ * // Play the beat in a loop.
+ * beat.loop();
+ *
+ * // Schedule a few events.
+ * beat.addCue(0, changeBackground, 'red');
+ * beat.addCue(2, changeBackground, 'deeppink');
+ * beat.addCue(4, changeBackground, 'orchid');
+ *
+ * // Record the ID of the "lavender" callback.
+ * lavenderID = beat.addCue(6, changeBackground, 'lavender');
+ *
+ * describe('The text "Double-click to remove lavender." written on a red square. The color changes every 2 seconds while the audio plays. The lavender option is removed when the user double-clicks the square.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Display different instructions based on the available callbacks.
+ * if (isRemoved === false) {
+ * text('Double-click to remove lavender.', 10, 10, 80, 80);
+ * } else {
+ * text('No more lavender.', 10, 10, 80, 80);
+ * }
+ * }
+ *
+ * // Change the background color.
+ * function changeBackground(c) {
+ * background(c);
+ * }
+ *
+ * // Remove the lavender color-change cue when the user double-clicks.
+ * function doubleClicked() {
+ * if (isRemoved === false) {
+ * beat.removeCue(lavenderID);
+ * isRemoved = true;
+ * }
+ * }
+ */
+ removeCue(id) {
+ for (let i = 0; i < this._cues.length; i++) {
+ if (this._cues[i].id === id) {
+ console.log(id);
+ this._cues.splice(i, 1);
+ }
+ }
+
+ if (this._cues.length === 0) {
+ this.elt.ontimeupdate = null;
+ }
+ }
+
+ /**
+ * Removes all functions scheduled with `media.addCue()`.
+ *
+ * @example
+ * let isChanging = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * let beat = createAudio('assets/beat.mp3');
+ *
+ * // Play the beat in a loop.
+ * beat.loop();
+ *
+ * // Schedule a few events.
+ * beat.addCue(0, changeBackground, 'red');
+ * beat.addCue(2, changeBackground, 'deeppink');
+ * beat.addCue(4, changeBackground, 'orchid');
+ * beat.addCue(6, changeBackground, 'lavender');
+ *
+ * describe('The text "Double-click to stop changing." written on a square. The color changes every 2 seconds while the audio plays. The color stops changing when the user double-clicks the square.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Display different instructions based on the available callbacks.
+ * if (isChanging === true) {
+ * text('Double-click to stop changing.', 10, 10, 80, 80);
+ * } else {
+ * text('No more changes.', 10, 10, 80, 80);
+ * }
+ * }
+ *
+ * // Change the background color.
+ * function changeBackground(c) {
+ * background(c);
+ * }
+ *
+ * // Remove cued functions and stop changing colors when the user
+ * // double-clicks.
+ * function doubleClicked() {
+ * if (isChanging === true) {
+ * beat.clearCues();
+ * isChanging = false;
+ * }
+ * }
+ */
+ clearCues() {
+ this._cues = [];
+ this.elt.ontimeupdate = null;
+ }
+
+ // private method that checks for cues to be fired if events
+ // have been scheduled using addCue(callback, time).
+ _onTimeUpdate() {
+ const playbackTime = this.time();
+
+ for (let i = 0; i < this._cues.length; i++) {
+ const callbackTime = this._cues[i].time;
+ const val = this._cues[i].val;
+
+ if (this._prevTime < callbackTime && callbackTime <= playbackTime) {
+ // pass the scheduled callbackTime as parameter to the callback
+ this._cues[i].callback(val);
+ }
+ }
+
+ this._prevTime = playbackTime;
+ }
+ }
+
+ // Cue inspired by JavaScript setTimeout, and the
+ // Tone.js Transport Timeline Event, MIT License Yotam Mann 2015 tonejs.org
+
+ function media(p5, fn){
+ /**
+ * Helpers for create methods.
+ */
+ function addElement(elt, pInst, media) {
+ const node = pInst._userNode ? pInst._userNode : document.body;
+ node.appendChild(elt);
+ const c = media
+ ? new MediaElement(elt, pInst)
+ : new Element(elt, pInst);
+ pInst._elements.push(c);
+ return c;
+ }
+
+ /* VIDEO STUFF */
+
+ // Helps perform similar tasks for media element methods.
+ function createMedia(pInst, type, src, callback) {
+ const elt = document.createElement(type);
+
+ // Create source elements from given sources
+ src = src || '';
+ if (typeof src === 'string') {
+ src = [src];
+ }
+ for (const mediaSource of src) {
+ const sourceEl = document.createElement('source');
+ sourceEl.setAttribute('src', mediaSource);
+ elt.appendChild(sourceEl);
+ }
+
+ const mediaEl = addElement(elt, pInst, true);
+ mediaEl.loadedmetadata = false;
+
+ // set width and height onload metadata
+ elt.addEventListener('loadedmetadata', () => {
+ mediaEl.width = elt.videoWidth;
+ mediaEl.height = elt.videoHeight;
+
+ // set elt width and height if not set
+ if (mediaEl.elt.width === 0) mediaEl.elt.width = elt.videoWidth;
+ if (mediaEl.elt.height === 0) mediaEl.elt.height = elt.videoHeight;
+ if (mediaEl.presetPlaybackRate) {
+ mediaEl.elt.playbackRate = mediaEl.presetPlaybackRate;
+ delete mediaEl.presetPlaybackRate;
+ }
+ mediaEl.loadedmetadata = true;
+ });
+
+ // If callback is provided, attach to element
+ if (typeof callback === 'function') {
+ const callbackHandler = () => {
+ callback(mediaEl);
+ elt.removeEventListener('canplaythrough', callbackHandler);
+ };
+ elt.addEventListener('canplaythrough', callbackHandler);
+ }
+
+ return mediaEl;
+ }
+
+ /**
+ * Creates a `<video>` element for simple audio/video playback.
+ *
+ * `createVideo()` returns a new
+ * p5.MediaElement object. Videos are shown by
+ * default. They can be hidden by calling `video.hide()` and drawn to the
+ * canvas using image().
+ *
+ * The first parameter, `src`, is the path the video. If a single string is
+ * passed, as in `'assets/topsecret.mp4'`, a single video is loaded. An array
+ * of strings can be used to load the same video in different formats. For
+ * example, `['assets/topsecret.mp4', 'assets/topsecret.ogv', 'assets/topsecret.webm']`.
+ * This is useful for ensuring that the video can play across different browsers with
+ * different capabilities. See
+ * MDN
+ * for more information about supported formats.
+ *
+ * The second parameter, `callback`, is optional. It's a function to call once
+ * the video is ready to play.
+ *
+ * @method createVideo
+ * @param {String|String[]} [src] path to a video file, or an array of paths for
+ * supporting different browsers.
+ * @param {Function} [callback] function to call once the video is ready to play.
+ * @return {p5.MediaElement} new p5.MediaElement object.
+ *
+ * @example
+ * function setup() {
+ * noCanvas();
+ *
+ * // Load a video and add it to the page.
+ * // Note: this may not work in some browsers.
+ * let video = createVideo('assets/small.mp4');
+ *
+ * // Show the default video controls.
+ * video.showControls();
+ *
+ * describe('A video of a toy robot with playback controls beneath it.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * noCanvas();
+ *
+ * // Load a video and add it to the page.
+ * // Provide an array options for different file formats.
+ * let video = createVideo(
+ * ['assets/small.mp4', 'assets/small.ogv', 'assets/small.webm']
+ * );
+ *
+ * // Show the default video controls.
+ * video.showControls();
+ *
+ * describe('A video of a toy robot with playback controls beneath it.');
+ * }
+ *
+ * @example
+ * let video;
+ *
+ * function setup() {
+ * noCanvas();
+ *
+ * // Load a video and add it to the page.
+ * // Provide an array options for different file formats.
+ * // Call mute() once the video loads.
+ * video = createVideo(
+ * ['assets/small.mp4', 'assets/small.ogv', 'assets/small.webm'],
+ * muteVideo
+ * );
+ *
+ * // Show the default video controls.
+ * video.showControls();
+ *
+ * describe('A video of a toy robot with playback controls beneath it.');
+ * }
+ *
+ * // Mute the video once it loads.
+ * function muteVideo() {
+ * video.volume(0);
+ * }
+ */
+ fn.createVideo = function (src, callback) {
+ // p5._validateParameters('createVideo', arguments);
+ return createMedia(this, VIDEO, src, callback);
+ };
+
+ /* AUDIO STUFF */
+
+ /**
+ * Creates a hidden `<audio>` element for simple audio playback.
+ *
+ * `createAudio()` returns a new
+ * p5.MediaElement object.
+ *
+ * The first parameter, `src`, is the path the audio. If a single string is
+ * passed, as in `'assets/audio.mp3'`, a single audio is loaded. An array
+ * of strings can be used to load the same audio in different formats. For
+ * example, `['assets/audio.mp3', 'assets/video.wav']`.
+ * This is useful for ensuring that the audio can play across different
+ * browsers with different capabilities. See
+ * MDN
+ * for more information about supported formats.
+ *
+ * The second parameter, `callback`, is optional. It's a function to call once
+ * the audio is ready to play.
+ *
+ * @method createAudio
+ * @param {String|String[]} [src] path to an audio file, or an array of paths
+ * for supporting different browsers.
+ * @param {Function} [callback] function to call once the audio is ready to play.
+ * @return {p5.MediaElement} new p5.MediaElement object.
+ *
+ * @example
+ * function setup() {
+ * noCanvas();
+ *
+ * // Load the audio.
+ * let beat = createAudio('assets/beat.mp3');
+ *
+ * // Show the default audio controls.
+ * beat.showControls();
+ *
+ * describe('An audio beat plays when the user double-clicks the square.');
+ * }
+ */
+ fn.createAudio = function (src, callback) {
+ // p5._validateParameters('createAudio', arguments);
+ return createMedia(this, AUDIO, src, callback);
+ };
+
+ /* CAMERA STUFF */
+
+ fn.VIDEO = VIDEO;
+ fn.AUDIO = AUDIO;
+
+ // from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
+ // Older browsers might not implement mediaDevices at all, so we set an empty object first
+ if (navigator.mediaDevices === undefined) {
+ navigator.mediaDevices = {};
+ }
+
+ // Some browsers partially implement mediaDevices. We can't just assign an object
+ // with getUserMedia as it would overwrite existing properties.
+ // Here, we will just add the getUserMedia property if it's missing.
+ if (navigator.mediaDevices.getUserMedia === undefined) {
+ navigator.mediaDevices.getUserMedia = function (constraints) {
+ // First get ahold of the legacy getUserMedia, if present
+ const getUserMedia =
+ navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+
+ // Some browsers just don't implement it - return a rejected promise with an error
+ // to keep a consistent interface
+ if (!getUserMedia) {
+ return Promise.reject(
+ new Error('getUserMedia is not implemented in this browser')
+ );
+ }
+
+ // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
+ return new Promise(function (resolve, reject) {
+ getUserMedia.call(navigator, constraints, resolve, reject);
+ });
+ };
+ }
+
+ /**
+ * Creates a `<video>` element that "captures" the audio/video stream from
+ * the webcam and microphone.
+ *
+ * `createCapture()` returns a new
+ * p5.MediaElement object. Videos are shown by
+ * default. They can be hidden by calling `capture.hide()` and drawn to the
+ * canvas using image().
+ *
+ * The first parameter, `type`, is optional. It sets the type of capture to
+ * use. By default, `createCapture()` captures both audio and video. If `VIDEO`
+ * is passed, as in `createCapture(VIDEO)`, only video will be captured.
+ * If `AUDIO` is passed, as in `createCapture(AUDIO)`, only audio will be
+ * captured. A constraints object can also be passed to customize the stream.
+ * See the
+ * W3C documentation for possible properties. Different browsers support different
+ * properties.
+ *
+ * The 'flipped' property is an optional property which can be set to `{flipped:true}`
+ * to mirror the video output.If it is true then it means that video will be mirrored
+ * or flipped and if nothing is mentioned then by default it will be `false`.
+ *
+ * The second parameter,`callback`, is optional. It's a function to call once
+ * the capture is ready for use. The callback function should have one
+ * parameter, `stream`, that's a
+ * MediaStream object.
+ *
+ * Note: `createCapture()` only works when running a sketch locally or using HTTPS. Learn more
+ * here
+ * and here.
+ *
+ * @method createCapture
+ * @param {(AUDIO|VIDEO|Object)} [type] type of capture, either AUDIO or VIDEO,
+ * or a constraints object. Both video and audio
+ * audio streams are captured by default.
+ * @param {Object} [flipped] flip the capturing video and mirror the output with `{flipped:true}`. By
+ * default it is false.
+ * @param {Function} [callback] function to call once the stream
+ * has loaded.
+ * @return {p5.MediaElement} new p5.MediaElement object.
+ *
+ * @example
+ * function setup() {
+ * noCanvas();
+ *
+ * // Create the video capture.
+ * createCapture(VIDEO);
+ *
+ * describe('A video stream from the webcam.');
+ * }
+ *
+ * @example
+ * let capture;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create the video capture and hide the element.
+ * capture = createCapture(VIDEO);
+ * capture.hide();
+ *
+ * describe('A video stream from the webcam with inverted colors.');
+ * }
+ *
+ * function draw() {
+ * // Draw the video capture within the canvas.
+ * image(capture, 0, 0, width, width * capture.height / capture.width);
+ *
+ * // Invert the colors in the stream.
+ * filter(INVERT);
+ * }
+ *
+ * @example
+ * let capture;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create the video capture with mirrored output.
+ * capture = createCapture(VIDEO,{ flipped:true });
+ * capture.size(100,100);
+ *
+ * describe('A video stream from the webcam with flipped or mirrored output.');
+ * }
+ *
+ *
+ * @example
+ * function setup() {
+ * createCanvas(480, 120);
+ *
+ * // Create a constraints object.
+ * let constraints = {
+ * video: {
+ * mandatory: {
+ * minWidth: 1280,
+ * minHeight: 720
+ * },
+ * optional: [{ maxFrameRate: 10 }]
+ * },
+ * audio: false
+ * };
+ *
+ * // Create the video capture.
+ * createCapture(constraints);
+ *
+ * describe('A video stream from the webcam.');
+ * }
+ */
+ fn.createCapture = function (...args) {
+ // p5._validateParameters('createCapture', args);
+
+ // return if getUserMedia is not supported by the browser
+ if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
+ throw new DOMException('getUserMedia not supported in this browser');
+ }
+
+ let useVideo = true;
+ let useAudio = true;
+ let constraints;
+ let callback;
+ let flipped = false;
+
+ for (const arg of args) {
+ if (arg === fn.VIDEO) useAudio = false;
+ else if (arg === fn.AUDIO) useVideo = false;
+ else if (typeof arg === 'object') {
+ if (arg.flipped !== undefined) {
+ flipped = arg.flipped;
+ delete arg.flipped;
+ }
+ constraints = Object.assign({}, constraints, arg);
+ }
+ else if (typeof arg === 'function') {
+ callback = arg;
+ }
+ }
+
+ const videoConstraints = { video: useVideo, audio: useAudio };
+ constraints = Object.assign({}, videoConstraints, constraints);
+ const domElement = document.createElement(VIDEO);
+ // required to work in iOS 11 & up:
+ domElement.setAttribute('playsinline', '');
+ navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
+ try {
+ if ('srcObject' in domElement) {
+ domElement.srcObject = stream;
+ } else {
+ domElement.src = window.URL.createObjectURL(stream);
+ }
+ }
+ catch (err) {
+ domElement.src = stream;
+ }
+ }).catch(e => {
+ if (e.name === 'NotFoundError')
+ p5._friendlyError('No webcam found on this device', 'createCapture');
+ if (e.name === 'NotAllowedError')
+ p5._friendlyError('Access to the camera was denied', 'createCapture');
+
+ console.error(e);
+ });
+
+ const videoEl = addElement(domElement, this, true);
+ videoEl.loadedmetadata = false;
+ // set width and height onload metadata
+ domElement.addEventListener('loadedmetadata', function () {
+ domElement.play();
+ if (domElement.width) {
+ videoEl.width = domElement.width;
+ videoEl.height = domElement.height;
+ if (flipped) {
+ videoEl.elt.style.transform = 'scaleX(-1)';
+ }
+ } else {
+ videoEl.width = videoEl.elt.width = domElement.videoWidth;
+ videoEl.height = videoEl.elt.height = domElement.videoHeight;
+ }
+ videoEl.loadedmetadata = true;
+
+ if (callback) callback(domElement.srcObject);
+ });
+ videoEl.flipped = flipped;
+ return videoEl;
+ };
+
+ // =============================================================================
+ // p5.MediaElement additions
+ // =============================================================================
+
+ /**
+ * A class to handle audio and video.
+ *
+ * `p5.MediaElement` extends p5.Element with
+ * methods to handle audio and video. `p5.MediaElement` objects are created by
+ * calling createVideo,
+ * createAudio, and
+ * createCapture.
+ *
+ * @class p5.MediaElement
+ * @param {String} elt DOM node that is wrapped
+ * @extends p5.Element
+ *
+ * @example
+ * let capture;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createCapture().
+ * capture = createCapture(VIDEO);
+ * capture.hide();
+ *
+ * describe('A webcam feed with inverted colors.');
+ * }
+ *
+ * function draw() {
+ * // Display the video stream and invert the colors.
+ * image(capture, 0, 0, width, width * capture.height / capture.width);
+ * filter(INVERT);
+ * }
+ */
+ p5.MediaElement = MediaElement;
+
+ // Patch MediaElement to give it access to fn, which p5.sound may attach things to
+ // if present in a sketch
+ MediaElement.prototype._getSoundOut = function() {
+ return p5.soundOut;
+ };
+ MediaElement.prototype._getAudioContext = function() {
+ if (typeof fn.getAudioContext === 'function') {
+ return fn.getAudioContext();
+ } else {
+ return undefined;
+ }
+ };
+
+ /**
+ * Path to the media element's source as a string.
+ *
+ * @for p5.MediaElement
+ * @property src
+ * @return {String} src
+ * @example
+ * let beat;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.MediaElement using createAudio().
+ * beat = createAudio('assets/beat.mp3');
+ *
+ * describe('The text "https://p5js.org/reference/assets/beat.mp3" written in black on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * textWrap(CHAR);
+ * text(beat.src, 10, 10, 80, 80);
+ * }
+ */
+ }
+
+ if(typeof p5 !== 'undefined'){
+ media(p5, p5.prototype);
+ }
+
+ /**
+ * @requires constants
+ */
+
+
+ /*
+ This function normalizes the first four arguments given to rect, ellipse and arc
+ according to the mode.
+ It returns a 'bounding box' object containing the coordinates of the upper left corner (x, y),
+ and width and height (w, h). The returned width and height are always positive.
+ */
+ function modeAdjust(a, b, c, d, mode) {
+ let bbox;
+
+ if (mode === CORNER) {
+
+ // CORNER mode already corresponds to a bounding box (top-left corner, width, height).
+ // For negative widhts or heights, the absolute value is used.
+ bbox = {
+ x: a,
+ y: b,
+ w: Math.abs(c),
+ h: Math.abs(d)
+ };
+
+ } else if (mode === CORNERS) {
+
+ // CORNERS mode uses two opposite corners, in any configuration.
+ // Make sure to get the top left corner by using the minimum of the x and y coordniates.
+ bbox = {
+ x: Math.min(a, c),
+ y: Math.min(b, d),
+ w: Math.abs(c - a),
+ h: Math.abs(d - b)
+ };
+
+ } else if (mode === RADIUS) {
+
+ // RADIUS mode uses the center point and half the width and height.
+ // c (half width) and d (half height) could be negative, so use the absolute value
+ // in calculating the top left corner (x, y).
+ c = Math.abs(c);
+ d = Math.abs(d);
+ bbox = {
+ x: a - c,
+ y: b - d,
+ w: 2 * c,
+ h: 2 * d
+ };
+
+ } else if (mode === CENTER) {
+
+ // CENTER mode uses the center point, width and height.
+ // c (width) and d (height) could be negative, so use the absolute value
+ // in calculating the top-left corner (x, y).
+ c = Math.abs(c);
+ d = Math.abs(d);
+ bbox = {
+ x: a - (c * 0.5),
+ y: b - (d * 0.5),
+ w: c,
+ h: d
+ };
+
+ }
+
+ return bbox;
+ }
+
+ var canvas = { modeAdjust };
+
+ /**
+ * @module Shape
+ * @submodule 2D Primitives
+ * @for p5
+ * @requires core
+ * @requires constants
+ */
+
+
+ function primitives(p5, fn){
+ /**
+ * This function does 3 things:
+ *
+ * 1. Bounds the desired start/stop angles for an arc (in radians) so that:
+ *
+ * 0 <= start < TWO_PI ; start <= stop < start + TWO_PI
+ *
+ * This means that the arc rendering functions don't have to be concerned
+ * with what happens if stop is smaller than start, or if the arc 'goes
+ * round more than once', etc.: they can just start at start and increase
+ * until stop and the correct arc will be drawn.
+ *
+ * 2. Optionally adjusts the angles within each quadrant to counter the naive
+ * scaling of the underlying ellipse up from the unit circle. Without
+ * this, the angles become arbitrary when width != height: 45 degrees
+ * might be drawn at 5 degrees on a 'wide' ellipse, or at 85 degrees on
+ * a 'tall' ellipse.
+ *
+ * 3. Flags up when start and stop correspond to the same place on the
+ * underlying ellipse. This is useful if you want to do something special
+ * there (like rendering a whole ellipse instead).
+ */
+ fn._normalizeArcAngles = (
+ start,
+ stop,
+ width,
+ height,
+ correctForScaling
+ ) => {
+ const epsilon = 0.00001; // Smallest visible angle on displays up to 4K.
+ let separation;
+
+ // The order of the steps is important here: each one builds upon the
+ // adjustments made in the steps that precede it.
+
+ // Constrain both start and stop to [0,TWO_PI).
+ start = start - TWO_PI * Math.floor(start / TWO_PI);
+ stop = stop - TWO_PI * Math.floor(stop / TWO_PI);
+
+ // Get the angular separation between the requested start and stop points.
+ //
+ // Technically this separation only matches what gets drawn if
+ // correctForScaling is enabled. We could add a more complicated calculation
+ // for when the scaling is uncorrected (in which case the drawn points could
+ // end up pushed together or pulled apart quite dramatically relative to what
+ // was requested), but it would make things more opaque for little practical
+ // benefit.
+ //
+ // (If you do disable correctForScaling and find that correspondToSamePoint
+ // is set too aggressively, the easiest thing to do is probably to just make
+ // epsilon smaller...)
+ separation = Math.min(
+ Math.abs(start - stop),
+ TWO_PI - Math.abs(start - stop)
+ );
+
+ // Optionally adjust the angles to counter linear scaling.
+ if (correctForScaling) {
+ if (start <= HALF_PI) {
+ start = Math.atan(width / height * Math.tan(start));
+ } else if (start > HALF_PI && start <= 3 * HALF_PI) {
+ start = Math.atan(width / height * Math.tan(start)) + PI;
+ } else {
+ start = Math.atan(width / height * Math.tan(start)) + TWO_PI;
+ }
+ if (stop <= HALF_PI) {
+ stop = Math.atan(width / height * Math.tan(stop));
+ } else if (stop > HALF_PI && stop <= 3 * HALF_PI) {
+ stop = Math.atan(width / height * Math.tan(stop)) + PI;
+ } else {
+ stop = Math.atan(width / height * Math.tan(stop)) + TWO_PI;
+ }
+ }
+
+ // Ensure that start <= stop < start + TWO_PI.
+ if (start > stop) {
+ stop += TWO_PI;
+ }
+
+ return {
+ start,
+ stop,
+ correspondToSamePoint: separation < epsilon
+ };
+ };
+
+ /**
+ * Draws an arc.
+ *
+ * An arc is a section of an ellipse defined by the `x`, `y`, `w`, and
+ * `h` parameters. `x` and `y` set the location of the arc's center. `w` and
+ * `h` set the arc's width and height. See
+ * ellipse() and
+ * ellipseMode() for more details.
+ *
+ * The fifth and sixth parameters, `start` and `stop`, set the angles
+ * between which to draw the arc. Arcs are always drawn clockwise from
+ * `start` to `stop`. Angles are always given in radians.
+ *
+ * The seventh parameter, `mode`, is optional. It determines the arc's fill
+ * style. The fill modes are a semi-circle (`OPEN`), a closed semi-circle
+ * (`CHORD`), or a closed pie segment (`PIE`).
+ *
+ * The eighth parameter, `detail`, is also optional. It determines how many
+ * vertices are used to draw the arc in WebGL mode. The default value is 25.
+ *
+ * @method arc
+ * @param {Number} x x-coordinate of the arc's ellipse.
+ * @param {Number} y y-coordinate of the arc's ellipse.
+ * @param {Number} w width of the arc's ellipse by default.
+ * @param {Number} h height of the arc's ellipse by default.
+ * @param {Number} start angle to start the arc, specified in radians.
+ * @param {Number} stop angle to stop the arc, specified in radians.
+ * @param {(CHORD|PIE|OPEN)} [mode] optional parameter to determine the way of drawing
+ * the arc. either CHORD, PIE, or OPEN.
+ * @param {Integer} [detail] optional parameter for WebGL mode only. This is to
+ * specify the number of vertices that makes up the
+ * perimeter of the arc. Default value is 25. Won't
+ * draw a stroke for a detail of more than 50.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * arc(50, 50, 80, 80, 0, PI + HALF_PI);
+ *
+ * describe('A white circle on a gray canvas. The top-right quarter of the circle is missing.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * arc(50, 50, 80, 40, 0, PI + HALF_PI);
+ *
+ * describe('A white ellipse on a gray canvas. The top-right quarter of the ellipse is missing.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Bottom-right.
+ * arc(50, 55, 50, 50, 0, HALF_PI);
+ *
+ * noFill();
+ *
+ * // Bottom-left.
+ * arc(50, 55, 60, 60, HALF_PI, PI);
+ *
+ * // Top-left.
+ * arc(50, 55, 70, 70, PI, PI + QUARTER_PI);
+ *
+ * // Top-right.
+ * arc(50, 55, 80, 80, PI + QUARTER_PI, TWO_PI);
+ *
+ * describe(
+ * 'A shattered outline of an circle with a quarter of a white circle at the bottom-right.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Default fill mode.
+ * arc(50, 50, 80, 80, 0, PI + QUARTER_PI);
+ *
+ * describe('A white circle with the top-right third missing. The bottom is outlined in black.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // OPEN fill mode.
+ * arc(50, 50, 80, 80, 0, PI + QUARTER_PI, OPEN);
+ *
+ * describe(
+ * 'A white circle missing a section from the top-right. The bottom is outlined in black.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // CHORD fill mode.
+ * arc(50, 50, 80, 80, 0, PI + QUARTER_PI, CHORD);
+ *
+ * describe('A white circle with a black outline missing a section from the top-right.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // PIE fill mode.
+ * arc(50, 50, 80, 80, 0, PI + QUARTER_PI, PIE);
+ *
+ * describe('A white circle with a black outline. The top-right third is missing.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // PIE fill mode.
+ * arc(0, 0, 80, 80, 0, PI + QUARTER_PI, PIE);
+ *
+ * describe('A white circle with a black outline. The top-right third is missing.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // PIE fill mode with 5 vertices.
+ * arc(0, 0, 80, 80, 0, PI + QUARTER_PI, PIE, 5);
+ *
+ * describe('A white circle with a black outline. The top-right third is missing.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * describe('A yellow circle on a black background. The circle opens and closes its mouth.');
+ * }
+ *
+ * function draw() {
+ * background(0);
+ *
+ * // Style the arc.
+ * noStroke();
+ * fill(255, 255, 0);
+ *
+ * // Update start and stop angles.
+ * let biteSize = PI / 16;
+ * let startAngle = biteSize * sin(frameCount * 0.1) + biteSize;
+ * let endAngle = TWO_PI - startAngle;
+ *
+ * // Draw the arc.
+ * arc(50, 50, 80, 80, startAngle, endAngle, PIE);
+ * }
+ */
+ fn.arc = function(x, y, w, h, start, stop, mode, detail) {
+ // this.validate("p5.arc", arguments);
+ // p5._validateParameters('arc', arguments);
+
+ // if the current stroke and fill settings wouldn't result in something
+ // visible, exit immediately
+ if (
+ !this._renderer.states.strokeColor &&
+ !this._renderer.states.fillColor
+ ) {
+ return this;
+ }
+
+ if (start === stop) {
+ return this;
+ }
+
+ start = this._toRadians(start);
+ stop = this._toRadians(stop);
+
+ const vals = canvas.modeAdjust(
+ x, y,
+ w, h,
+ this._renderer.states.ellipseMode
+ );
+ const angles = this._normalizeArcAngles(start, stop, vals.w, vals.h, true);
+
+ if (angles.correspondToSamePoint) {
+ // If the arc starts and ends at (near enough) the same place, we choose to
+ // draw an ellipse instead. This is preferable to faking an ellipse (by
+ // making stop ever-so-slightly less than start + TWO_PI) because the ends
+ // join up to each other rather than at a vertex at the centre (leaving
+ // an unwanted spike in the stroke/fill).
+ this._renderer.ellipse([vals.x, vals.y, vals.w, vals.h, detail]);
+ } else {
+ this._renderer.arc(
+ vals.x,
+ vals.y,
+ vals.w,
+ vals.h,
+ angles.start, // [0, TWO_PI)
+ angles.stop, // [start, start + TWO_PI)
+ mode,
+ detail
+ );
+
+ //accessible Outputs
+ if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
+ this._accsOutput('arc', [
+ vals.x,
+ vals.y,
+ vals.w,
+ vals.h,
+ angles.start,
+ angles.stop,
+ mode
+ ]);
+ }
+ }
+
+ return this;
+ };
+
+ /**
+ * Draws an ellipse (oval).
+ *
+ * An ellipse is a round shape defined by the `x`, `y`, `w`, and
+ * `h` parameters. `x` and `y` set the location of its center. `w` and
+ * `h` set its width and height. See
+ * ellipseMode() for other ways to set
+ * its position.
+ *
+ * If no height is set, the value of width is used for both the width and
+ * height. If a negative height or width is specified, the absolute value is
+ * taken.
+ *
+ * The fifth parameter, `detail`, is also optional. It determines how many
+ * vertices are used to draw the ellipse in WebGL mode. The default value is
+ * 25.
+ *
+ * @method ellipse
+ * @param {Number} x x-coordinate of the center of the ellipse.
+ * @param {Number} y y-coordinate of the center of the ellipse.
+ * @param {Number} w width of the ellipse.
+ * @param {Number} [h] height of the ellipse.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * ellipse(50, 50, 80, 80);
+ *
+ * describe('A white circle on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * ellipse(50, 50, 80);
+ *
+ * describe('A white circle on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * ellipse(50, 50, 80, 40);
+ *
+ * describe('A white ellipse on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * ellipse(0, 0, 80, 40);
+ *
+ * describe('A white ellipse on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Use 6 vertices.
+ * ellipse(0, 0, 80, 40, 6);
+ *
+ * describe('A white hexagon on a gray canvas.');
+ * }
+ */
+ /**
+ * @method ellipse
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} w
+ * @param {Number} h
+ * @param {Integer} [detail] optional parameter for WebGL mode only. This is to
+ * specify the number of vertices that makes up the
+ * perimeter of the ellipse. Default value is 25. Won't
+ * draw a stroke for a detail of more than 50.
+ */
+ fn.ellipse = function(x, y, w, h, detailX) {
+ // p5._validateParameters('ellipse', arguments);
+ return this._renderEllipse(...arguments);
+ };
+
+ /**
+ * Draws a circle.
+ *
+ * A circle is a round shape defined by the `x`, `y`, and `d` parameters.
+ * `x` and `y` set the location of its center. `d` sets its width and height (diameter).
+ * Every point on the circle's edge is the same distance, `0.5 * d`, from its center.
+ * `0.5 * d` (half the diameter) is the circle's radius.
+ * See ellipseMode() for other ways to set its position.
+ *
+ * @method circle
+ * @param {Number} x x-coordinate of the center of the circle.
+ * @param {Number} y y-coordinate of the center of the circle.
+ * @param {Number} d diameter of the circle.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * circle(50, 50, 25);
+ *
+ * describe('A white circle with black outline in the middle of a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * circle(0, 0, 25);
+ *
+ * describe('A white circle with black outline in the middle of a gray canvas.');
+ * }
+ */
+ fn.circle = function(...args) {
+ // p5._validateParameters('circle', args);
+ const argss = args.slice( 0, 2);
+ argss.push(args[2], args[2]);
+ return this._renderEllipse(...argss);
+ };
+
+ // internal method for drawing ellipses (without parameter validation)
+ fn._renderEllipse = function(x, y, w, h, detailX) {
+ // if the current stroke and fill settings wouldn't result in something
+ // visible, exit immediately
+ if (
+ !this._renderer.states.strokeColor &&
+ !this._renderer.states.fillColor
+ ) {
+ return this;
+ }
+
+ // Duplicate 3rd argument if only 3 given.
+ if (typeof h === 'undefined') {
+ h = w;
+ }
+
+ const vals = canvas.modeAdjust(
+ x, y,
+ w, h,
+ this._renderer.states.ellipseMode
+ );
+ this._renderer.ellipse([vals.x, vals.y, vals.w, vals.h, detailX]);
+
+ //accessible Outputs
+ if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
+ this._accsOutput('ellipse', [vals.x, vals.y, vals.w, vals.h]);
+ }
+
+ return this;
+ };
+
+ /**
+ * Draws a straight line between two points.
+ *
+ * A line's default width is one pixel. The version of `line()` with four
+ * parameters draws the line in 2D. To color a line, use the
+ * stroke() function. To change its width, use the
+ * strokeWeight() function. A line
+ * can't be filled, so the fill() function won't
+ * affect the line's color.
+ *
+ * The version of `line()` with six parameters allows the line to be drawn in
+ * 3D space. Doing so requires adding the `WEBGL` argument to
+ * createCanvas().
+ *
+ * @method line
+ * @param {Number} x1 the x-coordinate of the first point.
+ * @param {Number} y1 the y-coordinate of the first point.
+ * @param {Number} x2 the x-coordinate of the second point.
+ * @param {Number} y2 the y-coordinate of the second point.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * line(30, 20, 85, 75);
+ *
+ * describe(
+ * 'A black line on a gray canvas running from top-center to bottom-right.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the line.
+ * stroke('magenta');
+ * strokeWeight(5);
+ *
+ * line(30, 20, 85, 75);
+ *
+ * describe(
+ * 'A thick, magenta line on a gray canvas running from top-center to bottom-right.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Top.
+ * line(30, 20, 85, 20);
+ *
+ * // Right.
+ * stroke(126);
+ * line(85, 20, 85, 75);
+ *
+ * // Bottom.
+ * stroke(255);
+ * line(85, 75, 30, 75);
+ *
+ * describe(
+ * 'Three lines drawn in grayscale on a gray canvas. They form the top, right, and bottom sides of a square.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * line(-20, -30, 35, 25);
+ *
+ * describe(
+ * 'A black line on a gray canvas running from top-center to bottom-right.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A black line connecting two spheres. The scene spins slowly.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Draw a line.
+ * line(0, 0, 0, 30, 20, -10);
+ *
+ * // Draw the center sphere.
+ * sphere(10);
+ *
+ * // Translate to the second point.
+ * translate(30, 20, -10);
+ *
+ * // Draw the bottom-right sphere.
+ * sphere(10);
+ * }
+ */
+
+ /**
+ * @method line
+ * @param {Number} x1
+ * @param {Number} y1
+ * @param {Number} z1 the z-coordinate of the first point.
+ * @param {Number} x2
+ * @param {Number} y2
+ * @param {Number} z2 the z-coordinate of the second point.
+ * @chainable
+ */
+ fn.line = function(...args) {
+ // p5._validateParameters('line', args);
+
+ if (this._renderer.states.strokeColor) {
+ this._renderer.line(...args);
+ }
+
+ //accessible Outputs
+ if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
+ this._accsOutput('line', args);
+ }
+
+ return this;
+ };
+
+ /**
+ * Draws a single point in space.
+ *
+ * A point's default width is one pixel. To color a point, use the
+ * stroke() function. To change its width, use the
+ * strokeWeight() function. A point
+ * can't be filled, so the fill() function won't
+ * affect the point's color.
+ *
+ * The version of `point()` with two parameters allows the point's location to
+ * be set with its x- and y-coordinates, as in `point(10, 20)`.
+ *
+ * The version of `point()` with three parameters allows the point to be drawn
+ * in 3D space with x-, y-, and z-coordinates, as in `point(10, 20, 30)`.
+ * Doing so requires adding the `WEBGL` argument to
+ * createCanvas().
+ *
+ * The version of `point()` with one parameter allows the point's location to
+ * be set with a p5.Vector object.
+ *
+ * @method point
+ * @param {Number} x the x-coordinate.
+ * @param {Number} y the y-coordinate.
+ * @param {Number} [z] the z-coordinate (for WebGL mode).
+ * @chainable
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Making point to 5 pixels
+ * strokeWeight(5);
+ *
+ * // Top-left.
+ * point(30, 20);
+ *
+ * // Top-right.
+ * point(85, 20);
+ *
+ * // Bottom-right.
+ * point(85, 75);
+ *
+ * // Bottom-left.
+ * point(30, 75);
+ *
+ * describe(
+ * 'Four small, black points drawn on a gray canvas. The points form the corners of a square.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Making point to 5 pixels.
+ * strokeWeight(5);
+ *
+ * // Top-left.
+ * point(30, 20);
+ *
+ * // Top-right.
+ * point(70, 20);
+ *
+ * // Style the next points.
+ * stroke('purple');
+ * strokeWeight(10);
+ *
+ * // Bottom-right.
+ * point(70, 80);
+ *
+ * // Bottom-left.
+ * point(30, 80);
+ *
+ * describe(
+ * 'Four points drawn on a gray canvas. Two are black and two are purple. The points form the corners of a square.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Making point to 5 pixels.
+ * strokeWeight(5);
+ *
+ * // Top-left.
+ * let a = createVector(30, 20);
+ * point(a);
+ *
+ * // Top-right.
+ * let b = createVector(70, 20);
+ * point(b);
+ *
+ * // Bottom-right.
+ * let c = createVector(70, 80);
+ * point(c);
+ *
+ * // Bottom-left.
+ * let d = createVector(30, 80);
+ * point(d);
+ *
+ * describe(
+ * 'Four small, black points drawn on a gray canvas. The points form the corners of a square.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('Two purple points drawn on a gray canvas.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the points.
+ * stroke('purple');
+ * strokeWeight(10);
+ *
+ * // Top-left.
+ * point(-20, -30);
+ *
+ * // Bottom-right.
+ * point(20, 30);
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('Two purple points drawn on a gray canvas. The scene spins slowly.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Style the points.
+ * stroke('purple');
+ * strokeWeight(10);
+ *
+ * // Top-left.
+ * point(-20, -30, 0);
+ *
+ * // Bottom-right.
+ * point(20, 30, -50);
+ * }
+ */
+
+ /**
+ * @method point
+ * @param {p5.Vector} coordinateVector the coordinate vector.
+ * @chainable
+ */
+ fn.point = function(...args) {
+ // p5._validateParameters('point', args);
+
+ if (this._renderer.states.strokeColor) {
+ if (args.length === 1 && args[0] instanceof p5.Vector) {
+ this._renderer.point.call(
+ this._renderer,
+ args[0].x,
+ args[0].y,
+ args[0].z
+ );
+ } else {
+ this._renderer.point(...args);
+ //accessible Outputs
+ if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
+ this._accsOutput('point', args);
+ }
+ }
+ }
+
+ return this;
+ };
+
+ /**
+ * Draws a quadrilateral (four-sided shape).
+ *
+ * Quadrilaterals include rectangles, squares, rhombuses, and trapezoids. The
+ * first pair of parameters `(x1, y1)` sets the quad's first point. The next
+ * three pairs of parameters set the coordinates for its next three points
+ * `(x2, y2)`, `(x3, y3)`, and `(x4, y4)`. Points should be added in either
+ * clockwise or counter-clockwise order.
+ *
+ * The version of `quad()` with twelve parameters allows the quad to be drawn
+ * in 3D space. Doing so requires adding the `WEBGL` argument to
+ * createCanvas().
+ *
+ * The thirteenth and fourteenth parameters are optional. In WebGL mode, they
+ * set the number of segments used to draw the quadrilateral in the x- and
+ * y-directions. They're both 2 by default.
+ *
+ * @method quad
+ * @param {Number} x1 the x-coordinate of the first point.
+ * @param {Number} y1 the y-coordinate of the first point.
+ * @param {Number} x2 the x-coordinate of the second point.
+ * @param {Number} y2 the y-coordinate of the second point.
+ * @param {Number} x3 the x-coordinate of the third point.
+ * @param {Number} y3 the y-coordinate of the third point.
+ * @param {Number} x4 the x-coordinate of the fourth point.
+ * @param {Number} y4 the y-coordinate of the fourth point.
+ * @param {Integer} [detailX] number of segments in the x-direction.
+ * @param {Integer} [detailY] number of segments in the y-direction.
+ * @chainable
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * quad(20, 20, 80, 20, 80, 80, 20, 80);
+ *
+ * describe('A white square with a black outline drawn on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * quad(20, 30, 80, 30, 80, 70, 20, 70);
+ *
+ * describe('A white rectangle with a black outline drawn on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * quad(50, 62, 86, 50, 50, 38, 14, 50);
+ *
+ * describe('A white rhombus with a black outline drawn on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * quad(20, 50, 80, 30, 80, 70, 20, 70);
+ *
+ * describe('A white trapezoid with a black outline drawn on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * quad(-30, -30, 30, -30, 30, 30, -30, 30);
+ *
+ * describe('A white square with a black outline drawn on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A wavy white surface spins around on gray canvas.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Draw the quad.
+ * quad(-30, -30, 0, 30, -30, 0, 30, 30, 20, -30, 30, -20);
+ * }
+ */
+ /**
+ * @method quad
+ * @param {Number} x1
+ * @param {Number} y1
+ * @param {Number} z1 the z-coordinate of the first point.
+ * @param {Number} x2
+ * @param {Number} y2
+ * @param {Number} z2 the z-coordinate of the second point.
+ * @param {Number} x3
+ * @param {Number} y3
+ * @param {Number} z3 the z-coordinate of the third point.
+ * @param {Number} x4
+ * @param {Number} y4
+ * @param {Number} z4 the z-coordinate of the fourth point.
+ * @param {Integer} [detailX]
+ * @param {Integer} [detailY]
+ * @chainable
+ */
+ fn.quad = function(...args) {
+ // p5._validateParameters('quad', args);
+
+ if (this._renderer.states.strokeColor || this._renderer.states.fillColor) {
+ if (this._renderer.isP3D && args.length < 12) {
+ // if 3D and we weren't passed 12 args, assume Z is 0
+ this._renderer.quad.call(
+ this._renderer,
+ args[0], args[1], 0,
+ args[2], args[3], 0,
+ args[4], args[5], 0,
+ args[6], args[7], 0,
+ args[8], args[9]);
+ } else {
+ this._renderer.quad(...args);
+ //accessibile outputs
+ if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
+ this._accsOutput('quadrilateral', args);
+ }
+ }
+ }
+
+ return this;
+ };
+
+ /**
+ * Draws a rectangle.
+ *
+ * A rectangle is a four-sided shape defined by the `x`, `y`, `w`, and `h`
+ * parameters. `x` and `y` set the location of its top-left corner. `w` sets
+ * its width and `h` sets its height. Every angle in the rectangle measures
+ * 90˚. See rectMode() for other ways to define
+ * rectangles.
+ *
+ * The version of `rect()` with five parameters creates a rounded rectangle. The
+ * fifth parameter sets the radius for all four corners.
+ *
+ * The version of `rect()` with eight parameters also creates a rounded
+ * rectangle. Each of the last four parameters set the radius of a corner. The
+ * radii start with the top-left corner and move clockwise around the
+ * rectangle. If any of these parameters are omitted, they are set to the
+ * value of the last radius that was set.
+ *
+ * @method rect
+ * @param {Number} x x-coordinate of the rectangle.
+ * @param {Number} y y-coordinate of the rectangle.
+ * @param {Number} w width of the rectangle.
+ * @param {Number} [h] height of the rectangle.
+ * @param {Number} [tl] optional radius of top-left corner.
+ * @param {Number} [tr] optional radius of top-right corner.
+ * @param {Number} [br] optional radius of bottom-right corner.
+ * @param {Number} [bl] optional radius of bottom-left corner.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * rect(30, 20, 55, 55);
+ *
+ * describe('A white square with a black outline on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * rect(30, 20, 55, 40);
+ *
+ * describe('A white rectangle with a black outline on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Give all corners a radius of 20.
+ * rect(30, 20, 55, 50, 20);
+ *
+ * describe('A white rectangle with a black outline and round edges on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Give each corner a unique radius.
+ * rect(30, 20, 55, 50, 20, 15, 10, 5);
+ *
+ * describe('A white rectangle with a black outline and round edges of different radii.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * rect(-20, -30, 55, 55);
+ *
+ * describe('A white square with a black outline on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white square spins around on gray canvas.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Draw the rectangle.
+ * rect(-20, -30, 55, 55);
+ * }
+ */
+
+ /**
+ * @method rect
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} w
+ * @param {Number} h
+ * @param {Integer} [detailX] number of segments in the x-direction (for WebGL mode).
+ * @param {Integer} [detailY] number of segments in the y-direction (for WebGL mode).
+ * @chainable
+ */
+ fn.rect = function(...args) {
+ // p5._validateParameters('rect', args);
+ return this._renderRect(...args);
+ };
+
+ /**
+ * Draws a square.
+ *
+ * A square is a four-sided shape defined by the `x`, `y`, and `s`
+ * parameters. `x` and `y` set the location of its top-left corner. `s` sets
+ * its width and height. Every angle in the square measures 90˚ and all its
+ * sides are the same length. See rectMode() for
+ * other ways to define squares.
+ *
+ * The version of `square()` with four parameters creates a rounded square.
+ * The fourth parameter sets the radius for all four corners.
+ *
+ * The version of `square()` with seven parameters also creates a rounded
+ * square. Each of the last four parameters set the radius of a corner. The
+ * radii start with the top-left corner and move clockwise around the
+ * square. If any of these parameters are omitted, they are set to the
+ * value of the last radius that was set.
+ *
+ * @method square
+ * @param {Number} x x-coordinate of the square.
+ * @param {Number} y y-coordinate of the square.
+ * @param {Number} s side size of the square.
+ * @param {Number} [tl] optional radius of top-left corner.
+ * @param {Number} [tr] optional radius of top-right corner.
+ * @param {Number} [br] optional radius of bottom-right corner.
+ * @param {Number} [bl] optional radius of bottom-left corner.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * square(30, 20, 55);
+ *
+ * describe('A white square with a black outline in on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Give all corners a radius of 20.
+ * square(30, 20, 55, 20);
+ *
+ * describe(
+ * 'A white square with a black outline and round edges on a gray canvas.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Give each corner a unique radius.
+ * square(30, 20, 55, 20, 15, 10, 5);
+ *
+ * describe('A white square with a black outline and round edges of different radii.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * square(-20, -30, 55);
+ *
+ * describe('A white square with a black outline in on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white square spins around on gray canvas.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Draw the square.
+ * square(-20, -30, 55);
+ * }
+ */
+ fn.square = function(x, y, s, tl, tr, br, bl) {
+ // p5._validateParameters('square', arguments);
+ // duplicate width for height in case of square
+ return this._renderRect.call(this, x, y, s, s, tl, tr, br, bl);
+ };
+
+ // internal method to have renderer draw a rectangle
+ fn._renderRect = function() {
+ if (this._renderer.states.strokeColor || this._renderer.states.fillColor) {
+ // duplicate width for height in case only 3 arguments is provided
+ if (arguments.length === 3) {
+ arguments[3] = arguments[2];
+ }
+ const vals = canvas.modeAdjust(
+ arguments[0],
+ arguments[1],
+ arguments[2],
+ arguments[3],
+ this._renderer.states.rectMode
+ );
+
+ // For the default rectMode (CORNER), restore a possible negative width/height
+ // removed by modeAdjust(). This results in flipped/mirrored rendering,
+ // which is especially noticable when using WEGBL rendering and texture().
+ // Note that this behavior only applies to rect(), NOT to ellipse() and arc().
+ if (this._renderer.states.rectMode === CORNER) {
+ vals.w = arguments[2];
+ vals.h = arguments[3];
+ }
+
+ const args = [vals.x, vals.y, vals.w, vals.h];
+ // append the additional arguments (either cornder radii, or
+ // segment details) to the argument list
+ for (let i = 4; i < arguments.length; i++) {
+ args[i] = arguments[i];
+ }
+ this._renderer.rect(args);
+
+ //accessible outputs
+ if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
+ this._accsOutput('rectangle', [vals.x, vals.y, vals.w, vals.h]);
+ }
+ }
+
+ return this;
+ };
+
+ /**
+ * Draws a triangle.
+ *
+ * A triangle is a three-sided shape defined by three points. The
+ * first two parameters specify the triangle's first point `(x1, y1)`. The
+ * middle two parameters specify its second point `(x2, y2)`. And the last two
+ * parameters specify its third point `(x3, y3)`.
+ *
+ * @method triangle
+ * @param {Number} x1 x-coordinate of the first point.
+ * @param {Number} y1 y-coordinate of the first point.
+ * @param {Number} x2 x-coordinate of the second point.
+ * @param {Number} y2 y-coordinate of the second point.
+ * @param {Number} x3 x-coordinate of the third point.
+ * @param {Number} y3 y-coordinate of the third point.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * triangle(30, 75, 58, 20, 86, 75);
+ *
+ * describe('A white triangle with a black outline on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * triangle(-20, 25, 8, -30, 36, 25);
+ *
+ * describe('A white triangle with a black outline on a gray canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white triangle spins around on a gray canvas.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Draw the triangle.
+ * triangle(-20, 25, 8, -30, 36, 25);
+ * }
+ */
+ fn.triangle = function(...args) {
+ // p5._validateParameters('triangle', args);
+
+ if (this._renderer.states.strokeColor || this._renderer.states.fillColor) {
+ this._renderer.triangle(args);
+ }
+
+ //accessible outputs
+ if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
+ this._accsOutput('triangle', args);
+ }
+
+ return this;
+ };
+ }
+
+ if(typeof p5 !== 'undefined'){
+ primitives(p5, p5.prototype);
+ }
+
+ /**
+ * @module Shape
+ * @submodule Attributes
+ * @for p5
+ * @requires core
+ * @requires constants
+ */
+
+
+ function attributes(p5, fn){
+ /**
+ * Changes where ellipses, circles, and arcs are drawn.
+ *
+ * By default, the first two parameters of
+ * ellipse(), circle(),
+ * and arc()
+ * are the x- and y-coordinates of the shape's center. The next parameters set
+ * the shape's width and height. This is the same as calling
+ * `ellipseMode(CENTER)`.
+ *
+ * `ellipseMode(RADIUS)` also uses the first two parameters to set the x- and
+ * y-coordinates of the shape's center. The next parameters are half of the
+ * shapes's width and height. Calling `ellipse(0, 0, 10, 15)` draws a shape
+ * with a width of 20 and height of 30.
+ *
+ * `ellipseMode(CORNER)` uses the first two parameters as the upper-left
+ * corner of the shape. The next parameters are its width and height.
+ *
+ * `ellipseMode(CORNERS)` uses the first two parameters as the location of one
+ * corner of the ellipse's bounding box. The next parameters are the location
+ * of the opposite corner.
+ *
+ * The argument passed to `ellipseMode()` must be written in ALL CAPS because
+ * the constants `CENTER`, `RADIUS`, `CORNER`, and `CORNERS` are defined this
+ * way. JavaScript is a case-sensitive language.
+ *
+ * @method ellipseMode
+ * @param {(CENTER|RADIUS|CORNER|CORNERS)} mode either CENTER, RADIUS, CORNER, or CORNERS
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // White ellipse.
+ * ellipseMode(RADIUS);
+ * fill(255);
+ * ellipse(50, 50, 30, 30);
+ *
+ * // Gray ellipse.
+ * ellipseMode(CENTER);
+ * fill(100);
+ * ellipse(50, 50, 30, 30);
+ *
+ * describe('A white circle with a gray circle at its center. Both circles have black outlines.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // White ellipse.
+ * ellipseMode(CORNER);
+ * fill(255);
+ * ellipse(25, 25, 50, 50);
+ *
+ * // Gray ellipse.
+ * ellipseMode(CORNERS);
+ * fill(100);
+ * ellipse(25, 25, 50, 50);
+ *
+ * describe('A white circle with a gray circle at its top-left corner. Both circles have black outlines.');
+ * }
+ */
+ fn.ellipseMode = function(m) {
+ // p5._validateParameters('ellipseMode', arguments);
+ if (
+ m === CORNER ||
+ m === CORNERS ||
+ m === RADIUS ||
+ m === CENTER
+ ) {
+ this._renderer.states.setValue('ellipseMode', m);
+ }
+ return this;
+ };
+
+ /**
+ * Draws certain features with jagged (aliased) edges.
+ *
+ * smooth() is active by default. In 2D mode,
+ * `noSmooth()` is helpful for scaling up images without blurring. The
+ * functions don't affect shapes or fonts.
+ *
+ * In WebGL mode, `noSmooth()` causes all shapes to be drawn with jagged
+ * (aliased) edges. The functions don't affect images or fonts.
+ *
+ * @method noSmooth
+ * @chainable
+ *
+ * @example
+ * let heart;
+ *
+ * async function setup() {
+ * // Load a pixelated heart image from an image data string.
+ * heart = await loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAEZJREFUGFd9jcsNACAIQ9tB2MeR3YdBMBBq8CIXPi2vBICIiOwkOedatllqWO6Y8yOWoyuNf1GZwgmf+RRG2YXr+xVFmA8HZ9Mx/KGPMtcAAAAASUVORK5CYII=');
+ * createCanvas(100, 100);
+ *
+ * background(50);
+ *
+ * // Antialiased hearts.
+ * image(heart, 10, 10);
+ * image(heart, 20, 10, 16, 16);
+ * image(heart, 40, 10, 32, 32);
+ *
+ * // Aliased hearts.
+ * noSmooth();
+ * image(heart, 10, 60);
+ * image(heart, 20, 60, 16, 16);
+ * image(heart, 40, 60, 32, 32);
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * circle(0, 0, 80);
+ *
+ * describe('A white circle on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Disable smoothing.
+ * noSmooth();
+ *
+ * background(200);
+ *
+ * circle(0, 0, 80);
+ *
+ * describe('A pixelated white circle on a gray background.');
+ * }
+ */
+ fn.noSmooth = function() {
+ if (!this._renderer.isP3D) {
+ if ('imageSmoothingEnabled' in this.drawingContext) {
+ this.drawingContext.imageSmoothingEnabled = false;
+ }
+ } else {
+ this.setAttributes('antialias', false);
+ }
+ return this;
+ };
+
+ /**
+ * Changes where rectangles and squares are drawn.
+ *
+ * By default, the first two parameters of
+ * rect() and square(),
+ * are the x- and y-coordinates of the shape's upper left corner. The next parameters set
+ * the shape's width and height. This is the same as calling
+ * `rectMode(CORNER)`.
+ *
+ * `rectMode(CORNERS)` also uses the first two parameters as the location of
+ * one of the corners. The next parameters are the location of the opposite
+ * corner. This mode only works for rect().
+ *
+ * `rectMode(CENTER)` uses the first two parameters as the x- and
+ * y-coordinates of the shape's center. The next parameters are its width and
+ * height.
+ *
+ * `rectMode(RADIUS)` also uses the first two parameters as the x- and
+ * y-coordinates of the shape's center. The next parameters are
+ * half of the shape's width and height.
+ *
+ * The argument passed to `rectMode()` must be written in ALL CAPS because the
+ * constants `CENTER`, `RADIUS`, `CORNER`, and `CORNERS` are defined this way.
+ * JavaScript is a case-sensitive language.
+ *
+ * @method rectMode
+ * @param {(CENTER|RADIUS|CORNER|CORNERS)} mode either CORNER, CORNERS, CENTER, or RADIUS
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * rectMode(CORNER);
+ * fill(255);
+ * rect(25, 25, 50, 50);
+ *
+ * rectMode(CORNERS);
+ * fill(100);
+ * rect(25, 25, 50, 50);
+ *
+ * describe('A small gray square drawn at the top-left corner of a white square.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * rectMode(RADIUS);
+ * fill(255);
+ * rect(50, 50, 30, 30);
+ *
+ * rectMode(CENTER);
+ * fill(100);
+ * rect(50, 50, 30, 30);
+ *
+ * describe('A small gray square drawn at the center of a white square.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * rectMode(CORNER);
+ * fill(255);
+ * square(25, 25, 50);
+ *
+ * describe('A white square.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * rectMode(RADIUS);
+ * fill(255);
+ * square(50, 50, 30);
+ *
+ * rectMode(CENTER);
+ * fill(100);
+ * square(50, 50, 30);
+ *
+ * describe('A small gray square drawn at the center of a white square.');
+ * }
+ */
+ fn.rectMode = function(m) {
+ // p5._validateParameters('rectMode', arguments);
+ if (
+ m === CORNER ||
+ m === CORNERS ||
+ m === RADIUS ||
+ m === CENTER
+ ) {
+ this._renderer.states.setValue('rectMode', m);
+ }
+ return this; // return current rectMode ?
+ };
+
+ /**
+ * Draws certain features with smooth (antialiased) edges.
+ *
+ * `smooth()` is active by default. In 2D mode,
+ * noSmooth() is helpful for scaling up images
+ * without blurring. The functions don't affect shapes or fonts.
+ *
+ * In WebGL mode, noSmooth() causes all shapes to
+ * be drawn with jagged (aliased) edges. The functions don't affect images or
+ * fonts.
+ *
+ * @method smooth
+ * @chainable
+ *
+ * @example
+ * let heart;
+ *
+ * async function setup() {
+ * // Load a pixelated heart image from an image data string.
+ * heart = await loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAEZJREFUGFd9jcsNACAIQ9tB2MeR3YdBMBBq8CIXPi2vBICIiOwkOedatllqWO6Y8yOWoyuNf1GZwgmf+RRG2YXr+xVFmA8HZ9Mx/KGPMtcAAAAASUVORK5CYII=');
+ *
+ * createCanvas(100, 100);
+ *
+ * background(50);
+ *
+ * // Antialiased hearts.
+ * image(heart, 10, 10);
+ * image(heart, 20, 10, 16, 16);
+ * image(heart, 40, 10, 32, 32);
+ *
+ * // Aliased hearts.
+ * noSmooth();
+ * image(heart, 10, 60);
+ * image(heart, 20, 60, 16, 16);
+ * image(heart, 40, 60, 32, 32);
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * circle(0, 0, 80);
+ *
+ * describe('A white circle on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Disable smoothing.
+ * noSmooth();
+ *
+ * background(200);
+ *
+ * circle(0, 0, 80);
+ *
+ * describe('A pixelated white circle on a gray background.');
+ * }
+ */
+ fn.smooth = function() {
+ if (!this._renderer.isP3D) {
+ if ('imageSmoothingEnabled' in this.drawingContext) {
+ this.drawingContext.imageSmoothingEnabled = true;
+ }
+ } else {
+ this.setAttributes('antialias', true);
+ }
+ return this;
+ };
+
+ /**
+ * Sets the style for rendering the ends of lines.
+ *
+ * The caps for line endings are either rounded (`ROUND`), squared
+ * (`SQUARE`), or extended (`PROJECT`). The default cap is `ROUND`.
+ *
+ * The argument passed to `strokeCap()` must be written in ALL CAPS because
+ * the constants `ROUND`, `SQUARE`, and `PROJECT` are defined this way.
+ * JavaScript is a case-sensitive language.
+ *
+ * @method strokeCap
+ * @param {(ROUND|SQUARE|PROJECT)} cap either ROUND, SQUARE, or PROJECT
+ * @chainable
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * strokeWeight(12);
+ *
+ * // Top.
+ * strokeCap(ROUND);
+ * line(20, 30, 80, 30);
+ *
+ * // Middle.
+ * strokeCap(SQUARE);
+ * line(20, 50, 80, 50);
+ *
+ * // Bottom.
+ * strokeCap(PROJECT);
+ * line(20, 70, 80, 70);
+ *
+ * describe(
+ * 'Three horizontal lines. The top line has rounded ends, the middle line has squared ends, and the bottom line has longer, squared ends.'
+ * );
+ * }
+ */
+ fn.strokeCap = function(cap) {
+ // p5._validateParameters('strokeCap', arguments);
+ if (
+ cap === ROUND ||
+ cap === SQUARE ||
+ cap === PROJECT
+ ) {
+ this._renderer.strokeCap(cap);
+ }
+ return this;
+ };
+
+ /**
+ * Sets the style of the joints that connect line segments.
+ *
+ * Joints are either mitered (`MITER`), beveled (`BEVEL`), or rounded
+ * (`ROUND`). The default joint is `MITER` in 2D mode and `ROUND` in WebGL
+ * mode.
+ *
+ * The argument passed to `strokeJoin()` must be written in ALL CAPS because
+ * the constants `MITER`, `BEVEL`, and `ROUND` are defined this way.
+ * JavaScript is a case-sensitive language.
+ *
+ * @method strokeJoin
+ * @param {(MITER|BEVEL|ROUND)} join either MITER, BEVEL, or ROUND
+ * @chainable
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the line.
+ * noFill();
+ * strokeWeight(10);
+ * strokeJoin(MITER);
+ *
+ * // Draw the line.
+ * beginShape();
+ * vertex(35, 20);
+ * vertex(65, 50);
+ * vertex(35, 80);
+ * endShape();
+ *
+ * describe('A right-facing arrowhead shape with a pointed tip in center of canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the line.
+ * noFill();
+ * strokeWeight(10);
+ * strokeJoin(BEVEL);
+ *
+ * // Draw the line.
+ * beginShape();
+ * vertex(35, 20);
+ * vertex(65, 50);
+ * vertex(35, 80);
+ * endShape();
+ *
+ * describe('A right-facing arrowhead shape with a flat tip in center of canvas.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the line.
+ * noFill();
+ * strokeWeight(10);
+ * strokeJoin(ROUND);
+ *
+ * // Draw the line.
+ * beginShape();
+ * vertex(35, 20);
+ * vertex(65, 50);
+ * vertex(35, 80);
+ * endShape();
+ *
+ * describe('A right-facing arrowhead shape with a rounded tip in center of canvas.');
+ * }
+ */
+ fn.strokeJoin = function(join) {
+ // p5._validateParameters('strokeJoin', arguments);
+ if (
+ join === ROUND ||
+ join === BEVEL ||
+ join === MITER
+ ) {
+ this._renderer.strokeJoin(join);
+ }
+ return this;
+ };
+
+ /**
+ * Sets the width of the stroke used for points, lines, and the outlines of
+ * shapes.
+ *
+ * Note: `strokeWeight()` is affected by transformations, especially calls to
+ * scale().
+ *
+ * @method strokeWeight
+ * @param {Number} weight the weight of the stroke (in pixels).
+ * @chainable
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Top.
+ * line(20, 20, 80, 20);
+ *
+ * // Middle.
+ * strokeWeight(4);
+ * line(20, 40, 80, 40);
+ *
+ * // Bottom.
+ * strokeWeight(10);
+ * line(20, 70, 80, 70);
+ *
+ * describe('Three horizontal black lines. The top line is thin, the middle is medium, and the bottom is thick.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Top.
+ * line(20, 20, 80, 20);
+ *
+ * // Scale by a factor of 5.
+ * scale(5);
+ *
+ * // Bottom. Coordinates are adjusted for scaling.
+ * line(4, 8, 16, 8);
+ *
+ * describe('Two horizontal black lines. The top line is thin and the bottom is five times thicker than the top.');
+ * }
+ */
+ fn.strokeWeight = function(w) {
+ // p5._validateParameters('strokeWeight', arguments);
+ this._renderer.strokeWeight(w);
+ return this;
+ };
+ }
+
+ if(typeof p5 !== 'undefined'){
+ attributes(p5, p5.prototype);
+ }
+
+ /**
+ * @module Shape
+ * @submodule Curves
+ * @for p5
+ * @requires core
+ */
+
+ function curves(p5, fn){
+ /**
+ * Draws a Bézier curve.
+ *
+ * Bézier curves can form shapes and curves that slope gently. They're defined
+ * by two anchor points and two control points. Bézier curves provide more
+ * control than the spline curves created with the
+ * spline() function.
+ *
+ * The first two parameters, `x1` and `y1`, set the first anchor point. The
+ * first anchor point is where the curve starts.
+ *
+ * The next four parameters, `x2`, `y2`, `x3`, and `y3`, set the two control
+ * points. The control points "pull" the curve towards them.
+ *
+ * The seventh and eighth parameters, `x4` and `y4`, set the last anchor
+ * point. The last anchor point is where the curve ends.
+ *
+ * Bézier curves can also be drawn in 3D using WebGL mode. The 3D version of
+ * `bezier()` has twelve arguments because each point has x-, y-,
+ * and z-coordinates.
+ *
+ * @method bezier
+ * @param {Number} x1 x-coordinate of the first anchor point.
+ * @param {Number} y1 y-coordinate of the first anchor point.
+ * @param {Number} x2 x-coordinate of the first control point.
+ * @param {Number} y2 y-coordinate of the first control point.
+ * @param {Number} x3 x-coordinate of the second control point.
+ * @param {Number} y3 y-coordinate of the second control point.
+ * @param {Number} x4 x-coordinate of the second anchor point.
+ * @param {Number} y4 y-coordinate of the second anchor point.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Draw the anchor points in black.
+ * stroke(0);
+ * strokeWeight(5);
+ * point(85, 20);
+ * point(15, 80);
+ *
+ * // Draw the control points in red.
+ * stroke(255, 0, 0);
+ * point(10, 10);
+ * point(90, 90);
+ *
+ * // Draw a black bezier curve.
+ * noFill();
+ * stroke(0);
+ * strokeWeight(1);
+ * bezier(85, 20, 10, 10, 90, 90, 15, 80);
+ *
+ * // Draw red lines from the anchor points to the control points.
+ * stroke(255, 0, 0);
+ * line(85, 20, 10, 10);
+ * line(15, 80, 90, 90);
+ *
+ * describe(
+ * 'A gray square with three curves. A black s-curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.'
+ * );
+ * }
+ *
+ * @example
+ * // Click the mouse near the red dot in the top-left corner
+ * // and drag to change the curve's shape.
+ *
+ * let x2 = 10;
+ * let y2 = 10;
+ * let isChanging = false;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * describe(
+ * 'A gray square with three curves. A black s-curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Draw the anchor points in black.
+ * stroke(0);
+ * strokeWeight(5);
+ * point(85, 20);
+ * point(15, 80);
+ *
+ * // Draw the control points in red.
+ * stroke(255, 0, 0);
+ * point(x2, y2);
+ * point(90, 90);
+ *
+ * // Draw a black bezier curve.
+ * noFill();
+ * stroke(0);
+ * strokeWeight(1);
+ * bezier(85, 20, x2, y2, 90, 90, 15, 80);
+ *
+ * // Draw red lines from the anchor points to the control points.
+ * stroke(255, 0, 0);
+ * line(85, 20, x2, y2);
+ * line(15, 80, 90, 90);
+ * }
+ *
+ * // Start changing the first control point if the user clicks near it.
+ * function mousePressed() {
+ * if (dist(mouseX, mouseY, x2, y2) < 20) {
+ * isChanging = true;
+ * }
+ * }
+ *
+ * // Stop changing the first control point when the user releases the mouse.
+ * function mouseReleased() {
+ * isChanging = false;
+ * }
+ *
+ * // Update the first control point while the user drags the mouse.
+ * function mouseDragged() {
+ * if (isChanging === true) {
+ * x2 = mouseX;
+ * y2 = mouseY;
+ * }
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background('skyblue');
+ *
+ * // Draw the red balloon.
+ * fill('red');
+ * bezier(50, 60, 5, 15, 95, 15, 50, 60);
+ *
+ * // Draw the balloon string.
+ * line(50, 60, 50, 80);
+ *
+ * describe('A red balloon in a blue sky.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A red balloon in a blue sky. The balloon rotates slowly, revealing that it is flat.');
+ * }
+ *
+ * function draw() {
+ * background('skyblue');
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Draw the red balloon.
+ * fill('red');
+ * bezier(0, 0, 0, -45, -45, 0, 45, -45, 0, 0, 0, 0);
+ *
+ * // Draw the balloon string.
+ * line(0, 0, 0, 0, 20, 0);
+ * }
+ */
+ /**
+ * @method bezier
+ * @param {Number} x1
+ * @param {Number} y1
+ * @param {Number} z1 z-coordinate of the first anchor point.
+ * @param {Number} x2
+ * @param {Number} y2
+ * @param {Number} z2 z-coordinate of the first control point.
+ * @param {Number} x3
+ * @param {Number} y3
+ * @param {Number} z3 z-coordinate of the second control point.
+ * @param {Number} x4
+ * @param {Number} y4
+ * @param {Number} z4 z-coordinate of the second anchor point.
+ * @chainable
+ */
+ fn.bezier = function(...args) {
+ // p5._validateParameters('bezier', args);
+
+ // if the current stroke and fill settings wouldn't result in something
+ // visible, exit immediately
+ if (
+ !this._renderer.states.strokeColor &&
+ !this._renderer.states.fillColor
+ ) {
+ return this;
+ }
+
+ this._renderer.bezier(...args);
+
+ return this;
+ };
+
+ /**
+ * Calculates coordinates along a Bézier curve using interpolation.
+ *
+ * `bezierPoint()` calculates coordinates along a Bézier curve using the
+ * anchor and control points. It expects points in the same order as the
+ * bezier() function. `bezierPoint()` works one axis
+ * at a time. Passing the anchor and control points' x-coordinates will
+ * calculate the x-coordinate of a point on the curve. Passing the anchor and
+ * control points' y-coordinates will calculate the y-coordinate of a point on
+ * the curve.
+ *
+ * The first parameter, `a`, is the coordinate of the first anchor point.
+ *
+ * The second and third parameters, `b` and `c`, are the coordinates of the
+ * control points.
+ *
+ * The fourth parameter, `d`, is the coordinate of the last anchor point.
+ *
+ * The fifth parameter, `t`, is the amount to interpolate along the curve. 0
+ * is the first anchor point, 1 is the second anchor point, and 0.5 is halfway
+ * between them.
+ *
+ * @method bezierPoint
+ * @param {Number} a coordinate of first anchor point.
+ * @param {Number} b coordinate of first control point.
+ * @param {Number} c coordinate of second control point.
+ * @param {Number} d coordinate of second anchor point.
+ * @param {Number} t amount to interpolate between 0 and 1.
+ * @return {Number} coordinate of the point on the curve.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the coordinates for the curve's anchor and control points.
+ * let x1 = 85;
+ * let x2 = 10;
+ * let x3 = 90;
+ * let x4 = 15;
+ * let y1 = 20;
+ * let y2 = 10;
+ * let y3 = 90;
+ * let y4 = 80;
+ *
+ * // Style the curve.
+ * noFill();
+ *
+ * // Draw the curve.
+ * bezier(x1, y1, x2, y2, x3, y3, x4, y4);
+ *
+ * // Draw circles along the curve's path.
+ * fill(255);
+ *
+ * // Top-right.
+ * let x = bezierPoint(x1, x2, x3, x4, 0);
+ * let y = bezierPoint(y1, y2, y3, y4, 0);
+ * circle(x, y, 5);
+ *
+ * // Center.
+ * x = bezierPoint(x1, x2, x3, x4, 0.5);
+ * y = bezierPoint(y1, y2, y3, y4, 0.5);
+ * circle(x, y, 5);
+ *
+ * // Bottom-left.
+ * x = bezierPoint(x1, x2, x3, x4, 1);
+ * y = bezierPoint(y1, y2, y3, y4, 1);
+ * circle(x, y, 5);
+ *
+ * describe('A black s-curve on a gray square. The endpoints and center of the curve are marked with white circles.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * describe('A black s-curve on a gray square. A white circle moves back and forth along the curve.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Set the coordinates for the curve's anchor and control points.
+ * let x1 = 85;
+ * let x2 = 10;
+ * let x3 = 90;
+ * let x4 = 15;
+ * let y1 = 20;
+ * let y2 = 10;
+ * let y3 = 90;
+ * let y4 = 80;
+ *
+ * // Draw the curve.
+ * noFill();
+ * bezier(x1, y1, x2, y2, x3, y3, x4, y4);
+ *
+ * // Calculate the circle's coordinates.
+ * let t = 0.5 * sin(frameCount * 0.01) + 0.5;
+ * let x = bezierPoint(x1, x2, x3, x4, t);
+ * let y = bezierPoint(y1, y2, y3, y4, t);
+ *
+ * // Draw the circle.
+ * fill(255);
+ * circle(x, y, 5);
+ * }
+ */
+ fn.bezierPoint = function(a, b, c, d, t) {
+ // p5._validateParameters('bezierPoint', arguments);
+
+ const adjustedT = 1 - t;
+ return (
+ Math.pow(adjustedT, 3) * a +
+ 3 * Math.pow(adjustedT, 2) * t * b +
+ 3 * adjustedT * Math.pow(t, 2) * c +
+ Math.pow(t, 3) * d
+ );
+ };
+
+ /**
+ * Calculates coordinates along a line that's tangent to a Bézier curve.
+ *
+ * Tangent lines skim the surface of a curve. A tangent line's slope equals
+ * the curve's slope at the point where it intersects.
+ *
+ * `bezierTangent()` calculates coordinates along a tangent line using the
+ * Bézier curve's anchor and control points. It expects points in the same
+ * order as the bezier() function. `bezierTangent()`
+ * works one axis at a time. Passing the anchor and control points'
+ * x-coordinates will calculate the x-coordinate of a point on the tangent
+ * line. Passing the anchor and control points' y-coordinates will calculate
+ * the y-coordinate of a point on the tangent line.
+ *
+ * The first parameter, `a`, is the coordinate of the first anchor point.
+ *
+ * The second and third parameters, `b` and `c`, are the coordinates of the
+ * control points.
+ *
+ * The fourth parameter, `d`, is the coordinate of the last anchor point.
+ *
+ * The fifth parameter, `t`, is the amount to interpolate along the curve. 0
+ * is the first anchor point, 1 is the second anchor point, and 0.5 is halfway
+ * between them.
+ *
+ * @method bezierTangent
+ * @param {Number} a coordinate of first anchor point.
+ * @param {Number} b coordinate of first control point.
+ * @param {Number} c coordinate of second control point.
+ * @param {Number} d coordinate of second anchor point.
+ * @param {Number} t amount to interpolate between 0 and 1.
+ * @return {Number} coordinate of a point on the tangent line.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the coordinates for the curve's anchor and control points.
+ * let x1 = 85;
+ * let x2 = 10;
+ * let x3 = 90;
+ * let x4 = 15;
+ * let y1 = 20;
+ * let y2 = 10;
+ * let y3 = 90;
+ * let y4 = 80;
+ *
+ * // Style the curve.
+ * noFill();
+ *
+ * // Draw the curve.
+ * bezier(x1, y1, x2, y2, x3, y3, x4, y4);
+ *
+ * // Draw tangents along the curve's path.
+ * fill(255);
+ *
+ * // Top-right circle.
+ * stroke(0);
+ * let x = bezierPoint(x1, x2, x3, x4, 0);
+ * let y = bezierPoint(y1, y2, y3, y4, 0);
+ * circle(x, y, 5);
+ *
+ * // Top-right tangent line.
+ * // Scale the tangent point to draw a shorter line.
+ * stroke(255, 0, 0);
+ * let tx = 0.1 * bezierTangent(x1, x2, x3, x4, 0);
+ * let ty = 0.1 * bezierTangent(y1, y2, y3, y4, 0);
+ * line(x + tx, y + ty, x - tx, y - ty);
+ *
+ * // Center circle.
+ * stroke(0);
+ * x = bezierPoint(x1, x2, x3, x4, 0.5);
+ * y = bezierPoint(y1, y2, y3, y4, 0.5);
+ * circle(x, y, 5);
+ *
+ * // Center tangent line.
+ * // Scale the tangent point to draw a shorter line.
+ * stroke(255, 0, 0);
+ * tx = 0.1 * bezierTangent(x1, x2, x3, x4, 0.5);
+ * ty = 0.1 * bezierTangent(y1, y2, y3, y4, 0.5);
+ * line(x + tx, y + ty, x - tx, y - ty);
+ *
+ * // Bottom-left circle.
+ * stroke(0);
+ * x = bezierPoint(x1, x2, x3, x4, 1);
+ * y = bezierPoint(y1, y2, y3, y4, 1);
+ * circle(x, y, 5);
+ *
+ * // Bottom-left tangent.
+ * // Scale the tangent point to draw a shorter line.
+ * stroke(255, 0, 0);
+ * tx = 0.1 * bezierTangent(x1, x2, x3, x4, 1);
+ * ty = 0.1 * bezierTangent(y1, y2, y3, y4, 1);
+ * line(x + tx, y + ty, x - tx, y - ty);
+ *
+ * describe(
+ * 'A black s-curve on a gray square. The endpoints and center of the curve are marked with white circles. Red tangent lines extend from the white circles.'
+ * );
+ * }
+ */
+ fn.bezierTangent = function(a, b, c, d, t) {
+ // p5._validateParameters('bezierTangent', arguments);
+
+ const adjustedT = 1 - t;
+ return (
+ 3 * d * Math.pow(t, 2) -
+ 3 * c * Math.pow(t, 2) +
+ 6 * c * adjustedT * t -
+ 6 * b * adjustedT * t +
+ 3 * b * Math.pow(adjustedT, 2) -
+ 3 * a * Math.pow(adjustedT, 2)
+ );
+ };
+
+ /**
+ * Draws a curve using a Catmull-Rom spline.
+ *
+ * Spline curves can form shapes and curves that slope gently. They’re like
+ * cables that are attached to a set of points. By default (`ends: INCLUDE`),
+ * the curve passes through all four points you provide, in order
+ * `p0(x1,y1)` -> `p1(x2,y2)` -> `p2(x3,y3)` -> `p3(x4,y4)`. Think of them as
+ * points on a curve. If you switch to `ends: EXCLUDE`, p0 and p3 act
+ * like control points and only the middle span `p1->p2` is drawn.
+ *
+ * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of
+ * `spline()` has twelve arguments because each point has x-, y-, and
+ * z-coordinates.
+ *
+ * @method spline
+ * @param {Number} x1 x-coordinate of point p0.
+ * @param {Number} y1 y-coordinate of point p0.
+ * @param {Number} x2 x-coordinate of point p1.
+ * @param {Number} y2 y-coordinate of point p1.
+ * @param {Number} x3 x-coordinate of point p2.
+ * @param {Number} y3 y-coordinate of point p2.
+ * @param {Number} x4 x-coordinate of point p3.
+ * @param {Number} y4 y-coordinate of point p3.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(200, 200);
+ * background(240);
+ * noFill();
+ *
+ * stroke(0);
+ * strokeWeight(2);
+ * spline(40, 60, 100, 40, 120, 120, 60, 140);
+ *
+ * strokeWeight(5);
+ * point(40, 60);
+ * point(100, 40);
+ * point(120, 120);
+ * point(60, 140);
+ *
+ * describe('A black spline passes smoothly through four points');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(200, 200);
+ * background(245);
+ *
+ * // Ensure the curve includes both end spans p0->p1 and p2->p3
+ * splineProperty('ends', INCLUDE);
+ *
+ * // Control / anchor points
+ * const p0 = createVector(30, 160);
+ * const p1 = createVector(60, 40);
+ * const p2 = createVector(140, 40);
+ * const p3 = createVector(170, 160);
+ *
+ * // Draw the spline that passes through ALL four points (INCLUDE)
+ * noFill();
+ * stroke(0);
+ * strokeWeight(2);
+ * spline(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
+ *
+ * // Draw markers + labels
+ * fill(255);
+ * stroke(0);
+ * const r = 6;
+ * circle(p0.x, p0.y, r);
+ * circle(p1.x, p1.y, r);
+ * circle(p2.x, p2.y, r);
+ * circle(p3.x, p3.y, r);
+ *
+ * noStroke();
+ * fill(0);
+ * text('p0', p0.x - 14, p0.y + 14);
+ * text('p1', p1.x - 14, p1.y - 8);
+ * text('p2', p2.x + 4, p2.y - 8);
+ * text('p3', p3.x + 4, p3.y + 14);
+ *
+ * describe('A black Catmull-Rom spline passes through p0, p1, p2, p3 with endpoints included.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn.
+ * splineProperty('ends', EXCLUDE);
+ *
+ * // Draw a black spline curve.
+ * noFill();
+ * strokeWeight(1);
+ * stroke(0);
+ * spline(5, 26, 73, 24, 73, 61, 15, 65);
+ *
+ * // Draw red spline curves from the points.
+ * stroke(255, 0, 0);
+ * spline(5, 26, 5, 26, 73, 24, 73, 61);
+ * spline(73, 24, 73, 61, 15, 65, 15, 65);
+ *
+ * // Draw the points in black.
+ * strokeWeight(5);
+ * stroke(0);
+ * point(73, 24);
+ * point(73, 61);
+ *
+ * // Draw the points in red.
+ * stroke(255, 0, 0);
+ * point(5, 26);
+ * point(15, 65);
+ *
+ * describe(
+ * 'A gray square with a curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.'
+ * );
+ * }
+ *
+ * @example
+ * let x1 = 5;
+ * let y1 = 26;
+ * let isChanging = false;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * describe(
+ * 'A gray square with a curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn.
+ * splineProperty('ends', EXCLUDE);
+ *
+ * // Draw a black spline curve.
+ * noFill();
+ * strokeWeight(1);
+ * stroke(0);
+ * spline(x1, y1, 73, 24, 73, 61, 15, 65);
+ *
+ * // Draw red spline curves from the points.
+ * stroke(255, 0, 0);
+ * spline(x1, y1, x1, y1, 73, 24, 73, 61);
+ * spline(73, 24, 73, 61, 15, 65, 15, 65);
+ *
+ * // Draw the anchor points in black.
+ * strokeWeight(5);
+ * stroke(0);
+ * point(73, 24);
+ * point(73, 61);
+ *
+ * // Draw the points in red.
+ * stroke(255, 0, 0);
+ * point(x1, y1);
+ * point(15, 65);
+ * }
+ *
+ * // Start changing the first point if the user clicks near it.
+ * function mousePressed() {
+ * if (dist(mouseX, mouseY, x1, y1) < 20) {
+ * isChanging = true;
+ * }
+ * }
+ *
+ * // Stop changing the first point when the user releases the mouse.
+ * function mouseReleased() {
+ * isChanging = false;
+ * }
+ *
+ * // Update the first point while the user drags the mouse.
+ * function mouseDragged() {
+ * if (isChanging === true) {
+ * x1 = mouseX;
+ * y1 = mouseY;
+ * }
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background('skyblue');
+ *
+ * // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn.
+ * splineProperty('ends', EXCLUDE);
+ *
+ * // Draw the red balloon.
+ * fill('red');
+ * spline(-150, 275, 50, 60, 50, 60, 250, 275);
+ *
+ * // Draw the balloon string.
+ * line(50, 60, 50, 80);
+ *
+ * describe('A red balloon in a blue sky.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A red balloon in a blue sky.');
+ * }
+ *
+ * function draw() {
+ * background('skyblue');
+ *
+ * // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn.
+ * splineProperty('ends', EXCLUDE);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Draw the red balloon.
+ * fill('red');
+ * spline(-200, 225, 0, 0, 10, 0, 0, 10, 0, 200, 225, 0);
+ *
+ * // Draw the balloon string.
+ * line(0, 10, 0, 0, 30, 0);
+ * }
+ */
+ /**
+ * @method spline
+ * @param {Number} x1
+ * @param {Number} y1
+ * @param {Number} z1 z-coordinate of point p0.
+ * @param {Number} x2
+ * @param {Number} y2
+ * @param {Number} z2 z-coordinate of point p1.
+ * @param {Number} x3
+ * @param {Number} y3
+ * @param {Number} z3 z-coordinate of point p2.
+ * @param {Number} x4
+ * @param {Number} y4
+ * @param {Number} z4 z-coordinate of point p3.
+ * @chainable
+ */
+ fn.spline = function(...args) {
+ if (
+ !this._renderer.states.strokeColor &&
+ !this._renderer.states.fillColor
+ ) {
+ return this;
+ }
+ this._renderer.spline(...args);
+
+ return this;
+ };
+
+ /**
+ * Calculates coordinates along a spline curve using interpolation.
+ *
+ * `splinePoint()` calculates coordinates along a spline curve using four
+ * points p0, p1, p2, p3. It expects points in the same order as the
+ * spline() function. `splinePoint()` works one axis
+ * at a time. Passing the points' x-coordinates will
+ * calculate the x-coordinate of a point on the curve. Passing the
+ * points' y-coordinates will calculate the y-coordinate of a point on
+ * the curve.
+ *
+ * The first parameter, `a`, is the coordinate of point p0.
+ *
+ * The second and third parameters, `b` and `c`, are the coordinates of
+ * points p1 and p2.
+ *
+ * The fourth parameter, `d`, is the coordinate of point p3.
+ *
+ * The fifth parameter, `t`, is the amount to interpolate along the span
+ * from p1 to p2. `t = 0` is p1, `t = 1` is p2, and `t = 0.5` is halfway
+ * between them.
+ *
+ * @method splinePoint
+ * @param {Number} a coordinate of point p0.
+ * @param {Number} b coordinate of point p1.
+ * @param {Number} c coordinate of point p2.
+ * @param {Number} d coordinate of point p3.
+ * @param {Number} t amount to interpolate between 0 and 1.
+ * @return {Number} coordinate of a point on the curve.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ *
+ * // Set the coordinates for the curve's four points (p0, p1, p2, p3).
+ * let x1 = 5;
+ * let y1 = 26;
+ * let x2 = 73;
+ * let y2 = 24;
+ * let x3 = 73;
+ * let y3 = 61;
+ * let x4 = 15;
+ * let y4 = 65;
+ *
+ * // Draw the curve.
+ * noFill();
+ * spline(x1, y1, x2, y2, x3, y3, x4, y4);
+ *
+ * // Draw circles along the curve's path.
+ * fill(255);
+ *
+ * // Top.
+ * let x = splinePoint(x1, x2, x3, x4, 0);
+ * let y = splinePoint(y1, y2, y3, y4, 0);
+ * circle(x, y, 5);
+ *
+ * // Center.
+ * x = splinePoint(x1, x2, x3, x4, 0.5);
+ * y = splinePoint(y1, y2, y3, y4, 0.5);
+ * circle(x, y, 5);
+ *
+ * // Bottom.
+ * x = splinePoint(x1, x2, x3, x4, 1);
+ * y = splinePoint(y1, y2, y3, y4, 1);
+ * circle(x, y, 5);
+ *
+ * describe('A black curve on a gray square. The endpoints and center of the curve are marked with white circles.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * describe('A black curve on a gray square. A white circle moves back and forth along the curve.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Set the coordinates for the curve's four points (p0, p1, p2, p3).
+ * let x1 = 5;
+ * let y1 = 26;
+ * let x2 = 73;
+ * let y2 = 24;
+ * let x3 = 73;
+ * let y3 = 61;
+ * let x4 = 15;
+ * let y4 = 65;
+ *
+ * // Draw the curve.
+ * noFill();
+ * spline(x1, y1, x2, y2, x3, y3, x4, y4);
+ *
+ * // Calculate the circle's coordinates.
+ * let t = 0.5 * sin(frameCount * 0.01) + 0.5;
+ * let x = splinePoint(x1, x2, x3, x4, t);
+ * let y = splinePoint(y1, y2, y3, y4, t);
+ *
+ * // Draw the circle.
+ * fill(255);
+ * circle(x, y, 5);
+ * }
+ *
+ * @example
+ * let p0, p1, p2, p3;
+ *
+ * function setup() {
+ * createCanvas(200, 200);
+ * splineProperty('ends', INCLUDE); // make endpoints part of the curve
+ *
+ * // Four points forming a gentle arch
+ * p0 = createVector(30, 160);
+ * p1 = createVector(60, 50);
+ * p2 = createVector(140, 50);
+ * p3 = createVector(170, 160);
+ *
+ * describe('Black spline through p0–p3. A red dot marks the location at parameter t on p1->p2 using splinePoint.');
+ * }
+ *
+ * function draw() {
+ * background(245);
+ *
+ * // Draw the spline for context
+ * noFill();
+ * stroke(0);
+ * strokeWeight(2);
+ * spline(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
+ *
+ * // Map mouse X to t in [0, 1] (span p1->p2)
+ * let t = constrain(map(mouseX, 0, width, 0, 1), 0, 1);
+ *
+ * // Evaluate the curve point by axis (splinePoint works one axis at a time)
+ * let x = splinePoint(p0.x, p1.x, p2.x, p3.x, t);
+ * let y = splinePoint(p0.y, p1.y, p2.y, p3.y, t);
+ *
+ * // Marker at the evaluated position
+ * noStroke();
+ * fill('red');
+ * circle(x, y, 8);
+ *
+ * // Draw control/anchor points
+ * stroke(0);
+ * strokeWeight(1);
+ * fill(255);
+ * const r = 6;
+ * circle(p0.x, p0.y, r);
+ * circle(p1.x, p1.y, r);
+ * circle(p2.x, p2.y, r);
+ * circle(p3.x, p3.y, r);
+ *
+ * // Labels + UI hint
+ * noStroke();
+ * fill(20);
+ * textSize(10);
+ * text('p0', p0.x - 12, p0.y + 14);
+ * text('p1', p1.x - 12, p1.y - 8);
+ * text('p2', p2.x + 4, p2.y - 8);
+ * text('p3', p3.x + 4, p3.y + 14);
+ * text('t = ' + nf(t, 1, 2) + ' (p1→p2)', 8, 16);
+ * }
+ */
+ fn.splinePoint = function(a, b, c, d, t) {
+ const s = this._renderer.states.splineProperties.tightness,
+ t3 = t * t * t,
+ t2 = t * t,
+ f1 = (s - 1) / 2 * t3 + (1 - s) * t2 + (s - 1) / 2 * t,
+ f2 = (s + 3) / 2 * t3 + (-5 - s) / 2 * t2 + 1.0,
+ f3 = (-3 - s) / 2 * t3 + (s + 2) * t2 + (1 - s) / 2 * t,
+ f4 = (1 - s) / 2 * t3 + (s - 1) / 2 * t2;
+ return a * f1 + b * f2 + c * f3 + d * f4;
+ };
+
+ /**
+ * Calculates coordinates along a line that's tangent to a spline curve.
+ *
+ * Tangent lines skim the surface of a curve. A tangent line's slope equals
+ * the curve's slope at the point where it intersects.
+ *
+ * `splineTangent()` calculates coordinates along a tangent line using four
+ * points p0, p1, p2, p3. It expects points in the same order as the
+ * spline() function. `splineTangent()` works one
+ * axis at a time. Passing the points' x-coordinates returns the x-component of
+ * the tangent vector; passing the points' y-coordinates returns the y-component.
+ * The first parameter, `a`, is the coordinate of point p0.
+ *
+ * The second and third parameters, `b` and `c`, are the coordinates of
+ * points p1 and p2.
+ *
+ * The fourth parameter, `d`, is the coordinate of point p3.
+ *
+ * The fifth parameter, `t`, is the amount to interpolate along the span
+ * from p1 to p2. `t = 0` is p1, `t = 1` is p2, and `t = 0.5` is halfway
+ * between them.
+ *
+ * @method splineTangent
+ * @param {Number} a coordinate of point p0.
+ * @param {Number} b coordinate of point p1.
+ * @param {Number} c coordinate of point p2.
+ * @param {Number} d coordinate of point p3.
+ * @param {Number} t amount to interpolate between 0 and 1.
+ * @return {Number} coordinate of a point on the tangent line.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(120, 120);
+ * describe('A black spline on a gray canvas. A red dot moves along the curve on its own. A short line shows the tangent direction at the dot.');
+ * }
+ *
+ * function draw() {
+ * background(240);
+ *
+ * const x1 = 15, y1 = 40;
+ * const x2 = 90, y2 = 25;
+ * const x3 = 95, y3 = 95;
+ * const x4 = 30, y4 = 110;
+ *
+ * noFill();
+ * stroke(0);
+ * strokeWeight(2);
+ * spline(x1, y1, x2, y2, x3, y3, x4, y4);
+ *
+ * const t = 0.5 + 0.5 * sin(frameCount * 0.03);
+ *
+ * const px = splinePoint(x1, x2, x3, x4, t);
+ * const py = splinePoint(y1, y2, y3, y4, t);
+ *
+ * let tx = splineTangent(x1, x2, x3, x4, t);
+ * let ty = splineTangent(y1, y2, y3, y4, t);
+ *
+ * const m = Math.hypot(tx, ty) || 1;
+ * tx = (tx / m) * 16;
+ * ty = (ty / m) * 16;
+ *
+ * stroke(0);
+ * strokeWeight(2);
+ * line(px, py, px + tx, py + ty);
+ *
+ * noStroke();
+ * fill('red');
+ * circle(px, py, 7);
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the coordinates for the curve's four points (p0, p1, p2, p3).
+ * let x1 = 5;
+ * let y1 = 26;
+ * let x2 = 73;
+ * let y2 = 24;
+ * let x3 = 73;
+ * let y3 = 61;
+ * let x4 = 15;
+ * let y4 = 65;
+ *
+ * // Draw the curve.
+ * noFill();
+ * spline(x1, y1, x2, y2, x3, y3, x4, y4);
+ *
+ * // Draw tangents along the curve's path.
+ * fill(255);
+ *
+ * // Top circle.
+ * stroke(0);
+ * let x = splinePoint(x1, x2, x3, x4, 0);
+ * let y = splinePoint(y1, y2, y3, y4, 0);
+ * circle(x, y, 5);
+ *
+ * // Top tangent line.
+ * // Scale the tangent point to draw a shorter line.
+ * stroke(255, 0, 0);
+ * let tx = 0.2 * splineTangent(x1, x2, x3, x4, 0);
+ * let ty = 0.2 * splineTangent(y1, y2, y3, y4, 0);
+ * line(x + tx, y + ty, x - tx, y - ty);
+ *
+ * // Center circle.
+ * stroke(0);
+ * x = splinePoint(x1, x2, x3, x4, 0.5);
+ * y = splinePoint(y1, y2, y3, y4, 0.5);
+ * circle(x, y, 5);
+ *
+ * // Center tangent line.
+ * // Scale the tangent point to draw a shorter line.
+ * stroke(255, 0, 0);
+ * tx = 0.2 * splineTangent(x1, x2, x3, x4, 0.5);
+ * ty = 0.2 * splineTangent(y1, y2, y3, y4, 0.5);
+ * line(x + tx, y + ty, x - tx, y - ty);
+ *
+ * // Bottom circle.
+ * stroke(0);
+ * x = splinePoint(x1, x2, x3, x4, 1);
+ * y = splinePoint(y1, y2, y3, y4, 1);
+ * circle(x, y, 5);
+ *
+ * // Bottom tangent line.
+ * // Scale the tangent point to draw a shorter line.
+ * stroke(255, 0, 0);
+ * tx = 0.2 * splineTangent(x1, x2, x3, x4, 1);
+ * ty = 0.2 * splineTangent(y1, y2, y3, y4, 1);
+ * line(x + tx, y + ty, x - tx, y - ty);
+ *
+ * describe(
+ * 'A black curve on a gray square. A white circle moves back and forth along the curve.'
+ * );
+ * }
+ */
+ fn.splineTangent = function(a, b, c, d, t) {
+ const s = this._renderer.states.splineProperties.tightness,
+ tt3 = t * t * 3,
+ t2 = t * 2,
+ f1 = (s - 1) / 2 * tt3 + (1 - s) * t2 + (s - 1) / 2,
+ f2 = (s + 3) / 2 * tt3 + (-5 - s) / 2 * t2,
+ f3 = (-3 - s) / 2 * tt3 + (s + 2) * t2 + (1 - s) / 2,
+ f4 = (1 - s) / 2 * tt3 + (s - 1) / 2 * t2;
+ return a * f1 + b * f2 + c * f3 + d * f4;
+ };
+ }
+
+ if(typeof p5 !== 'undefined'){
+ curves(p5, p5.prototype);
+ }
+
+ /**
+ * @module Shape
+ * @submodule Custom Shapes
+ * @for p5
+ * @requires core
+ * @requires constants
+ */
+
+ function vertex(p5, fn){
+ /**
+ * Begins adding vertices to a custom shape.
+ *
+ * The `beginShape()` and endShape() functions
+ * allow for creating custom shapes in 2D or 3D. `beginShape()` begins adding
+ * vertices to a custom shape and endShape() stops
+ * adding them.
+ *
+ * The parameter, `kind`, sets the kind of shape to make. The available kinds are:
+ *
+ * - `PATH` (the default) to draw shapes by tracing out the path along their edges.
+ * - `POINTS` to draw a series of points.
+ * - `LINES` to draw a series of unconnected line segments.
+ * - `TRIANGLES` to draw a series of separate triangles.
+ * - `TRIANGLE_FAN` to draw a series of connected triangles sharing the first vertex in a fan-like fashion.
+ * - `TRIANGLE_STRIP` to draw a series of connected triangles in strip fashion.
+ * - `QUADS` to draw a series of separate quadrilaterals (quads).
+ * - `QUAD_STRIP` to draw quad strip using adjacent edges to form the next quad.
+ *
+ * After calling `beginShape()`, shapes can be built by calling
+ * vertex(),
+ * bezierVertex(), and/or
+ * splineVertex(). Calling
+ * endShape() will stop adding vertices to the
+ * shape. Each shape will be outlined with the current stroke color and filled
+ * with the current fill color.
+ *
+ * Transformations such as translate(),
+ * rotate(), and
+ * scale() don't work between `beginShape()` and
+ * endShape(). It's also not possible to use
+ * other shapes, such as ellipse() or
+ * rect(), between `beginShape()` and
+ * endShape().
+ *
+ * @method beginShape
+ * @param {(POINTS|LINES|TRIANGLES|TRIANGLE_FAN|TRIANGLE_STRIP|QUADS|QUAD_STRIP|PATH)} [kind=PATH] either POINTS, LINES, TRIANGLES, TRIANGLE_FAN
+ * TRIANGLE_STRIP, QUADS, QUAD_STRIP or PATH. Defaults to PATH.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(30, 20);
+ * vertex(85, 20);
+ * vertex(85, 75);
+ * vertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ *
+ * describe('A white square on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * // Only draw the vertices (points).
+ * beginShape(POINTS);
+ *
+ * // Add vertices.
+ * vertex(30, 20);
+ * vertex(85, 20);
+ * vertex(85, 75);
+ * vertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('Four black dots that form a square are drawn on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * // Only draw lines between alternating pairs of vertices.
+ * beginShape(LINES);
+ *
+ * // Add vertices.
+ * vertex(30, 20);
+ * vertex(85, 20);
+ * vertex(85, 75);
+ * vertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('Two horizontal black lines on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the shape.
+ * noFill();
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(30, 20);
+ * vertex(85, 20);
+ * vertex(85, 75);
+ * vertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('Three black lines form a sideways U shape on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the shape.
+ * noFill();
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(30, 20);
+ * vertex(85, 20);
+ * vertex(85, 75);
+ * vertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * // Connect the first and last vertices.
+ * endShape(CLOSE);
+ *
+ * describe('A black outline of a square drawn on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * // Draw a series of triangles.
+ * beginShape(TRIANGLES);
+ *
+ * // Left triangle.
+ * vertex(30, 75);
+ * vertex(40, 20);
+ * vertex(50, 75);
+ *
+ * // Right triangle.
+ * vertex(60, 20);
+ * vertex(70, 75);
+ * vertex(80, 20);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('Two white triangles drawn on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * // Draw a series of triangles.
+ * beginShape(TRIANGLE_STRIP);
+ *
+ * // Add vertices.
+ * vertex(30, 75);
+ * vertex(40, 20);
+ * vertex(50, 75);
+ * vertex(60, 20);
+ * vertex(70, 75);
+ * vertex(80, 20);
+ * vertex(90, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('Five white triangles that are interleaved drawn on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * // Draw a series of triangles that share their first vertex.
+ * beginShape(TRIANGLE_FAN);
+ *
+ * // Add vertices.
+ * vertex(57.5, 50);
+ * vertex(57.5, 15);
+ * vertex(92, 50);
+ * vertex(57.5, 85);
+ * vertex(22, 50);
+ * vertex(57.5, 15);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('Four white triangles form a square are drawn on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * // Draw a series of quadrilaterals.
+ * beginShape(QUADS);
+ *
+ * // Left rectangle.
+ * vertex(30, 20);
+ * vertex(30, 75);
+ * vertex(50, 75);
+ * vertex(50, 20);
+ *
+ * // Right rectangle.
+ * vertex(65, 20);
+ * vertex(65, 75);
+ * vertex(85, 75);
+ * vertex(85, 20);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('Two white rectangles drawn on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * // Draw a series of quadrilaterals.
+ * beginShape(QUAD_STRIP);
+ *
+ * // Add vertices.
+ * vertex(30, 20);
+ * vertex(30, 75);
+ * vertex(50, 20);
+ * vertex(50, 75);
+ * vertex(65, 20);
+ * vertex(65, 75);
+ * vertex(85, 20);
+ * vertex(85, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('Three white rectangles that share edges are drawn on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * // Draw a series of quadrilaterals.
+ * beginShape(PATH);
+ *
+ * // Add the vertices.
+ * vertex(-30, -30, 0);
+ * vertex(30, -30, 0);
+ * vertex(30, -10, 0);
+ * vertex(-10, -10, 0);
+ * vertex(-10, 10, 0);
+ * vertex(30, 10, 0);
+ * vertex(30, 30, 0);
+ * vertex(-30, 30, 0);
+ *
+ * // Stop drawing the shape.
+ * // Connect the first and last vertices.
+ * endShape(CLOSE);
+ *
+ * describe('A blocky C shape drawn in white on a gray background.');
+ * }
+ *
+ * @example
+ * // Click and drag with the mouse to view the scene from different angles.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A blocky C shape drawn in red, blue, and green on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Start drawing the shape.
+ * // Draw a series of quadrilaterals.
+ * beginShape(PATH);
+ *
+ * // Add the vertices.
+ * fill('red');
+ * stroke('red');
+ * vertex(-30, -30, 0);
+ * vertex(30, -30, 0);
+ * vertex(30, -10, 0);
+ * fill('green');
+ * stroke('green');
+ * vertex(-10, -10, 0);
+ * vertex(-10, 10, 0);
+ * vertex(30, 10, 0);
+ * fill('blue');
+ * stroke('blue');
+ * vertex(30, 30, 0);
+ * vertex(-30, 30, 0);
+ *
+ * // Stop drawing the shape.
+ * // Connect the first and last vertices.
+ * endShape(CLOSE);
+ * }
+ */
+ fn.beginShape = function(kind) {
+ // p5._validateParameters('beginShape', arguments);
+ this._renderer.beginShape(...arguments);
+ };
+
+ /**
+ * Adds a Bézier curve segment to a custom shape.
+ *
+ * `bezierVertex()` adds a curved segment to custom shapes. The Bézier curves
+ * it creates are defined like those made by the
+ * bezier() function. `bezierVertex()` must be
+ * called between the
+ * beginShape() and
+ * endShape() functions.
+ * Bézier need a starting point. Building a shape
+ * only with Bézier curves needs one initial
+ * call to bezierVertex(), before
+ * a number of `bezierVertex()` calls that is a multiple of the parameter
+ * set by bezierOrder(...) (default 3).
+ * But shapes can mix different types of vertices, so if there
+ * are some previous vertices, then the initial anchor is not needed,
+ * only the multiples of 3 (or the Bézier order) calls to
+ * `bezierVertex` for each curve.
+ *
+ * Each curve of order 3 requires three calls to `bezierVertex`, so
+ * 2 curves would need 7 calls to `bezierVertex()`:
+ * (1 one initial anchor point, two sets of 3 curves describing the curves)
+ * With `bezierOrder(2)`, two curves would need 5 calls: 1 + 2 + 2.
+ *
+ * Bézier curves can also be drawn in 3D using WebGL mode.
+ *
+ * Note: `bezierVertex()` won’t work when an argument is passed to
+ * beginShape().
+ *
+ * @method bezierVertex
+ * @param {Number} x x-coordinate of the first control point.
+ * @param {Number} y y-coordinate of the first control point.
+ * @param {Number} [u]
+ * @param {Number} [v]
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the shape.
+ * noFill();
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add the first anchor point.
+ * bezierVertex(30, 20);
+ *
+ * // Add the Bézier vertex.
+ * bezierVertex(80, 0);
+ * bezierVertex(80, 75);
+ * bezierVertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('A black C curve on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Draw the anchor points in black.
+ * stroke(0);
+ * strokeWeight(5);
+ * point(30, 20);
+ * point(30, 75);
+ *
+ * // Draw the control points in red.
+ * stroke(255, 0, 0);
+ * point(80, 0);
+ * point(80, 75);
+ *
+ * // Style the shape.
+ * noFill();
+ * stroke(0);
+ * strokeWeight(1);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add the first anchor point.
+ * bezierVertex(30, 20);
+ *
+ * // Add the Bézier vertex.
+ * bezierVertex(80, 0);
+ * bezierVertex(80, 75);
+ * bezierVertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * // Draw red lines from the anchor points to the control points.
+ * stroke(255, 0, 0);
+ * line(30, 20, 80, 0);
+ * line(30, 75, 80, 75);
+ *
+ * describe(
+ * 'A gray square with three curves. A black curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.'
+ * );
+ * }
+ *
+ * @example
+ * // Click the mouse near the red dot in the top-right corner
+ * // and drag to change the curve's shape.
+ *
+ * let x2 = 80;
+ * let y2 = 0;
+ * let isChanging = false;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * describe(
+ * 'A gray square with three curves. A black curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Draw the anchor points in black.
+ * stroke(0);
+ * strokeWeight(5);
+ * point(30, 20);
+ * point(30, 75);
+ *
+ * // Draw the control points in red.
+ * stroke(255, 0, 0);
+ * point(x2, y2);
+ * point(80, 75);
+ *
+ * // Style the shape.
+ * noFill();
+ * stroke(0);
+ * strokeWeight(1);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add the first anchor point.
+ * bezierVertex(30, 20);
+ *
+ * // Add the Bézier vertex.
+ * bezierVertex(x2, y2);
+ * bezierVertex(80, 75);
+ * bezierVertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * // Draw red lines from the anchor points to the control points.
+ * stroke(255, 0, 0);
+ * line(30, 20, x2, y2);
+ * line(30, 75, 80, 75);
+ * }
+ *
+ * // Start changing the first control point if the user clicks near it.
+ * function mousePressed() {
+ * if (dist(mouseX, mouseY, x2, y2) < 20) {
+ * isChanging = true;
+ * }
+ * }
+ *
+ * // Stop changing the first control point when the user releases the mouse.
+ * function mouseReleased() {
+ * isChanging = false;
+ * }
+ *
+ * // Update the first control point while the user drags the mouse.
+ * function mouseDragged() {
+ * if (isChanging === true) {
+ * x2 = mouseX;
+ * y2 = mouseY;
+ * }
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add the first anchor point.
+ * bezierVertex(30, 20);
+ *
+ * // Add the Bézier vertices.
+ * bezierVertex(80, 0);
+ * bezierVertex(80, 75);
+ * bezierVertex(30, 75);
+ *
+ * bezierVertex(50, 80);
+ * bezierVertex(60, 25);
+ * bezierVertex(30, 20);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('A crescent moon shape drawn in white on a gray background.');
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A crescent moon shape drawn in white on a blue background. When the user drags the mouse, the scene rotates and a second moon is revealed.');
+ * }
+ *
+ * function draw() {
+ * background('midnightblue');
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Style the moons.
+ * noStroke();
+ * fill('lemonchiffon');
+ *
+ * // Draw the first moon.
+ * beginShape();
+ * bezierVertex(-20, -30, 0);
+ *
+ * bezierVertex(30, -50, 0);
+ * bezierVertex(30, 25, 0);
+ * bezierVertex(-20, 25, 0);
+ *
+ * bezierVertex(0, 30, 0);
+ * bezierVertex(10, -25, 0);
+ * bezierVertex(-20, -30, 0);
+ * endShape();
+ *
+ * // Draw the second moon.
+ * beginShape();
+ *
+ * bezierVertex(-20, -30, -20);
+ *
+ * bezierVertex(30, -50, -20);
+ * bezierVertex(30, 25, -20);
+ * bezierVertex(-20, 25, -20);
+ *
+ * bezierVertex(0, 30, -20);
+ * bezierVertex(10, -25, -20);
+ * bezierVertex(-20, -30, -20);
+ *
+ * endShape();
+ * }
+ */
+ /**
+ * @method bezierVertex
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} z
+ * @param {Number} [u]
+ * @param {Number} [v]
+ */
+ fn.bezierVertex = function(...args) {
+ this._renderer.bezierVertex(...args);
+ };
+
+ /**
+ * Concludes the vertices of a custom shape.
+ *
+ * The beginShape() and `endShape()` functions
+ * allow for creating custom shapes in 2D or 3D.
+ * beginShape() begins adding vertices to a
+ * custom shape and `endShape()` stops adding them.
+ *
+ * The first parameter, `mode`, is optional. By default, the first and last
+ * vertices of a shape aren't connected. If the constant `CLOSE` is passed, as
+ * in `endShape(CLOSE)`, then the first and last vertices will be connected.
+ * When CLOSE mode is used for splines (with `splineVeertex()`), the shape is ended smoothly.
+ *
+ *
+ * The second parameter, `count`, is also optional. In WebGL mode, it’s more
+ * efficient to draw many copies of the same shape using a technique called
+ * instancing.
+ * The `count` parameter tells WebGL mode how many copies to draw. For
+ * example, calling `endShape(CLOSE, 400)` after drawing a custom shape will
+ * make it efficient to draw 400 copies. This feature requires
+ * writing a custom shader.
+ *
+ * After calling beginShape(), shapes can be
+ * built by calling vertex(),
+ * bezierVertex() and/or
+ * splineVertex(). Calling
+ * `endShape()` will stop adding vertices to the
+ * shape. Each shape will be outlined with the current stroke color and filled
+ * with the current fill color.
+ *
+ * Transformations such as translate(),
+ * rotate(), and
+ * scale() don't work between
+ * beginShape() and `endShape()`. It's also not
+ * possible to use other shapes, such as ellipse() or
+ * rect(), between
+ * beginShape() and `endShape()`.
+ *
+ * @method endShape
+ * @param {CLOSE} [mode] use CLOSE to close the shape
+ * @param {Integer} [count] number of times you want to draw/instance the shape (for WebGL mode).
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the shapes.
+ * noFill();
+ *
+ * // Left triangle.
+ * beginShape();
+ * vertex(20, 20);
+ * vertex(45, 20);
+ * vertex(45, 80);
+ * endShape(CLOSE);
+ *
+ * // Right triangle.
+ * beginShape();
+ * vertex(50, 20);
+ * vertex(75, 20);
+ * vertex(75, 80);
+ * endShape();
+ *
+ * describe(
+ * 'Two sets of black lines drawn on a gray background. The three lines on the left form a right triangle. The two lines on the right form a right angle.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(200, 100);
+ *
+ * background(240);
+ *
+ * noFill();
+ * stroke(0);
+ *
+ * // Open shape (left)
+ * beginShape();
+ * vertex(20, 20);
+ * vertex(80, 20);
+ * vertex(80, 80);
+ * endShape(); // Not closed
+ *
+ * // Closed shape (right)
+ * beginShape();
+ * vertex(120, 20);
+ * vertex(180, 20);
+ * vertex(180, 80);
+ * endShape(CLOSE); // Closed
+ *
+ * describe(
+ * 'Two right-angled shapes on a light gray background. The left shape is open with three lines. The right shape is closed, forming a triangle.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ * background(200);
+ *
+ * beginShape();
+ *
+ * splineVertex(32, 91);
+ * splineVertex(21, 17);
+ * splineVertex(68, 19);
+ * splineVertex(82, 91);
+ *
+ * endShape(CLOSE);
+ *
+ * describe(
+ * 'A curvy four-sided slightly lopsided blob.'
+ * );
+ * }
+ *
+ * @example
+ * // Note: A "uniform" is a global variable within a shader program.
+ *
+ * // Create a string with the vertex shader program.
+ * // The vertex shader is called for each vertex.
+ * let vertSrc = `#version 300 es
+ *
+ * precision mediump float;
+ *
+ * in vec3 aPosition;
+ * flat out int instanceID;
+ *
+ * uniform mat4 uModelViewMatrix;
+ * uniform mat4 uProjectionMatrix;
+ *
+ * void main() {
+ *
+ * // Copy the instance ID to the fragment shader.
+ * instanceID = gl_InstanceID;
+ * vec4 positionVec4 = vec4(aPosition, 1.0);
+ *
+ * // gl_InstanceID represents a numeric value for each instance.
+ * // Using gl_InstanceID allows us to move each instance separately.
+ * // Here we move each instance horizontally by ID * 23.
+ * float xOffset = float(gl_InstanceID) * 23.0;
+ *
+ * // Apply the offset to the final position.
+ * gl_Position = uProjectionMatrix * uModelViewMatrix * (positionVec4 -
+ * vec4(xOffset, 0.0, 0.0, 0.0));
+ * }
+ * `;
+ *
+ * // Create a string with the fragment shader program.
+ * // The fragment shader is called for each pixel.
+ * let fragSrc = `#version 300 es
+ *
+ * precision mediump float;
+ *
+ * out vec4 outColor;
+ * flat in int instanceID;
+ * uniform float numInstances;
+ *
+ * void main() {
+ * vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
+ * vec4 blue = vec4(0.0, 0.0, 1.0, 1.0);
+ *
+ * // Normalize the instance ID.
+ * float normId = float(instanceID) / numInstances;
+ *
+ * // Mix between two colors using the normalized instance ID.
+ * outColor = mix(red, blue, normId);
+ * }
+ * `;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Shader object.
+ * let myShader = createShader(vertSrc, fragSrc);
+ *
+ * background(220);
+ *
+ * // Compile and apply the p5.Shader.
+ * shader(myShader);
+ *
+ * // Set the numInstances uniform.
+ * myShader.setUniform('numInstances', 4);
+ *
+ * // Translate the origin to help align the drawing.
+ * translate(25, -10);
+ *
+ * // Style the shapes.
+ * noStroke();
+ *
+ * // Draw the shapes.
+ * beginShape();
+ * vertex(0, 0);
+ * vertex(0, 20);
+ * vertex(20, 20);
+ * vertex(20, 0);
+ * vertex(0, 0);
+ * endShape(CLOSE, 4);
+ *
+ * describe('A row of four squares. Their colors transition from purple on the left to red on the right');
+ * }
+ */
+ fn.endShape = function(mode, count = 1) {
+ // p5._validateParameters('endShape', arguments);
+ if (count < 1) {
+ console.log('🌸 p5.js says: You can not have less than one instance');
+ count = 1;
+ }
+
+ this._renderer.endShape(mode, count);
+ };
+
+ /**
+ * Sets the normal vector for vertices in a custom 3D shape.
+ *
+ * 3D shapes created with beginShape() and
+ * endShape() are made by connecting sets of
+ * points called vertices. Each vertex added with
+ * vertex() has a normal vector that points away
+ * from it. The normal vector controls how light reflects off the shape.
+ *
+ * `normal()` can be called two ways with different parameters to define the
+ * normal vector's components.
+ *
+ * The first way to call `normal()` has three parameters, `x`, `y`, and `z`.
+ * If `Number`s are passed, as in `normal(1, 2, 3)`, they set the x-, y-, and
+ * z-components of the normal vector.
+ *
+ * The second way to call `normal()` has one parameter, `vector`. If a
+ * p5.Vector object is passed, as in
+ * `normal(myVector)`, its components will be used to set the normal vector.
+ *
+ * `normal()` changes the normal vector of vertices added to a custom shape
+ * with vertex(). `normal()` must be called between
+ * the beginShape() and
+ * endShape() functions, just like
+ * vertex(). The normal vector set by calling
+ * `normal()` will affect all following vertices until `normal()` is called
+ * again:
+ *
+ * ```js
+ * beginShape();
+ *
+ * // Set the vertex normal.
+ * normal(-0.4, -0.4, 0.8);
+ *
+ * // Add a vertex.
+ * vertex(-30, -30, 0);
+ *
+ * // Set the vertex normal.
+ * normal(0, 0, 1);
+ *
+ * // Add vertices.
+ * vertex(30, -30, 0);
+ * vertex(30, 30, 0);
+ *
+ * // Set the vertex normal.
+ * normal(0.4, -0.4, 0.8);
+ *
+ * // Add a vertex.
+ * vertex(-30, 30, 0);
+ *
+ * endShape();
+ * ```
+ *
+ * @method normal
+ * @param {p5.Vector} vector vertex normal as a p5.Vector object.
+ * @chainable
+ *
+ * @example
+ * // Click the and drag the mouse to view the scene from a different angle.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe(
+ * 'A colorful square on a black background. The square changes color and rotates when the user drags the mouse. Parts of its surface reflect light in different directions.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(0);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Style the shape.
+ * normalMaterial();
+ * noStroke();
+ *
+ * // Draw the shape.
+ * beginShape();
+ * vertex(-30, -30, 0);
+ * vertex(30, -30, 0);
+ * vertex(30, 30, 0);
+ * vertex(-30, 30, 0);
+ * endShape();
+ * }
+ *
+ * @example
+ * // Click the and drag the mouse to view the scene from a different angle.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe(
+ * 'A colorful square on a black background. The square changes color and rotates when the user drags the mouse. Parts of its surface reflect light in different directions.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(0);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Style the shape.
+ * normalMaterial();
+ * noStroke();
+ *
+ * // Draw the shape.
+ * // Use normal() to set vertex normals.
+ * beginShape();
+ * normal(-0.4, -0.4, 0.8);
+ * vertex(-30, -30, 0);
+ *
+ * normal(0, 0, 1);
+ * vertex(30, -30, 0);
+ * vertex(30, 30, 0);
+ *
+ * normal(0.4, -0.4, 0.8);
+ * vertex(-30, 30, 0);
+ * endShape();
+ * }
+ *
+ * @example
+ * // Click the and drag the mouse to view the scene from a different angle.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe(
+ * 'A colorful square on a black background. The square changes color and rotates when the user drags the mouse. Parts of its surface reflect light in different directions.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(0);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Style the shape.
+ * normalMaterial();
+ * noStroke();
+ *
+ * // Create p5.Vector objects.
+ * let n1 = createVector(-0.4, -0.4, 0.8);
+ * let n2 = createVector(0, 0, 1);
+ * let n3 = createVector(0.4, -0.4, 0.8);
+ *
+ * // Draw the shape.
+ * // Use normal() to set vertex normals.
+ * beginShape();
+ * normal(n1);
+ * vertex(-30, -30, 0);
+ *
+ * normal(n2);
+ * vertex(30, -30, 0);
+ * vertex(30, 30, 0);
+ *
+ * normal(n3);
+ * vertex(-30, 30, 0);
+ * endShape();
+ * }
+ */
+ /**
+ * @method normal
+ * @param {Number} x x-component of the vertex normal.
+ * @param {Number} y y-component of the vertex normal.
+ * @param {Number} z z-component of the vertex normal.
+ * @chainable
+ */
+ fn.normal = function(x, y, z) {
+ this._assert3d('normal');
+ // p5._validateParameters('normal', arguments);
+ this._renderer.normal(...arguments);
+
+ return this;
+ };
+
+ /**
+ * Sets the shader's vertex property or attribute variables.
+ *
+ * A vertex property, or vertex attribute, is a variable belonging to a vertex in a shader. p5.js provides some
+ * default properties, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are
+ * set using vertex(), normal()
+ * and fill() respectively. Custom properties can also
+ * be defined within beginShape() and
+ * endShape().
+ *
+ * The first parameter, `propertyName`, is a string with the property's name.
+ * This is the same variable name which should be declared in the shader, such as
+ * `in vec3 aProperty`, similar to .`setUniform()`.
+ *
+ * The second parameter, `data`, is the value assigned to the shader variable. This
+ * value will be applied to subsequent vertices created with
+ * vertex(). It can be a Number or an array of numbers,
+ * and in the shader program the type can be declared according to the WebGL
+ * specification. Common types include `float`, `vec2`, `vec3`, `vec4` or matrices.
+ *
+ * See also the vertexProperty() method on
+ * Geometry objects.
+ *
+ * @method vertexProperty
+ * @for p5
+ * @param {String} attributeName the name of the vertex attribute.
+ * @param {Number|Number[]} data the data tied to the vertex attribute.
+ *
+ * @example
+ * const vertSrc = `#version 300 es
+ * precision mediump float;
+ * uniform mat4 uModelViewMatrix;
+ * uniform mat4 uProjectionMatrix;
+ *
+ * in vec3 aPosition;
+ * in vec2 aOffset;
+ *
+ * void main(){
+ * vec4 positionVec4 = vec4(aPosition.xyz, 1.0);
+ * positionVec4.xy += aOffset;
+ * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
+ * }
+ * `;
+ *
+ * const fragSrc = `#version 300 es
+ * precision mediump float;
+ * out vec4 outColor;
+ * void main(){
+ * outColor = vec4(0.0, 1.0, 1.0, 1.0);
+ * }
+ * `;
+ *
+ * function setup(){
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create and use the custom shader.
+ * const myShader = createShader(vertSrc, fragSrc);
+ * shader(myShader);
+ *
+ * describe('A wobbly, cyan circle on a gray background.');
+ * }
+ *
+ * function draw(){
+ * // Set the styles
+ * background(125);
+ * noStroke();
+ *
+ * // Draw the circle.
+ * beginShape();
+ * for (let i = 0; i < 30; i++){
+ * const x = 40 * cos(i/30 * TWO_PI);
+ * const y = 40 * sin(i/30 * TWO_PI);
+ *
+ * // Apply some noise to the coordinates.
+ * const xOff = 10 * noise(x + millis()/1000) - 5;
+ * const yOff = 10 * noise(y + millis()/1000) - 5;
+ *
+ * // Apply these noise values to the following vertex.
+ * vertexProperty('aOffset', [xOff, yOff]);
+ * vertex(x, y);
+ * }
+ * endShape(CLOSE);
+ * }
+ *
+ * @example
+ * let myShader;
+ * const cols = 10;
+ * const rows = 10;
+ * const cellSize = 6;
+ *
+ * const vertSrc = `#version 300 es
+ * precision mediump float;
+ * uniform mat4 uProjectionMatrix;
+ * uniform mat4 uModelViewMatrix;
+ *
+ * in vec3 aPosition;
+ * in vec3 aNormal;
+ * in vec3 aVertexColor;
+ * in float aDistance;
+ *
+ * out vec3 vVertexColor;
+ *
+ * void main(){
+ * vec4 positionVec4 = vec4(aPosition, 1.0);
+ * positionVec4.xyz += aDistance * aNormal * 2.0;;
+ * vVertexColor = aVertexColor;
+ * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
+ * }
+ * `;
+ *
+ * const fragSrc = `#version 300 es
+ * precision mediump float;
+ *
+ * in vec3 vVertexColor;
+ * out vec4 outColor;
+ *
+ * void main(){
+ * outColor = vec4(vVertexColor, 1.0);
+ * }
+ * `;
+ *
+ * function setup(){
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create and apply the custom shader.
+ * myShader = createShader(vertSrc, fragSrc);
+ * shader(myShader);
+ * noStroke();
+ * describe('A blue grid, which moves away from the mouse position, on a gray background.');
+ * }
+ *
+ * function draw(){
+ * background(200);
+ *
+ * // Draw the grid in the middle of the screen.
+ * translate(-cols*cellSize/2, -rows*cellSize/2);
+ * beginShape(QUADS);
+ * for (let i = 0; i < cols; i++) {
+ * for (let j = 0; j < rows; j++) {
+ *
+ * // Calculate the cell position.
+ * let x = i * cellSize;
+ * let y = j * cellSize;
+ *
+ * fill(j/rows*255, j/cols*255, 255);
+ *
+ * // Calculate the distance from the corner of each cell to the mouse.
+ * let distance = dist(x, y, mouseX, mouseY);
+ *
+ * // Send the distance to the shader.
+ * vertexProperty('aDistance', min(distance, 100));
+ *
+ * vertex(x, y);
+ * vertex(x + cellSize, y);
+ * vertex(x + cellSize, y + cellSize);
+ * vertex(x, y + cellSize);
+ * }
+ * }
+ * endShape();
+ * }
+ */
+ fn.vertexProperty = function(attributeName, data){
+ // this._assert3d('vertexProperty');
+ // p5._validateParameters('vertexProperty', arguments);
+ this._renderer.vertexProperty(attributeName, data);
+ };
+ }
+
+ if(typeof p5 !== 'undefined'){
+ vertex(p5, p5.prototype);
+ }
+
+ /**
+ * @module Color
+ * @submodule Setting
+ * @for p5
+ * @requires core
+ * @requires constants
+ */
+
+
+ function setting(p5, fn){
+ /**
+ * Starts defining a shape that will mask any shapes drawn afterward.
+ *
+ * Any shapes drawn between `beginClip()` and
+ * endClip() will add to the mask shape. The mask
+ * will apply to anything drawn after endClip().
+ *
+ * The parameter, `options`, is optional. If an object with an `invert`
+ * property is passed, as in `beginClip({ invert: true })`, it will be used to
+ * set the masking mode. `{ invert: true }` inverts the mask, creating holes
+ * in shapes that are masked. `invert` is `false` by default.
+ *
+ * Masks can be contained between the
+ * push() and pop() functions.
+ * Doing so allows unmasked shapes to be drawn after masked shapes.
+ *
+ * Masks can also be defined in a callback function that's passed to
+ * clip().
+ *
+ * @method beginClip
+ * @param {Object} [options] an object containing clip settings.
+ * @param {Boolean} [options.invert=false] Whether or not to invert the mask.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create a mask.
+ * beginClip();
+ * triangle(15, 37, 30, 13, 43, 37);
+ * circle(45, 45, 7);
+ * endClip();
+ *
+ * // Draw a backing shape.
+ * square(5, 5, 45);
+ *
+ * describe('A white triangle and circle on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create an inverted mask.
+ * beginClip({ invert: true });
+ * triangle(15, 37, 30, 13, 43, 37);
+ * circle(45, 45, 7);
+ * endClip();
+ *
+ * // Draw a backing shape.
+ * square(5, 5, 45);
+ *
+ * describe('A white square at the top-left corner of a gray square. The white square has a triangle and a circle cut out of it.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * noStroke();
+ *
+ * // Draw a masked shape.
+ * push();
+ * // Create a mask.
+ * beginClip();
+ * triangle(15, 37, 30, 13, 43, 37);
+ * circle(45, 45, 7);
+ * endClip();
+ *
+ * // Draw a backing shape.
+ * square(5, 5, 45);
+ * pop();
+ *
+ * // Translate the origin to the center.
+ * translate(50, 50);
+ *
+ * // Draw an inverted masked shape.
+ * push();
+ * // Create an inverted mask.
+ * beginClip({ invert: true });
+ * triangle(15, 37, 30, 13, 43, 37);
+ * circle(45, 45, 7);
+ * endClip();
+ *
+ * // Draw a backing shape.
+ * square(5, 5, 45);
+ * pop();
+ *
+ * describe('In the top left, a white triangle and circle. In the bottom right, a white square with a triangle and circle cut out of it.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A silhouette of a rotating torus colored fuchsia.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Create a mask.
+ * beginClip();
+ * push();
+ * rotateX(frameCount * 0.01);
+ * rotateY(frameCount * 0.01);
+ * scale(0.5);
+ * torus(30, 15);
+ * pop();
+ * endClip();
+ *
+ * // Draw a backing shape.
+ * noStroke();
+ * fill('fuchsia');
+ * plane(100);
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A silhouette of a rotating torus colored with a gradient from cyan to purple.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Create a mask.
+ * beginClip();
+ * push();
+ * rotateX(frameCount * 0.01);
+ * rotateY(frameCount * 0.01);
+ * scale(0.5);
+ * torus(30, 15);
+ * pop();
+ * endClip();
+ *
+ * // Draw a backing shape.
+ * noStroke();
+ * beginShape(QUAD_STRIP);
+ * fill(0, 255, 255);
+ * vertex(-width / 2, -height / 2);
+ * vertex(width / 2, -height / 2);
+ * fill(100, 0, 100);
+ * vertex(-width / 2, height / 2);
+ * vertex(width / 2, height / 2);
+ * endShape();
+ * }
+ */
+ fn.beginClip = function(options = {}) {
+ this._renderer.beginClip(options);
+ };
+
+ /**
+ * Ends defining a mask that was started with
+ * beginClip().
+ *
+ * @method endClip
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create a mask.
+ * beginClip();
+ * triangle(15, 37, 30, 13, 43, 37);
+ * circle(45, 45, 7);
+ * endClip();
+ *
+ * // Draw a backing shape.
+ * square(5, 5, 45);
+ *
+ * describe('A white triangle and circle on a gray background.');
+ * }
+ */
+ fn.endClip = function() {
+ this._renderer.endClip();
+ };
+
+ /**
+ * Defines a shape that will mask any shapes drawn afterward.
+ *
+ * The first parameter, `callback`, is a function that defines the mask.
+ * Any shapes drawn in `callback` will add to the mask shape. The mask
+ * will apply to anything drawn after `clip()` is called.
+ *
+ * The second parameter, `options`, is optional. If an object with an `invert`
+ * property is passed, as in `beginClip({ invert: true })`, it will be used to
+ * set the masking mode. `{ invert: true }` inverts the mask, creating holes
+ * in shapes that are masked. `invert` is `false` by default.
+ *
+ * Masks can be contained between the
+ * push() and pop() functions.
+ * Doing so allows unmasked shapes to be drawn after masked shapes.
+ *
+ * Masks can also be defined with beginClip()
+ * and endClip().
+ *
+ * @method clip
+ * @param {Function} callback a function that draws the mask shape.
+ * @param {Object} [options] an object containing clip settings.
+ * @param {Boolean} [options.invert=false] Whether or not to invert the mask.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create a mask.
+ * clip(mask);
+ *
+ * // Draw a backing shape.
+ * square(5, 5, 45);
+ *
+ * describe('A white triangle and circle on a gray background.');
+ * }
+ *
+ * // Declare a function that defines the mask.
+ * function mask() {
+ * triangle(15, 37, 30, 13, 43, 37);
+ * circle(45, 45, 7);
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create an inverted mask.
+ * clip(mask, { invert: true });
+ *
+ * // Draw a backing shape.
+ * square(5, 5, 45);
+ *
+ * describe('A white square at the top-left corner of a gray square. The white square has a triangle and a circle cut out of it.');
+ * }
+ *
+ * // Declare a function that defines the mask.
+ * function mask() {
+ * triangle(15, 37, 30, 13, 43, 37);
+ * circle(45, 45, 7);
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * noStroke();
+ *
+ * // Draw a masked shape.
+ * push();
+ * // Create a mask.
+ * clip(mask);
+ *
+ * // Draw a backing shape.
+ * square(5, 5, 45);
+ * pop();
+ *
+ * // Translate the origin to the center.
+ * translate(50, 50);
+ *
+ * // Draw an inverted masked shape.
+ * push();
+ * // Create an inverted mask.
+ * clip(mask, { invert: true });
+ *
+ * // Draw a backing shape.
+ * square(5, 5, 45);
+ * pop();
+ *
+ * describe('In the top left, a white triangle and circle. In the bottom right, a white square with a triangle and circle cut out of it.');
+ * }
+ *
+ * // Declare a function that defines the mask.
+ * function mask() {
+ * triangle(15, 37, 30, 13, 43, 37);
+ * circle(45, 45, 7);
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A silhouette of a rotating torus colored fuchsia.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Create a mask.
+ * clip(mask);
+ *
+ * // Draw a backing shape.
+ * noStroke();
+ * fill('fuchsia');
+ * plane(100);
+ * }
+ *
+ * // Declare a function that defines the mask.
+ * function mask() {
+ * push();
+ * rotateX(frameCount * 0.01);
+ * rotateY(frameCount * 0.01);
+ * scale(0.5);
+ * torus(30, 15);
+ * pop();
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A silhouette of a rotating torus colored with a gradient from cyan to purple.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Create a mask.
+ * clip(mask);
+ *
+ * // Draw a backing shape.
+ * noStroke();
+ * beginShape(QUAD_STRIP);
+ * fill(0, 255, 255);
+ * vertex(-width / 2, -height / 2);
+ * vertex(width / 2, -height / 2);
+ * fill(100, 0, 100);
+ * vertex(-width / 2, height / 2);
+ * vertex(width / 2, height / 2);
+ * endShape();
+ * }
+ *
+ * // Declare a function that defines the mask.
+ * function mask() {
+ * push();
+ * rotateX(frameCount * 0.01);
+ * rotateY(frameCount * 0.01);
+ * scale(0.5);
+ * torus(30, 15);
+ * pop();
+ * }
+ */
+ fn.clip = function(callback, options) {
+ this._renderer.beginClip(options);
+ callback();
+ this._renderer.endClip(options);
+ };
+
+ /**
+ * Sets the color used for the background of the canvas.
+ *
+ * By default, the background is transparent. `background()` is typically used
+ * within draw() to clear the display window at the
+ * beginning of each frame. It can also be used inside
+ * setup() to set the background on the first frame
+ * of animation.
+ *
+ * The version of `background()` with one parameter interprets the value one
+ * of four ways. If the parameter is a `Number`, it's interpreted as a grayscale
+ * value. If the parameter is a `String`, it's interpreted as a CSS color string.
+ * RGB, RGBA, HSL, HSLA, hex, and named color strings are supported. If the
+ * parameter is a p5.Color object, it will be used as
+ * the background color. If the parameter is a
+ * p5.Image object, it will be used as the background
+ * image.
+ *
+ * The version of `background()` with two parameters interprets the first one
+ * as a grayscale value. The second parameter sets the alpha (transparency)
+ * value.
+ *
+ * The version of `background()` with three parameters interprets them as RGB,
+ * HSB, or HSL colors, depending on the current
+ * colorMode(). By default, colors are specified
+ * in RGB values. Calling `background(255, 204, 0)` sets the background a bright
+ * yellow color.
+ *
+ * The version of `background()` with four parameters interprets them as RGBA,
+ * HSBA, or HSLA colors, depending on the current
+ * colorMode(). The last parameter sets the alpha
+ * (transparency) value.
+ *
+ * @method background
+ * @param {p5.Color} color any value created by the color() function
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // A grayscale value.
+ * background(51);
+ *
+ * describe('A canvas with a dark charcoal gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // A grayscale value and an alpha value.
+ * background(51, 0.4);
+ * describe('A canvas with a transparent gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // R, G & B values.
+ * background(255, 204, 0);
+ *
+ * describe('A canvas with a yellow background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // R, G, B, and Alpha values.
+ * background(255, 0, 0, 128);
+ *
+ * describe('A canvas with a semi-transparent red background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Use HSB color.
+ * colorMode(HSB);
+ *
+ * // H, S & B values.
+ * background(255, 204, 100);
+ *
+ * describe('A canvas with a royal blue background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // A CSS named color.
+ * background('red');
+ *
+ * describe('A canvas with a red background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Three-digit hex RGB notation.
+ * background('#fae');
+ *
+ * describe('A canvas with a pink background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Six-digit hex RGB notation.
+ * background('#222222');
+ *
+ * describe('A canvas with a black background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Integer RGB notation.
+ * background('rgb(0, 255, 0)');
+ *
+ * describe('A canvas with a bright green background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Integer RGBA notation.
+ * background('rgba(0, 255, 0, 0.25)');
+ *
+ * describe('A canvas with a transparent green background.');
+ * }
+ * <
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Percentage RGB notation.
+ * background('rgb(100%, 0%, 10%)');
+ *
+ * describe('A canvas with a red background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Percentage RGBA notation.
+ * background('rgba(100%, 0%, 100%, 0.5)');
+ *
+ * describe('A canvas with a transparent purple background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // A p5.Color object.
+ * let c = color(0, 0, 255);
+ * background(c);
+ *
+ * describe('A canvas with a blue background.');
+ * }
+ */
+ /**
+ * @method background
+ * @param {String} colorstring color string, possible formats include: integer
+ * rgb() or rgba(), percentage rgb() or rgba(),
+ * 3-digit hex, 6-digit hex.
+ * @param {Number} [a] opacity of the background relative to current
+ * color range (default is 0-255).
+ * @chainable
+ */
+ /**
+ * @method background
+ * @param {Number} gray specifies a value between white and black.
+ * @param {Number} [a]
+ * @chainable
+ */
+ /**
+ * @method background
+ * @param {Number} v1 red value if color mode is RGB, or hue value if color mode is HSB.
+ * @param {Number} v2 green value if color mode is RGB, or saturation value if color mode is HSB.
+ * @param {Number} v3 blue value if color mode is RGB, or brightness value if color mode is HSB.
+ * @param {Number} [a]
+ * @chainable
+ */
+ /**
+ * @method background
+ * @param {Number[]} values an array containing the red, green, blue
+ * and alpha components of the color.
+ * @chainable
+ */
+ /**
+ * @method background
+ * @param {p5.Image} image image created with loadImage()
+ * or createImage(),
+ * to set as background.
+ * (must be same size as the sketch window).
+ * @param {Number} [a]
+ * @chainable
+ */
+ fn.background = function(...args) {
+ this._renderer.background(...args);
+ return this;
+ };
+
+ /**
+ * Clears the pixels on the canvas.
+ *
+ * `clear()` makes every pixel 100% transparent. Calling `clear()` doesn't
+ * clear objects created by `createX()` functions such as
+ * createGraphics(),
+ * createVideo(), and
+ * createImg(). These objects will remain
+ * unchanged after calling `clear()` and can be redrawn.
+ *
+ * In WebGL mode, this function can clear the screen to a specific color. It
+ * interprets four numeric parameters as normalized RGBA color values. It also
+ * clears the depth buffer. If you are not using the WebGL renderer, these
+ * parameters will have no effect.
+ *
+ * @method clear
+ * @chainable
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * describe('A gray square. White circles are drawn as the user moves the mouse. The circles disappear when the user presses the mouse.');
+ * }
+ *
+ * function draw() {
+ * circle(mouseX, mouseY, 20);
+ * }
+ *
+ * function mousePressed() {
+ * clear();
+ * background(200);
+ * }
+ *
+ * @example
+ * let pg;
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ * background(200);
+ *
+ * pg = createGraphics(60, 60);
+ * pg.background(200);
+ * pg.noStroke();
+ * pg.circle(pg.width / 2, pg.height / 2, 15);
+ * image(pg, 20, 20);
+ *
+ * describe('A white circle drawn on a gray square. The square gets smaller when the mouse is pressed.');
+ * }
+ *
+ * function mousePressed() {
+ * clear();
+ * image(pg, 20, 20);
+ * }
+ *
+ * @param {Number} [r] normalized red value.
+ * @param {Number} [g] normalized green value.
+ * @param {Number} [b] normalized blue value.
+ * @param {Number} [a] normalized alpha value.
+ */
+ fn.clear = function(...args) {
+ const _r = args[0] || 0;
+ const _g = args[1] || 0;
+ const _b = args[2] || 0;
+ const _a = args[3] || 0;
+
+ this._renderer.clear(_r, _g, _b, _a);
+ return this;
+ };
+
+ /**
+ * Changes the way color values are interpreted.
+ *
+ * By default, the `Number` parameters for fill(),
+ * stroke(),
+ * background(), and
+ * color() are defined by values between 0 and 255
+ * using the RGB color model. This is equivalent to calling
+ * `colorMode(RGB, 255)`. Pure red is `color(255, 0, 0)` in this model.
+ *
+ * Calling `colorMode(RGB, 100)` sets colors to use RGB color values
+ * between 0 and 100. Pure red is `color(100, 0, 0)` in this model.
+ *
+ * Calling `colorMode(HSB)` or `colorMode(HSL)` changes to HSB or HSL systems instead of RGB.
+ * Pure red is `color(0, 100, 100)` in HSB and `color(0, 100, 50)` in HSL.
+ *
+ * Some additional color modes that p5.js supports are:
+ *
+ * `RGBHDR` - High Dynamic Range RGB defined within the Display P3 color space.
+ * Colors are expressed with an extended dynamic range. To render these colors
+ * accurately, you must use the HDR canvas.
+ *
+ * `HWB` - Hue, Whiteness, Blackness.
+ * Similar to HSB and HSL, this mode uses a hue angle.
+ * Instead of saturation and lightness, HWB defines colors based on the percentage
+ * of whiteness and blackness. This is the color model used by Chrome's GUI color picker.
+ * Pure red in HWB is represented as `color(0, 0, 0)` (i.e., hue 0 with 0% whiteness and 0% blackness).
+ *
+ *
+ *
+ * `LAB` - Also known as CIE Lab, this color mode defines colors with Lightness, Alpha, and Beta.
+ * It is widely used in professional color measurement contexts due to its perceptual uniformity.
+ *
+ * `LCH` - A more intuitive representation of the CIE Lab color space using Lightness, Chroma, and Hue.
+ * This mode separates the color's chromatic intensity (chroma) from its lightness,
+ * simplifying color selection and manipulation.
+ *
+ * `OKLAB` - A variant of the CIE Lab color space that corrects for non-uniformities inherent in LAB.
+ * The adjustment provides a more perceptually accurate and uniform representation,
+ * which is particularly beneficial for smooth color transitions.
+ *
+ * `OKLCH` - An easier-to-use representation of OKLAB, expressing colors in terms of Lightness, Chroma, and Hue.
+ * This mode retains the perceptual benefits of OKLAB while offering a more intuitive format for color manipulation.
+ *
+ * p5.Color objects remember the mode that they were
+ * created in. Changing modes doesn't affect their appearance.
+ *
+ * `Single-value (Grayscale) Colors`:
+ * When a color is specified with only one parameter (e.g., `color(g)`), p5.js will interpret it
+ * as a grayscale color. However, how that single parameter translates into a grayscale value
+ * depends on the color mode:
+ *
+ * - `RGB, HSB, and HSL`: In RGB, the single value is interpreted using the “blue” maximum
+ * (i.e., the single parameter is mapped to the blue channel's max).
+ * In HSB and HSL, the single value is mapped to Brightness and Lightness max respectively with hue=0 .
+ * and saturation=0.
+ *
+ * - `LAB, LCH, OKLAB, and OKLCH`: The single value is taken to be the `lightness (L)` component,
+ * with the specified max range for that channel.
+ *
+ * - `HWB`: Grayscale relies on both the `whiteness (W)` and `blackness (B)` channels. Since
+ * a single value cannot directly account for two distinct channels, the library uses an
+ * average of their max values to interpret the single grayscale parameter. For instance,
+ * if W has a max of 50 and B has a max of 100, then the single grayscale parameter
+ * is mapped using (50 + 100) / 2 = 75 as its effective maximum. More complex or negative
+ * ranges are currently not handled, so results in those cases may be ambiguous.
+ *
+ * @method colorMode
+ * @param {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} mode either RGB, HSB, HSL,
+ * or one of the extended modes described above.
+ * @param {Number} [max] range for all values.
+ * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Fill with pure red.
+ * fill(255, 0, 0);
+ *
+ * circle(50, 50, 25);
+ *
+ * describe('A gray square with a red circle at its center.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Use RGB color with values in the range 0-100.
+ * colorMode(RGB, 100);
+ *
+ * // Fill with pure red.
+ * fill(100, 0, 0);
+ *
+ * circle(50, 50, 25);
+ *
+ * describe('A gray square with a red circle at its center.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Use HSB color.
+ * colorMode(HSB);
+ *
+ * // Fill with pure red.
+ * fill(0, 100, 100);
+ *
+ * circle(50, 50, 25);
+ *
+ * describe('A gray square with a red circle at its center.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Use HSL color.
+ * colorMode(HSL);
+ *
+ * // Fill with pure red.
+ * fill(0, 100, 50);
+ *
+ * circle(50, 50, 25);
+ *
+ * describe('A gray square with a red circle at its center.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Draw a neutral gray background using the default color mode.
+ * background(200);
+ *
+ * // Switch to HWB color mode.
+ * // (Assuming p5.js supports HWB with a range of:
+ * // hue: 0–360, whiteness: 0–100, blackness: 0–100.)
+ * colorMode(HWB);
+ *
+ * // Set fill to pure red in HWB.
+ * // Pure red in HWB is: hue = 0°, whiteness = 0%, blackness = 0%.
+ * fill(0, 0, 0);
+ *
+ * // Draw a circle at the center.
+ * circle(50, 50, 25);
+ *
+ * describe('A gray square with a red circle at its center, drawn using HWB color mode.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Draw a neutral gray background using the default color mode.
+ * background(200);
+ *
+ * // Switch to LAB color mode.
+ * // In this mode, L typically ranges from 0 to 100 while a and b span roughly -128 to 127.
+ * colorMode(LAB);
+ *
+ * // Set fill to pure red in LAB.
+ * // The sRGB red (255, 0, 0) converts approximately to LAB as:
+ * // L = 53, a = 80, b = 67.
+ * fill(53, 80, 67);
+ *
+ * // Draw a circle at the center.
+ * circle(50, 50, 25);
+ *
+ * describe('A gray square with a red circle at its center, drawn using LAB color mode.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Draw a neutral gray background.
+ * background(200);
+ *
+ * // Switch to LCH color mode.
+ * // In LCH, colors are defined by Lightness, Chroma, and Hue (in degrees).
+ * colorMode(LCH);
+ *
+ * // Set fill to an approximation of pure red in LCH:
+ * // Lightness ≈ 53, Chroma ≈ 104, Hue ≈ 40°.
+ * fill(53, 104, 40);
+ *
+ * // Draw a circle at the center.
+ * circle(50, 50, 25);
+ *
+ * describe('A gray square with a red circle at its center, drawn using LCH color mode.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Use RGB color with values in the range 0-100.
+ * colorMode(RGB, 100);
+ *
+ * for (let x = 0; x < 100; x += 1) {
+ * for (let y = 0; y < 100; y += 1) {
+ * stroke(x, y, 0);
+ * point(x, y);
+ * }
+ * }
+ *
+ * describe(
+ * 'A diagonal green to red gradient from bottom-left to top-right with shading transitioning to black at top-left corner.'
+ * );
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Use HSB color with values in the range 0-100.
+ * colorMode(HSB, 100);
+ *
+ * for (let x = 0; x < 100; x += 1) {
+ * for (let y = 0; y < 100; y += 1) {
+ * stroke(x, y, 100);
+ * point(x, y);
+ * }
+ * }
+ *
+ * describe('A rainbow gradient from left-to-right. Brightness transitions to white at the top.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.Color object.
+ * let myColor = color(180, 175, 230);
+ * background(myColor);
+ *
+ * // Use RGB color with values in the range 0-1.
+ * colorMode(RGB, 1);
+ *
+ * // Get the red, green, and blue color components.
+ * let redValue = red(myColor);
+ * let greenValue = green(myColor);
+ * let blueValue = blue(myColor);
+ *
+ * // Round the color components for display.
+ * redValue = round(redValue, 2);
+ * greenValue = round(greenValue, 2);
+ * blueValue = round(blueValue, 2);
+ *
+ * // Display the color components.
+ * text(`Red: ${redValue}`, 10, 10, 80, 80);
+ * text(`Green: ${greenValue}`, 10, 40, 80, 80);
+ * text(`Blue: ${blueValue}`, 10, 70, 80, 80);
+ *
+ * describe('A purple canvas with the red, green, and blue decimal values of the color written on it.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(255);
+ *
+ * // Use RGB color with alpha values in the range 0-1.
+ * colorMode(RGB, 255, 255, 255, 1);
+ *
+ * noFill();
+ * strokeWeight(4);
+ * stroke(255, 0, 10, 0.3);
+ * circle(40, 40, 50);
+ * circle(50, 60, 50);
+ *
+ * describe('Two overlapping translucent pink circle outlines.');
+ * }
+ *
+ * @example
+ * let hslGraphic, lchGraphic, oklchGraphic;
+ *
+ * function setup() {
+ * createCanvas(600, 200);
+ * noLoop();
+ *
+ * // Create three graphics objects for HSL, LCH, and OKLCH color modes
+ * hslGraphic = createGraphics(200, 200);
+ * lchGraphic = createGraphics(200, 200);
+ * oklchGraphic = createGraphics(200, 200);
+ *
+ * // Draw HSL color wheel
+ * colorMode(HSL);
+ * hslGraphic.translate(100, 100);
+ * for (let i = 0; i < 1000; i++) {
+ * hslGraphic.stroke(360 / 1000 * i, 70, 50);
+ * hslGraphic.line(0, 0, hslGraphic.width / 2, 0);
+ * hslGraphic.rotate(TAU / 1000);
+ * }
+ *
+ * // Draw LCH color wheel
+ * colorMode(LCH);
+ * lchGraphic.translate(100, 100);
+ * for (let i = 0; i < 1000; i++) {
+ * lchGraphic.stroke(54, 106, 360 / 1000 * i);
+ * lchGraphic.line(0, 0, lchGraphic.width / 2, 0);
+ * lchGraphic.rotate(TAU / 1000);
+ * }
+ *
+ * // Draw OKLCH color wheel
+ * colorMode(OKLCH);
+ * oklchGraphic.translate(100, 100);
+ * for (let i = 0; i < 1000; i++) {
+ * oklchGraphic.stroke(54, 106, 360 / 1000 * i);
+ * oklchGraphic.line(0, 0, oklchGraphic.width / 2, 0);
+ * oklchGraphic.rotate(TAU / 1000);
+ * }
+ * }
+ *
+ * function draw() {
+ * // Set the styles
+ * colorMode(RGB);
+ * background(220);
+ *
+ * // Display the color wheels
+ * image(hslGraphic, 0, 0);
+ * image(lchGraphic, 200, 0);
+ * image(oklchGraphic, 400, 0);
+ * }
+ *
+ * @example
+ * // Example: Single-value (Grayscale) colors in different color modes.
+ * // The rectangle is filled with one parameter, but its final color depends
+ * // on how that parameter is interpreted by the current color mode.
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ * noStroke();
+ * noLoop();
+ * }
+ *
+ * function draw() {
+ * // Set color mode to RGB with range 0-255
+ * colorMode(RGB, 255);
+ *
+ * // Fill with single grayscale value
+ * fill(128);
+ * rect(0, 0, 100, 100);
+ *
+ * // Add text label
+ * fill(0); // Switch to black text for clarity
+ * textSize(14);
+ * text("RGB (128)", 10, 20);
+ * }
+ */
+ /**
+ * @method colorMode
+ * @param {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} mode
+ * @param {Number} max1 range for the red or hue depending on the
+ * current color mode.
+ * @param {Number} max2 range for the green or saturation depending
+ * on the current color mode.
+ * @param {Number} max3 range for the blue or brightness/lightness
+ * depending on the current color mode.
+ * @param {Number} [maxA] range for the alpha.
+ *
+ * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode.
+ */
+ /**
+ * @method colorMode
+ * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode.
+ */
+ fn.colorMode = function(mode, max1, max2, max3, maxA) {
+ // p5._validateParameters('colorMode', arguments);
+ if (
+ [
+ RGB,
+ RGBHDR,
+ HSB,
+ HSL,
+ HWB,
+ LAB,
+ LCH,
+ OKLAB,
+ OKLCH
+ ].includes(mode)
+ ) {
+ // Set color mode.
+ this._renderer.states.setValue('colorMode', mode);
+
+ // Set color maxes.
+ this._renderer.states.setValue('colorMaxes', this._renderer.states.colorMaxes.clone());
+ const maxes = this._renderer.states.colorMaxes[mode];
+ if (arguments.length === 2) {
+ maxes[0] = max1; // Red
+ maxes[1] = max1; // Green
+ maxes[2] = max1; // Blue
+ maxes[3] = max1; // Alpha
+ } else if (arguments.length === 4) {
+ maxes[0] = max1; // Red
+ maxes[1] = max2; // Green
+ maxes[2] = max3; // Blue
+ } else if (arguments.length === 5) {
+ maxes[0] = max1; // Red
+ maxes[1] = max2; // Green
+ maxes[2] = max3; // Blue
+ maxes[3] = maxA; // Alpha
+ }
+ }
+
+ return this._renderer.states.colorMode;
+ };
+
+ /**
+ * Sets the color used to fill shapes.
+ *
+ * Calling `fill(255, 165, 0)` or `fill('orange')` means all shapes drawn
+ * after the fill command will be filled with the color orange.
+ *
+ * The version of `fill()` with one parameter interprets the value one of
+ * three ways. If the parameter is a `Number`, it's interpreted as a grayscale
+ * value. If the parameter is a `String`, it's interpreted as a CSS color
+ * string. A p5.Color object can also be provided to
+ * set the fill color.
+ *
+ * The version of `fill()` with three parameters interprets them as RGB, HSB,
+ * or HSL colors, depending on the current
+ * colorMode(). The default color space is RGB,
+ * with each value in the range from 0 to 255.
+ *
+ * The version of `fill()` with four parameters interprets them as `RGBA`, `HSBA`,
+ * or `HSLA` colors, depending on the current colorMode(). The last parameter
+ * sets the alpha (transparency) value.
+ *
+ * @method fill
+ * @param {Number} v1 red value if color mode is RGB or hue value if color mode is HSB.
+ * @param {Number} v2 green value if color mode is RGB or saturation value if color mode is HSB.
+ * @param {Number} v3 blue value if color mode is RGB or brightness value if color mode is HSB.
+ * @param {Number} [alpha] alpha value, controls transparency (0 - transparent, 255 - opaque).
+ * @chainable
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // A grayscale value.
+ * fill(51);
+ * square(20, 20, 60);
+ *
+ * describe('A dark charcoal gray square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // R, G & B values.
+ * fill(255, 204, 0);
+ * square(20, 20, 60);
+ *
+ * describe('A yellow square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // R, G, B, and Alpha values.
+ * fill(255, 0, 0, 128);
+ * square(20, 20, 60);
+ *
+ * describe('A semi-transparent red square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(100);
+ *
+ * // Use HSB color.
+ * colorMode(HSB);
+ *
+ * // H, S & B values.
+ * fill(255, 204, 100);
+ * square(20, 20, 60);
+ *
+ * describe('A royal blue square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // A CSS named color.
+ * fill('red');
+ * square(20, 20, 60);
+ *
+ * describe('A red square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Three-digit hex RGB notation.
+ * fill('#fae');
+ * square(20, 20, 60);
+ *
+ * describe('A pink square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Six-digit hex RGB notation.
+ * fill('#A251FA');
+ * square(20, 20, 60);
+ *
+ * describe('A purple square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Integer RGB notation.
+ * fill('rgb(0, 255, 0)');
+ * square(20, 20, 60);
+ *
+ * describe('A bright green square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Integer RGBA notation.
+ * fill('rgba(0, 255, 0, 0.25)');
+ * square(20, 20, 60);
+ *
+ * describe('A soft green rectange with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Percentage RGB notation.
+ * fill('rgb(100%, 0%, 10%)');
+ * square(20, 20, 60);
+ *
+ * describe('A red square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Percentage RGBA notation.
+ * fill('rgba(100%, 0%, 100%, 0.5)');
+ * square(20, 20, 60);
+ *
+ * describe('A dark fuchsia square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // A p5.Color object.
+ * let c = color(0, 0, 255);
+ * fill(c);
+ * square(20, 20, 60);
+ *
+ * describe('A blue square with a black outline.');
+ * }
+ */
+ /**
+ * @method fill
+ * @param {String} value a color string.
+ * @chainable
+ */
+ /**
+ * @method fill
+ * @param {Number} gray a grayscale value.
+ * @param {Number} [alpha]
+ * @chainable
+ */
+ /**
+ * @method fill
+ * @param {Number[]} values an array containing the red, green, blue &
+ * and alpha components of the color.
+ * @chainable
+ */
+ /**
+ * @method fill
+ * @param {p5.Color} color the fill color.
+ * @chainable
+ */
+ fn.fill = function(...args) {
+ this._renderer.fill(...args);
+ return this;
+ };
+
+ /**
+ * Disables setting the fill color for shapes.
+ *
+ * Calling `noFill()` is the same as making the fill completely transparent,
+ * as in `fill(0, 0)`. If both noStroke() and
+ * `noFill()` are called, nothing will be drawn to the screen.
+ *
+ * @method noFill
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Draw the top square.
+ * square(32, 10, 35);
+ *
+ * // Draw the bottom square.
+ * noFill();
+ * square(32, 55, 35);
+ *
+ * describe('A white square on above an empty square. Both squares have black outlines.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A purple cube wireframe spinning on a black canvas.');
+ * }
+ *
+ * function draw() {
+ * background(0);
+ *
+ * // Style the box.
+ * noFill();
+ * stroke(100, 100, 240);
+ *
+ * // Rotate the coordinates.
+ * rotateX(frameCount * 0.01);
+ * rotateY(frameCount * 0.01);
+ *
+ * // Draw the box.
+ * box(45);
+ * }
+ */
+ fn.noFill = function() {
+ this._renderer.noFill();
+ return this;
+ };
+
+ /**
+ * Disables drawing points, lines, and the outlines of shapes.
+ *
+ * Calling `noStroke()` is the same as making the stroke completely transparent,
+ * as in `stroke(0, 0)`. If both `noStroke()` and
+ * noFill() are called, nothing will be drawn to the
+ * screen.
+ *
+ * @method noStroke
+ * @chainable
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * noStroke();
+ * square(20, 20, 60);
+ *
+ * describe('A white square with no outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A pink cube with no edge outlines spinning on a black canvas.');
+ * }
+ *
+ * function draw() {
+ * background(0);
+ *
+ * // Style the box.
+ * noStroke();
+ * fill(240, 150, 150);
+ *
+ * // Rotate the coordinates.
+ * rotateX(frameCount * 0.01);
+ * rotateY(frameCount * 0.01);
+ *
+ * // Draw the box.
+ * box(45);
+ * }
+ */
+ fn.noStroke = function() {
+ this._renderer.states.setValue('strokeColor', null);
+ return this;
+ };
+
+ /**
+ * Sets the color used to draw points, lines, and the outlines of shapes.
+ *
+ * Calling `stroke(255, 165, 0)` or `stroke('orange')` means all shapes drawn
+ * after calling `stroke()` will be outlined with the color orange. The way
+ * these parameters are interpreted may be changed with the
+ * colorMode() function.
+ *
+ * The version of `stroke()` with one parameter interprets the value one of
+ * three ways. If the parameter is a `Number`, it's interpreted as a grayscale
+ * value. If the parameter is a `String`, it's interpreted as a CSS color
+ * string. A p5.Color object can also be provided to
+ * set the stroke color.
+ *
+ * The version of `stroke()` with two parameters interprets the first one as a
+ * grayscale value. The second parameter sets the alpha (transparency) value.
+ *
+ * The version of `stroke()` with three parameters interprets them as RGB, HSB,
+ * or HSL colors, depending on the current `colorMode()`.
+ *
+ * The version of `stroke()` with four parameters interprets them as RGBA, HSBA,
+ * or HSLA colors, depending on the current `colorMode()`. The last parameter
+ * sets the alpha (transparency) value.
+ *
+ * @method stroke
+ * @param {Number} v1 red value if color mode is RGB or hue value if color mode is HSB.
+ * @param {Number} v2 green value if color mode is RGB or saturation value if color mode is HSB.
+ * @param {Number} v3 blue value if color mode is RGB or brightness value if color mode is HSB.
+ * @param {Number} [alpha] alpha value, controls transparency (0 - transparent, 255 - opaque).
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // A grayscale value.
+ * strokeWeight(4);
+ * stroke(51);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a dark charcoal gray outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // R, G & B values.
+ * stroke(255, 204, 0);
+ * strokeWeight(4);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a yellow outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Use HSB color.
+ * colorMode(HSB);
+ *
+ * // H, S & B values.
+ * strokeWeight(4);
+ * stroke(255, 204, 100);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a royal blue outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // A CSS named color.
+ * stroke('red');
+ * strokeWeight(4);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a red outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Three-digit hex RGB notation.
+ * stroke('#fae');
+ * strokeWeight(4);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a pink outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Six-digit hex RGB notation.
+ * stroke('#222222');
+ * strokeWeight(4);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a black outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Integer RGB notation.
+ * stroke('rgb(0, 255, 0)');
+ * strokeWeight(4);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a bright green outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Integer RGBA notation.
+ * stroke('rgba(0, 255, 0, 0.25)');
+ * strokeWeight(4);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a soft green outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Percentage RGB notation.
+ * stroke('rgb(100%, 0%, 10%)');
+ * strokeWeight(4);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a red outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Percentage RGBA notation.
+ * stroke('rgba(100%, 0%, 100%, 0.5)');
+ * strokeWeight(4);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a dark fuchsia outline.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // A p5.Color object.
+ * stroke(color(0, 0, 255));
+ * strokeWeight(4);
+ * square(20, 20, 60);
+ *
+ * describe('A white square with a blue outline.');
+ * }
+ */
+ /**
+ * @method stroke
+ * @param {String} value a color string.
+ * @chainable
+ */
+ /**
+ * @method stroke
+ * @param {Number} gray a grayscale value.
+ * @param {Number} [alpha]
+ * @chainable
+ */
+ /**
+ * @method stroke
+ * @param {Number[]} values an array containing the red, green, blue,
+ * and alpha components of the color.
+ * @chainable
+ */
+ /**
+ * @method stroke
+ * @param {p5.Color} color the stroke color.
+ * @chainable
+ */
+ fn.stroke = function(...args) {
+ this._renderer.stroke(...args);
+ return this;
+ };
+
+ /**
+ * Starts using shapes to erase parts of the canvas.
+ *
+ * All drawing that follows `erase()` will subtract from the canvas, revealing
+ * the web page underneath. The erased areas will become transparent, allowing
+ * the content behind the canvas to show through. The
+ * fill(), stroke(), and
+ * blendMode() have no effect once `erase()` is
+ * called.
+ *
+ * The `erase()` function has two optional parameters. The first parameter
+ * sets the strength of erasing by the shape's interior. A value of 0 means
+ * that no erasing will occur. A value of 255 means that the shape's interior
+ * will fully erase the content underneath. The default value is 255
+ * (full strength).
+ *
+ * The second parameter sets the strength of erasing by the shape's edge. A
+ * value of 0 means that no erasing will occur. A value of 255 means that the
+ * shape's edge will fully erase the content underneath. The default value is
+ * 255 (full strength).
+ *
+ * To cancel the erasing effect, use the noErase()
+ * function.
+ *
+ * `erase()` has no effect on drawing done with the
+ * image() and
+ * background() functions.
+ *
+ * @method erase
+ * @param {Number} [strengthFill] a number (0-255) for the strength of erasing under a shape's interior.
+ * Defaults to 255, which is full strength.
+ * @param {Number} [strengthStroke] a number (0-255) for the strength of erasing under a shape's edge.
+ * Defaults to 255, which is full strength.
+ *
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(100, 100, 250);
+ *
+ * // Draw a pink square.
+ * fill(250, 100, 100);
+ * square(20, 20, 60);
+ *
+ * // Erase a circular area.
+ * erase();
+ * circle(25, 30, 30);
+ * noErase();
+ *
+ * describe('A purple canvas with a pink square in the middle. A circle is erased from the top-left, leaving a hole.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(100, 100, 250);
+ *
+ * // Draw a pink square.
+ * fill(250, 100, 100);
+ * square(20, 20, 60);
+ *
+ * // Erase a circular area.
+ * strokeWeight(5);
+ * erase(150, 255);
+ * circle(25, 30, 30);
+ * noErase();
+ *
+ * describe('A purple canvas with a pink square in the middle. A circle at the top-left partially erases its interior and a fully erases its outline.');
+ * }
+ */
+ fn.erase = function(opacityFill = 255, opacityStroke = 255) {
+ this._renderer.erase(opacityFill, opacityStroke);
+
+ return this;
+ };
+
+ /**
+ * Ends erasing that was started with erase().
+ *
+ * The fill(), stroke(), and
+ * blendMode() settings will return to what they
+ * were prior to calling erase().
+ *
+ * @method noErase
+ * @chainable
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(235, 145, 15);
+ *
+ * // Draw the left rectangle.
+ * noStroke();
+ * fill(30, 45, 220);
+ * rect(30, 10, 10, 80);
+ *
+ * // Erase a circle.
+ * erase();
+ * circle(50, 50, 60);
+ * noErase();
+ *
+ * // Draw the right rectangle.
+ * rect(70, 10, 10, 80);
+ *
+ * describe('An orange canvas with two tall blue rectangles. A circular hole in the center erases the rectangle on the left but not the one on the right.');
+ * }
+ */
+ fn.noErase = function() {
+ this._renderer.noErase();
+ return this;
+ };
+
+ /**
+ * Sets the way colors blend when added to the canvas.
+ *
+ * By default, drawing with a solid color paints over the current pixel values
+ * on the canvas. `blendMode()` offers many options for blending colors.
+ *
+ * Shapes, images, and text can be used as sources for drawing to the canvas.
+ * A source pixel changes the color of the canvas pixel where it's drawn. The
+ * final color results from blending the source pixel's color with the canvas
+ * pixel's color. RGB color values from the source and canvas pixels are
+ * compared, added, subtracted, multiplied, and divided to create different
+ * effects. Red values with red values, greens with greens, and blues with
+ * blues.
+ *
+ * The parameter, `mode`, sets the blend mode. For example, calling
+ * `blendMode(ADD)` sets the blend mode to `ADD`. The following blend modes
+ * are available in both 2D and WebGL mode:
+ *
+ * - `BLEND`: color values from the source overwrite the canvas. This is the default mode.
+ * - `ADD`: color values from the source are added to values from the canvas.
+ * - `DARKEST`: keeps the darkest color value.
+ * - `LIGHTEST`: keeps the lightest color value.
+ * - `EXCLUSION`: similar to `DIFFERENCE` but with less contrast.
+ * - `MULTIPLY`: color values from the source are multiplied with values from the canvas. The result is always darker.
+ * - `SCREEN`: all color values are inverted, then multiplied, then inverted again. The result is always lighter. (Opposite of `MULTIPLY`)
+ * - `REPLACE`: the last source drawn completely replaces the rest of the canvas.
+ * - `REMOVE`: overlapping pixels are removed by making them completely transparent.
+ *
+ * The following blend modes are only available in 2D mode:
+ *
+ * - `DIFFERENCE`: color values from the source are subtracted from the values from the canvas. If the difference is a negative number, it's made positive.
+ * - `OVERLAY`: combines `MULTIPLY` and `SCREEN`. Dark values in the canvas get darker and light values get lighter.
+ * - `HARD_LIGHT`: combines `MULTIPLY` and `SCREEN`. Dark values in the source get darker and light values get lighter.
+ * - `SOFT_LIGHT`: a softer version of `HARD_LIGHT`.
+ * - `DODGE`: lightens light tones and increases contrast. Divides the canvas color values by the inverted color values from the source.
+ * - `BURN`: darkens dark tones and increases contrast. Divides the source color values by the inverted color values from the canvas, then inverts the result.
+ *
+ * The following blend modes are only available in WebGL mode:
+ *
+ * - `SUBTRACT`: RGB values from the source are subtracted from the values from the canvas. If the difference is a negative number, it's made positive. Alpha (transparency) values from the source and canvas are added.
+ *
+ * @method blendMode
+ * @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT)} mode blend mode to set.
+ * either BLEND, DARKEST, LIGHTEST, DIFFERENCE, MULTIPLY,
+ * EXCLUSION, SCREEN, REPLACE, OVERLAY, HARD_LIGHT,
+ * SOFT_LIGHT, DODGE, BURN, ADD, REMOVE or SUBTRACT
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Use the default blend mode.
+ * blendMode(BLEND);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A blue line and a red line form an X on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(ADD);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint magenta.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(DARKEST);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A blue line and a red line form an X on a gray background. The area where they overlap is black.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(LIGHTEST);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint magenta.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(EXCLUSION);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A yellow line and a cyan line form an X on a gray background. The area where they overlap is green.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(MULTIPLY);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A blue line and a red line form an X on a gray background. The area where they overlap is black.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(SCREEN);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint magenta.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(REPLACE);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A diagonal red line.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(REMOVE);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('The silhouette of an X is missing from a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(DIFFERENCE);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A yellow line and a cyan line form an X on a gray background. The area where they overlap is green.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(OVERLAY);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is bright magenta.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(HARD_LIGHT);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A blue line and a red line form an X on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(SOFT_LIGHT);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is violet.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(DODGE);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint violet.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(BURN);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A blue line and a red line form an X on a gray background. The area where they overlap is black.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set the blend mode.
+ * blendMode(SUBTRACT);
+ *
+ * // Style the lines.
+ * strokeWeight(30);
+ *
+ * // Draw the blue line.
+ * stroke('blue');
+ * line(25, 25, 75, 75);
+ *
+ * // Draw the red line.
+ * stroke('red');
+ * line(75, 25, 25, 75);
+ *
+ * describe('A yellow line and a turquoise line form an X on a gray background. The area where they overlap is green.');
+ * }
+ */
+ fn.blendMode = function (mode) {
+ // p5._validateParameters('blendMode', arguments);
+ if (mode === NORMAL) {
+ // Warning added 3/26/19, can be deleted in future (1.0 release?)
+ console.warn(
+ 'NORMAL has been deprecated for use in blendMode. defaulting to BLEND instead.'
+ );
+ mode = BLEND;
+ }
+ this._renderer.blendMode(mode);
+ };
+ }
+
+ if(typeof p5 !== 'undefined'){
+ setting(p5, p5.prototype);
+ }
+
+ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
+
+ function getDefaultExportFromCjs (x) {
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
+ }
+
+ var GifReader_1;
+ var GifWriter_1;
+
+ function GifWriter(buf, width, height, gopts) {
+ var p = 0;
+
+ var gopts = gopts === undefined ? { } : gopts;
+ var loop_count = gopts.loop === undefined ? null : gopts.loop;
+ var global_palette = gopts.palette === undefined ? null : gopts.palette;
+
+ if (width <= 0 || height <= 0 || width > 65535 || height > 65535)
+ throw new Error("Width/Height invalid.");
+
+ function check_palette_and_num_colors(palette) {
+ var num_colors = palette.length;
+ if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors-1)) {
+ throw new Error(
+ "Invalid code/color length, must be power of 2 and 2 .. 256.");
+ }
+ return num_colors;
+ }
+
+ // - Header.
+ buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46; // GIF
+ buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61; // 89a
+
+ // Handling of Global Color Table (palette) and background index.
+ var gp_num_colors_pow2 = 0;
+ var background = 0;
+ if (global_palette !== null) {
+ var gp_num_colors = check_palette_and_num_colors(global_palette);
+ while (gp_num_colors >>= 1) ++gp_num_colors_pow2;
+ gp_num_colors = 1 << gp_num_colors_pow2;
+ --gp_num_colors_pow2;
+ if (gopts.background !== undefined) {
+ background = gopts.background;
+ if (background >= gp_num_colors)
+ throw new Error("Background index out of range.");
+ // The GIF spec states that a background index of 0 should be ignored, so
+ // this is probably a mistake and you really want to set it to another
+ // slot in the palette. But actually in the end most browsers, etc end
+ // up ignoring this almost completely (including for dispose background).
+ if (background === 0)
+ throw new Error("Background index explicitly passed as 0.");
+ }
+ }
+
+ // - Logical Screen Descriptor.
+ // NOTE(deanm): w/h apparently ignored by implementations, but set anyway.
+ buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff;
+ buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff;
+ // NOTE: Indicates 0-bpp original color resolution (unused?).
+ buf[p++] = (global_palette !== null ? 0x80 : 0) | // Global Color Table Flag.
+ gp_num_colors_pow2; // NOTE: No sort flag (unused?).
+ buf[p++] = background; // Background Color Index.
+ buf[p++] = 0; // Pixel aspect ratio (unused?).
+
+ // - Global Color Table
+ if (global_palette !== null) {
+ for (var i = 0, il = global_palette.length; i < il; ++i) {
+ var rgb = global_palette[i];
+ buf[p++] = rgb >> 16 & 0xff;
+ buf[p++] = rgb >> 8 & 0xff;
+ buf[p++] = rgb & 0xff;
+ }
+ }
+
+ if (loop_count !== null) { // Netscape block for looping.
+ if (loop_count < 0 || loop_count > 65535)
+ throw new Error("Loop count invalid.")
+ // Extension code, label, and length.
+ buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b;
+ // NETSCAPE2.0
+ buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53;
+ buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45;
+ buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30;
+ // Sub-block
+ buf[p++] = 0x03; buf[p++] = 0x01;
+ buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff;
+ buf[p++] = 0x00; // Terminator.
+ }
+
+
+ var ended = false;
+
+ this.addFrame = function(x, y, w, h, indexed_pixels, opts) {
+ if (ended === true) { --p; ended = false; } // Un-end.
+
+ opts = opts === undefined ? { } : opts;
+
+ // TODO(deanm): Bounds check x, y. Do they need to be within the virtual
+ // canvas width/height, I imagine?
+ if (x < 0 || y < 0 || x > 65535 || y > 65535)
+ throw new Error("x/y invalid.")
+
+ if (w <= 0 || h <= 0 || w > 65535 || h > 65535)
+ throw new Error("Width/Height invalid.")
+
+ if (indexed_pixels.length < w * h)
+ throw new Error("Not enough pixels for the frame size.");
+
+ var using_local_palette = true;
+ var palette = opts.palette;
+ if (palette === undefined || palette === null) {
+ using_local_palette = false;
+ palette = global_palette;
+ }
+
+ if (palette === undefined || palette === null)
+ throw new Error("Must supply either a local or global palette.");
+
+ var num_colors = check_palette_and_num_colors(palette);
+
+ // Compute the min_code_size (power of 2), destroying num_colors.
+ var min_code_size = 0;
+ while (num_colors >>= 1) ++min_code_size;
+ num_colors = 1 << min_code_size; // Now we can easily get it back.
+
+ var delay = opts.delay === undefined ? 0 : opts.delay;
+
+ // From the spec:
+ // 0 - No disposal specified. The decoder is
+ // not required to take any action.
+ // 1 - Do not dispose. The graphic is to be left
+ // in place.
+ // 2 - Restore to background color. The area used by the
+ // graphic must be restored to the background color.
+ // 3 - Restore to previous. The decoder is required to
+ // restore the area overwritten by the graphic with
+ // what was there prior to rendering the graphic.
+ // 4-7 - To be defined.
+ // NOTE(deanm): Dispose background doesn't really work, apparently most
+ // browsers ignore the background palette index and clear to transparency.
+ var disposal = opts.disposal === undefined ? 0 : opts.disposal;
+ if (disposal < 0 || disposal > 3) // 4-7 is reserved.
+ throw new Error("Disposal out of range.");
+
+ var use_transparency = false;
+ var transparent_index = 0;
+ if (opts.transparent !== undefined && opts.transparent !== null) {
+ use_transparency = true;
+ transparent_index = opts.transparent;
+ if (transparent_index < 0 || transparent_index >= num_colors)
+ throw new Error("Transparent color index.");
+ }
+
+ if (disposal !== 0 || use_transparency || delay !== 0) {
+ // - Graphics Control Extension
+ buf[p++] = 0x21; buf[p++] = 0xf9; // Extension / Label.
+ buf[p++] = 4; // Byte size.
+
+ buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0);
+ buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff;
+ buf[p++] = transparent_index; // Transparent color index.
+ buf[p++] = 0; // Block Terminator.
+ }
+
+ // - Image Descriptor
+ buf[p++] = 0x2c; // Image Seperator.
+ buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff; // Left.
+ buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff; // Top.
+ buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff;
+ buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff;
+ // NOTE: No sort flag (unused?).
+ // TODO(deanm): Support interlace.
+ buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0;
+
+ // - Local Color Table
+ if (using_local_palette === true) {
+ for (var i = 0, il = palette.length; i < il; ++i) {
+ var rgb = palette[i];
+ buf[p++] = rgb >> 16 & 0xff;
+ buf[p++] = rgb >> 8 & 0xff;
+ buf[p++] = rgb & 0xff;
+ }
+ }
+
+ p = GifWriterOutputLZWCodeStream(
+ buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels);
+
+ return p;
+ };
+
+ this.end = function() {
+ if (ended === false) {
+ buf[p++] = 0x3b; // Trailer.
+ ended = true;
+ }
+ return p;
+ };
+
+ this.getOutputBuffer = function() { return buf; };
+ this.setOutputBuffer = function(v) { buf = v; };
+ this.getOutputBufferPosition = function() { return p; };
+ this.setOutputBufferPosition = function(v) { p = v; };
+ }
+
+ // Main compression routine, palette indexes -> LZW code stream.
+ // |index_stream| must have at least one entry.
+ function GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) {
+ buf[p++] = min_code_size;
+ var cur_subblock = p++; // Pointing at the length field.
+
+ var clear_code = 1 << min_code_size;
+ var code_mask = clear_code - 1;
+ var eoi_code = clear_code + 1;
+ var next_code = eoi_code + 1;
+
+ var cur_code_size = min_code_size + 1; // Number of bits per code.
+ var cur_shift = 0;
+ // We have at most 12-bit codes, so we should have to hold a max of 19
+ // bits here (and then we would write out).
+ var cur = 0;
+
+ function emit_bytes_to_buffer(bit_block_size) {
+ while (cur_shift >= bit_block_size) {
+ buf[p++] = cur & 0xff;
+ cur >>= 8; cur_shift -= 8;
+ if (p === cur_subblock + 256) { // Finished a subblock.
+ buf[cur_subblock] = 255;
+ cur_subblock = p++;
+ }
+ }
+ }
+
+ function emit_code(c) {
+ cur |= c << cur_shift;
+ cur_shift += cur_code_size;
+ emit_bytes_to_buffer(8);
+ }
+
+ // I am not an expert on the topic, and I don't want to write a thesis.
+ // However, it is good to outline here the basic algorithm and the few data
+ // structures and optimizations here that make this implementation fast.
+ // The basic idea behind LZW is to build a table of previously seen runs
+ // addressed by a short id (herein called output code). All data is
+ // referenced by a code, which represents one or more values from the
+ // original input stream. All input bytes can be referenced as the same
+ // value as an output code. So if you didn't want any compression, you
+ // could more or less just output the original bytes as codes (there are
+ // some details to this, but it is the idea). In order to achieve
+ // compression, values greater then the input range (codes can be up to
+ // 12-bit while input only 8-bit) represent a sequence of previously seen
+ // inputs. The decompressor is able to build the same mapping while
+ // decoding, so there is always a shared common knowledge between the
+ // encoding and decoder, which is also important for "timing" aspects like
+ // how to handle variable bit width code encoding.
+ //
+ // One obvious but very important consequence of the table system is there
+ // is always a unique id (at most 12-bits) to map the runs. 'A' might be
+ // 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc. This relationship
+ // can be used for an effecient lookup strategy for the code mapping. We
+ // need to know if a run has been seen before, and be able to map that run
+ // to the output code. Since we start with known unique ids (input bytes),
+ // and then from those build more unique ids (table entries), we can
+ // continue this chain (almost like a linked list) to always have small
+ // integer values that represent the current byte chains in the encoder.
+ // This means instead of tracking the input bytes (AAAABCD) to know our
+ // current state, we can track the table entry for AAAABC (it is guaranteed
+ // to exist by the nature of the algorithm) and the next character D.
+ // Therefor the tuple of (table_entry, byte) is guaranteed to also be
+ // unique. This allows us to create a simple lookup key for mapping input
+ // sequences to codes (table indices) without having to store or search
+ // any of the code sequences. So if 'AAAA' has a table entry of 12, the
+ // tuple of ('AAAA', K) for any input byte K will be unique, and can be our
+ // key. This leads to a integer value at most 20-bits, which can always
+ // fit in an SMI value and be used as a fast sparse array / object key.
+
+ // Output code for the current contents of the index buffer.
+ var ib_code = index_stream[0] & code_mask; // Load first input index.
+ var code_table = { }; // Key'd on our 20-bit "tuple".
+
+ emit_code(clear_code); // Spec says first code should be a clear code.
+
+ // First index already loaded, process the rest of the stream.
+ for (var i = 1, il = index_stream.length; i < il; ++i) {
+ var k = index_stream[i] & code_mask;
+ var cur_key = ib_code << 8 | k; // (prev, k) unique tuple.
+ var cur_code = code_table[cur_key]; // buffer + k.
+
+ // Check if we have to create a new code table entry.
+ if (cur_code === undefined) { // We don't have buffer + k.
+ // Emit index buffer (without k).
+ // This is an inline version of emit_code, because this is the core
+ // writing routine of the compressor (and V8 cannot inline emit_code
+ // because it is a closure here in a different context). Additionally
+ // we can call emit_byte_to_buffer less often, because we can have
+ // 30-bits (from our 31-bit signed SMI), and we know our codes will only
+ // be 12-bits, so can safely have 18-bits there without overflow.
+ // emit_code(ib_code);
+ cur |= ib_code << cur_shift;
+ cur_shift += cur_code_size;
+ while (cur_shift >= 8) {
+ buf[p++] = cur & 0xff;
+ cur >>= 8; cur_shift -= 8;
+ if (p === cur_subblock + 256) { // Finished a subblock.
+ buf[cur_subblock] = 255;
+ cur_subblock = p++;
+ }
+ }
+
+ if (next_code === 4096) { // Table full, need a clear.
+ emit_code(clear_code);
+ next_code = eoi_code + 1;
+ cur_code_size = min_code_size + 1;
+ code_table = { };
+ } else { // Table not full, insert a new entry.
+ // Increase our variable bit code sizes if necessary. This is a bit
+ // tricky as it is based on "timing" between the encoding and
+ // decoder. From the encoders perspective this should happen after
+ // we've already emitted the index buffer and are about to create the
+ // first table entry that would overflow our current code bit size.
+ if (next_code >= (1 << cur_code_size)) ++cur_code_size;
+ code_table[cur_key] = next_code++; // Insert into code table.
+ }
+
+ ib_code = k; // Index buffer to single input k.
+ } else {
+ ib_code = cur_code; // Index buffer to sequence in code table.
+ }
+ }
+
+ emit_code(ib_code); // There will still be something in the index buffer.
+ emit_code(eoi_code); // End Of Information.
+
+ // Flush / finalize the sub-blocks stream to the buffer.
+ emit_bytes_to_buffer(1);
+
+ // Finish the sub-blocks, writing out any unfinished lengths and
+ // terminating with a sub-block of length 0. If we have already started
+ // but not yet used a sub-block it can just become the terminator.
+ if (cur_subblock + 1 === p) { // Started but unused.
+ buf[cur_subblock] = 0;
+ } else { // Started and used, write length and additional terminator block.
+ buf[cur_subblock] = p - cur_subblock - 1;
+ buf[p++] = 0;
+ }
+ return p;
+ }
+
+ function GifReader(buf) {
+ var p = 0;
+
+ // - Header (GIF87a or GIF89a).
+ if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 ||
+ buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) {
+ throw new Error("Invalid GIF 87a/89a header.");
+ }
+
+ // - Logical Screen Descriptor.
+ var width = buf[p++] | buf[p++] << 8;
+ var height = buf[p++] | buf[p++] << 8;
+ var pf0 = buf[p++]; // | ${e}`); + pWriter.print(' | '); + } + pWriter.print('
| ${htmlEntry}`); + pWriter.print(' | '); + } + pWriter.print('
+ *
+ * The fourth and fifth parameters, `dw` and `dh`, are optional. They set the
+ * the width and height to draw the destination image. By default, `image()`
+ * draws the full source image at its original size.
+ *
+ * The sixth and seventh parameters, `sx` and `sy`, are also optional.
+ * These coordinates define the top left corner of a subsection to draw from
+ * the source image.
+ *
+ * The eighth and ninth parameters, `sw` and `sh`, are also optional.
+ * They define the width and height of a subsection to draw from the source
+ * image. By default, `image()` draws the full subsection that begins at
+ * `(sx, sy)` and extends to the edges of the source image.
+ *
+ * The ninth parameter, `fit`, is also optional. It enables a subsection of
+ * the source image to be drawn without affecting its aspect ratio. If
+ * `CONTAIN` is passed, the full subsection will appear within the destination
+ * rectangle. If `COVER` is passed, the subsection will completely cover the
+ * destination rectangle. This may have the effect of zooming into the
+ * subsection.
+ *
+ * The tenth and eleventh paremeters, `xAlign` and `yAlign`, are also
+ * optional. They determine how to align the fitted subsection. `xAlign` can
+ * be set to either `LEFT`, `RIGHT`, or `CENTER`. `yAlign` can be set to
+ * either `TOP`, `BOTTOM`, or `CENTER`. By default, both `xAlign` and `yAlign`
+ * are set to `CENTER`.
+ *
+ * @method image
+ * @param {p5.Image|p5.Element|p5.Texture|p5.Framebuffer|p5.FramebufferTexture|p5.Renderer|p5.Graphics} img image to display.
+ * @param {Number} x x-coordinate of the top-left corner of the image.
+ * @param {Number} y y-coordinate of the top-left corner of the image.
+ * @param {Number} [width] width to draw the image.
+ * @param {Number} [height] height to draw the image.
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/laDefense.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * background(50);
+ *
+ * // Draw the image.
+ * image(img, 10, 10);
+ *
+ * describe('An image of the underside of a white umbrella with a gridded ceiling above. The image has dark gray borders on its left and top.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/laDefense.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * background(50);
+ *
+ * // Draw the image 50x50.
+ * image(img, 0, 0, 50, 50);
+ *
+ * describe('An image of the underside of a white umbrella with a gridded ceiling above. The image is drawn in the top left corner of a dark gray square.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/laDefense.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * background(50);
+ *
+ * // Draw the center of the image.
+ * image(img, 25, 25, 50, 50, 25, 25, 50, 50);
+ *
+ * describe('An image of a gridded ceiling drawn in the center of a dark gray square.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/moonwalk.jpg');
+ * createCanvas(100, 100);
+ *
+ * background(50);
+ *
+ * // Draw the image and scale it to fit within the canvas.
+ * image(img, 0, 0, width, height, 0, 0, img.width, img.height, CONTAIN);
+ *
+ * describe('An image of an astronaut on the moon. The top and bottom borders of the image are dark gray.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/laDefense50.png');
+ *
+ * createCanvas(100, 100);
+ *
+ * background(50);
+ *
+ * // Draw the image and scale it to cover the canvas.
+ * image(img, 0, 0, width, height, 0, 0, img.width, img.height, COVER);
+ *
+ * describe('A pixelated image of the underside of a white umbrella with a gridded ceiling above.');
+ * }
+ */
+ /**
+ * @method image
+ * @param {p5.Image|p5.Element|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} img
+ * @param {Number} dx the x-coordinate of the destination
+ * rectangle in which to draw the source image
+ * @param {Number} dy the y-coordinate of the destination
+ * rectangle in which to draw the source image
+ * @param {Number} dWidth the width of the destination rectangle
+ * @param {Number} dHeight the height of the destination rectangle
+ * @param {Number} sx the x-coordinate of the subsection of the source
+ * image to draw into the destination rectangle
+ * @param {Number} sy the y-coordinate of the subsection of the source
+ * image to draw into the destination rectangle
+ * @param {Number} [sWidth] the width of the subsection of the
+ * source image to draw into the destination
+ * rectangle
+ * @param {Number} [sHeight] the height of the subsection of the
+ * source image to draw into the destination rectangle
+ * @param {(CONTAIN|COVER)} [fit] either CONTAIN or COVER
+ * @param {(LEFT|RIGHT|CENTER)} [xAlign=CENTER] either LEFT, RIGHT or CENTER default is CENTER
+ * @param {(TOP|BOTTOM|CENTER)} [yAlign=CENTER] either TOP, BOTTOM or CENTER default is CENTER
+ */
+ fn.image = function(
+ img,
+ dx,
+ dy,
+ dWidth,
+ dHeight,
+ sx,
+ sy,
+ sWidth,
+ sHeight,
+ fit,
+ xAlign,
+ yAlign
+ ) {
+ // set defaults per spec: https://goo.gl/3ykfOq
+
+ // p5._validateParameters('image', arguments);
+
+ let defW = img.width;
+ let defH = img.height;
+ yAlign = yAlign || CENTER;
+ xAlign = xAlign || CENTER;
+
+ if (img.elt) {
+ defW = defW !== undefined ? defW : img.elt.width;
+ defH = defH !== undefined ? defH : img.elt.height;
+ }
+ if (img.elt && img.elt.videoWidth && !img.canvas) {
+ // video no canvas
+ defW = defW !== undefined ? defW : img.elt.videoWidth;
+ defH = defH !== undefined ? defH : img.elt.videoHeight;
+ }
+
+ let _dx = dx;
+ let _dy = dy;
+ let _dw = dWidth || defW;
+ let _dh = dHeight || defH;
+ let _sx = sx || 0;
+ let _sy = sy || 0;
+ let _sw = sWidth !== undefined ? sWidth : defW;
+ let _sh = sHeight !== undefined ? sHeight : defH;
+
+ _sw = _sAssign(_sw, defW);
+ _sh = _sAssign(_sh, defH);
+
+ // This part needs cleanup and unit tests
+ // see issues https://github.com/processing/p5.js/issues/1741
+ // and https://github.com/processing/p5.js/issues/1673
+ let pd = 1;
+
+ if (img.elt && !img.canvas && img.elt.style.width) {
+ //if img is video and img.elt.size() has been used and
+ //no width passed to image()
+ if (img.elt.videoWidth && !dWidth) {
+ pd = img.elt.videoWidth;
+ } else {
+ //all other cases
+ pd = img.elt.width;
+ }
+ pd /= parseInt(img.elt.style.width, 10);
+ }
+
+ _sx *= pd;
+ _sy *= pd;
+ _sh *= pd;
+ _sw *= pd;
+
+ let vals = canvas.modeAdjust(
+ _dx, _dy,
+ _dw, _dh,
+ this._renderer.states.imageMode
+ );
+ vals = _imageFit(
+ fit,
+ xAlign,
+ yAlign,
+ vals.x,
+ vals.y,
+ vals.w,
+ vals.h,
+ _sx,
+ _sy,
+ _sw,
+ _sh
+ );
+
+ // tint the image if there is a tint
+ this._renderer.image(
+ img,
+ vals.sx,
+ vals.sy,
+ vals.sw,
+ vals.sh,
+ vals.dx,
+ vals.dy,
+ vals.dw,
+ vals.dh
+ );
+ };
+
+ /**
+ * Tints images using a color.
+ *
+ * The version of `tint()` with one parameter interprets it one of four ways.
+ * If the parameter is a number, it's interpreted as a grayscale value. If the
+ * parameter is a string, it's interpreted as a CSS color string. An array of
+ * `[R, G, B, A]` values or a p5.Color object can
+ * also be used to set the tint color.
+ *
+ * The version of `tint()` with two parameters uses the first one as a
+ * grayscale value and the second as an alpha value. For example, calling
+ * `tint(255, 128)` will make an image 50% transparent.
+ *
+ * The version of `tint()` with three parameters interprets them as RGB or
+ * HSB values, depending on the current
+ * colorMode(). The optional fourth parameter
+ * sets the alpha value. For example, `tint(255, 0, 0, 100)` will give images
+ * a red tint and make them transparent.
+ *
+ * @method tint
+ * @param {Number} v1 red or hue value.
+ * @param {Number} v2 green or saturation value.
+ * @param {Number} v3 blue or brightness.
+ * @param {Number} [alpha]
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/laDefense.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Left image.
+ * image(img, 0, 0);
+ *
+ * // Right image.
+ * // Tint with a CSS color string.
+ * tint('red');
+ * image(img, 50, 0);
+ *
+ * describe('Two images of an umbrella and a ceiling side-by-side. The image on the right has a red tint.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/laDefense.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Left image.
+ * image(img, 0, 0);
+ *
+ * // Right image.
+ * // Tint with RGB values.
+ * tint(255, 0, 0);
+ * image(img, 50, 0);
+ *
+ * describe('Two images of an umbrella and a ceiling side-by-side. The image on the right has a red tint.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/laDefense.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Left.
+ * image(img, 0, 0);
+ *
+ * // Right.
+ * // Tint with RGBA values.
+ * tint(255, 0, 0, 100);
+ * image(img, 50, 0);
+ *
+ * describe('Two images of an umbrella and a ceiling side-by-side. The image on the right has a transparent red tint.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/laDefense.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Left.
+ * image(img, 0, 0);
+ *
+ * // Right.
+ * // Tint with grayscale and alpha values.
+ * tint(255, 180);
+ * image(img, 50, 0);
+ *
+ * describe('Two images of an umbrella and a ceiling side-by-side. The image on the right is transparent.');
+ * }
+ */
+ /**
+ * @method tint
+ * @param {String} value CSS color string.
+ */
+ /**
+ * @method tint
+ * @param {Number} gray grayscale value.
+ * @param {Number} [alpha]
+ */
+ /**
+ * @method tint
+ * @param {Number[]} values array containing the red, green, blue &
+ * alpha components of the color.
+ */
+ /**
+ * @method tint
+ * @param {p5.Color} color the tint color
+ */
+ fn.tint = function(...args) {
+ // p5._validateParameters('tint', args);
+ const c = this.color(...args);
+ this._renderer.states.setValue('tint', c._getRGBA([255, 255, 255, 255]));
+ };
+
+ /**
+ * Removes the current tint set by tint().
+ *
+ * `noTint()` restores images to their original colors.
+ *
+ * @method noTint
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/laDefense.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Left.
+ * // Tint with a CSS color string.
+ * tint('red');
+ * image(img, 0, 0);
+ *
+ * // Right.
+ * // Remove the tint.
+ * noTint();
+ * image(img, 50, 0);
+ *
+ * describe('Two images of an umbrella and a ceiling side-by-side. The image on the left has a red tint.');
+ * }
+ */
+ fn.noTint = function() {
+ this._renderer.states.setValue('tint', null);
+ };
+
+ /**
+ * Apply the current tint color to the input image, return the resulting
+ * canvas.
+ *
+ * @private
+ * @param {p5.Image} The image to be tinted
+ * @return {canvas} The resulting tinted canvas
+ */
+ // fn._getTintedImageCanvas =
+ // p5.Renderer2D.prototype._getTintedImageCanvas;
+
+ /**
+ * Changes the location from which images are drawn when
+ * image() is called.
+ *
+ * By default, the first
+ * two parameters of image() are the x- and
+ * y-coordinates of the image's upper-left corner. The next parameters are
+ * its width and height. This is the same as calling `imageMode(CORNER)`.
+ *
+ * `imageMode(CORNERS)` also uses the first two parameters of
+ * image() as the x- and y-coordinates of the image's
+ * top-left corner. The third and fourth parameters are the coordinates of its
+ * bottom-right corner.
+ *
+ * `imageMode(CENTER)` uses the first two parameters of
+ * image() as the x- and y-coordinates of the image's
+ * center. The next parameters are its width and height.
+ *
+ * @method imageMode
+ * @param {(CORNER|CORNERS|CENTER)} mode either CORNER, CORNERS, or CENTER.
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Use CORNER mode.
+ * imageMode(CORNER);
+ *
+ * // Display the image.
+ * image(img, 10, 10, 50, 50);
+ *
+ * describe('A square image of a brick wall is drawn at the top left of a gray square.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Use CORNERS mode.
+ * imageMode(CORNERS);
+ *
+ * // Display the image.
+ * image(img, 10, 10, 90, 40);
+ *
+ * describe('An image of a brick wall is drawn on a gray square. The image is squeezed into a small rectangular area.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Use CENTER mode.
+ * imageMode(CENTER);
+ *
+ * // Display the image.
+ * image(img, 50, 50, 80, 80);
+ *
+ * describe('A square image of a brick wall is drawn on a gray square.');
+ * }
+ */
+ fn.imageMode = function(m) {
+ // p5._validateParameters('imageMode', arguments);
+ if (
+ m === CORNER ||
+ m === CORNERS ||
+ m === CENTER
+ ) {
+ this._renderer.states.setValue('imageMode', m);
+ }
+ };
+ }
+
+ if(typeof p5 !== 'undefined'){
+ loadingDisplaying(p5, p5.prototype);
+ }
+
+ /**
+ * @module Image
+ * @submodule Pixels
+ * @for p5
+ * @requires core
+ */
+
+
+ function pixels(p5, fn){
+ /**
+ * Copies a region of pixels from one image to another.
+ *
+ * The first parameter, `srcImage`, is the
+ * p5.Image object to blend.
+ *
+ * The next four parameters, `sx`, `sy`, `sw`, and `sh` determine the region
+ * to blend from the source image. `(sx, sy)` is the top-left corner of the
+ * region. `sw` and `sh` are the regions width and height.
+ *
+ * The next four parameters, `dx`, `dy`, `dw`, and `dh` determine the region
+ * of the canvas to blend into. `(dx, dy)` is the top-left corner of the
+ * region. `dw` and `dh` are the regions width and height.
+ *
+ * The tenth parameter, `blendMode`, sets the effect used to blend the images'
+ * colors. The options are `BLEND`, `DARKEST`, `LIGHTEST`, `DIFFERENCE`,
+ * `MULTIPLY`, `EXCLUSION`, `SCREEN`, `REPLACE`, `OVERLAY`, `HARD_LIGHT`,
+ * `SOFT_LIGHT`, `DODGE`, `BURN`, `ADD`, or `NORMAL`
+ *
+ * @method blend
+ * @param {p5.Image} srcImage source image.
+ * @param {Integer} sx x-coordinate of the source's upper-left corner.
+ * @param {Integer} sy y-coordinate of the source's upper-left corner.
+ * @param {Integer} sw source image width.
+ * @param {Integer} sh source image height.
+ * @param {Integer} dx x-coordinate of the destination's upper-left corner.
+ * @param {Integer} dy y-coordinate of the destination's upper-left corner.
+ * @param {Integer} dw destination image width.
+ * @param {Integer} dh destination image height.
+ * @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode the blend mode. either
+ * BLEND, DARKEST, LIGHTEST, DIFFERENCE,
+ * MULTIPLY, EXCLUSION, SCREEN, REPLACE, OVERLAY, HARD_LIGHT,
+ * SOFT_LIGHT, DODGE, BURN, ADD or NORMAL.
+ *
+ * @example
+ * let img0;
+ * let img1;
+ *
+ * async function setup() {
+ * // Load the images.
+ * img0 = await loadImage('assets/rockies.jpg');
+ * img1 = await loadImage('assets/bricks_third.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Use the mountains as the background.
+ * background(img0);
+ *
+ * // Display the bricks.
+ * image(img1, 0, 0);
+ *
+ * // Display the bricks faded into the landscape.
+ * blend(img1, 0, 0, 33, 100, 67, 0, 33, 100, LIGHTEST);
+ *
+ * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears faded on the right of the image.');
+ * }
+ *
+ * @example
+ * let img0;
+ * let img1;
+ *
+ * async function setup() {
+ * // Load the images.
+ * img0 = await loadImage('assets/rockies.jpg');
+ * img1 = await loadImage('assets/bricks_third.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Use the mountains as the background.
+ * background(img0);
+ *
+ * // Display the bricks.
+ * image(img1, 0, 0);
+ *
+ * // Display the bricks partially transparent.
+ * blend(img1, 0, 0, 33, 100, 67, 0, 33, 100, DARKEST);
+ *
+ * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears transparent on the right of the image.');
+ * }
+ *
+ * @example
+ * let img0;
+ * let img1;
+ *
+ * async function setup() {
+ * // Load the images.
+ * img0 = await loadImage('assets/rockies.jpg');
+ * img1 = await loadImage('assets/bricks_third.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Use the mountains as the background.
+ * background(img0);
+ *
+ * // Display the bricks.
+ * image(img1, 0, 0);
+ *
+ * // Display the bricks washed out into the landscape.
+ * blend(img1, 0, 0, 33, 100, 67, 0, 33, 100, ADD);
+ *
+ * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears washed out on the right of the image.');
+ * }
+ */
+ /**
+ * @method blend
+ * @param {Integer} sx
+ * @param {Integer} sy
+ * @param {Integer} sw
+ * @param {Integer} sh
+ * @param {Integer} dx
+ * @param {Integer} dy
+ * @param {Integer} dw
+ * @param {Integer} dh
+ * @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode
+ */
+ fn.blend = function(...args) {
+ // p5._validateParameters('blend', args);
+ if (this._renderer) {
+ this._renderer.blend(...args);
+ } else {
+ p5.Renderer2D.prototype.blend.apply(this, args);
+ }
+ };
+
+ /**
+ * Copies pixels from a source image to a region of the canvas.
+ *
+ * The first parameter, `srcImage`, is the
+ * p5.Image object to blend. The source image can be
+ * the canvas itself or a
+ * p5.Image object. `copy()` will scale pixels from
+ * the source region if it isn't the same size as the destination region.
+ *
+ * The next four parameters, `sx`, `sy`, `sw`, and `sh` determine the region
+ * to copy from the source image. `(sx, sy)` is the top-left corner of the
+ * region. `sw` and `sh` are the region's width and height.
+ *
+ * The next four parameters, `dx`, `dy`, `dw`, and `dh` determine the region
+ * of the canvas to copy into. `(dx, dy)` is the top-left corner of the
+ * region. `dw` and `dh` are the region's width and height.
+ *
+ * @method copy
+ * @param {p5.Image|p5.Element} srcImage source image.
+ * @param {Integer} sx x-coordinate of the source's upper-left corner.
+ * @param {Integer} sy y-coordinate of the source's upper-left corner.
+ * @param {Integer} sw source image width.
+ * @param {Integer} sh source image height.
+ * @param {Integer} dx x-coordinate of the destination's upper-left corner.
+ * @param {Integer} dy y-coordinate of the destination's upper-left corner.
+ * @param {Integer} dw destination image width.
+ * @param {Integer} dh destination image height.
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/rockies.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Use the mountains as the background.
+ * background(img);
+ *
+ * // Copy a region of pixels to another spot.
+ * copy(img, 7, 22, 10, 10, 35, 25, 50, 50);
+ *
+ * // Outline the copied region.
+ * stroke(255);
+ * noFill();
+ * square(7, 22, 10);
+ *
+ * describe('An image of a mountain landscape. A square region is outlined in white. A larger square contains a pixelated view of the outlined region.');
+ * }
+ */
+ /**
+ * @method copy
+ * @param {Integer} sx
+ * @param {Integer} sy
+ * @param {Integer} sw
+ * @param {Integer} sh
+ * @param {Integer} dx
+ * @param {Integer} dy
+ * @param {Integer} dw
+ * @param {Integer} dh
+ */
+ fn.copy = function(...args) {
+ let srcImage, sx, sy, sw, sh, dx, dy, dw, dh;
+ if (args.length === 9) {
+ srcImage = args[0];
+ sx = args[1];
+ sy = args[2];
+ sw = args[3];
+ sh = args[4];
+ dx = args[5];
+ dy = args[6];
+ dw = args[7];
+ dh = args[8];
+ } else if (args.length === 8) {
+ srcImage = this;
+ sx = args[0];
+ sy = args[1];
+ sw = args[2];
+ sh = args[3];
+ dx = args[4];
+ dy = args[5];
+ dw = args[6];
+ dh = args[7];
+ } else {
+ throw new Error('Signature not supported');
+ }
+
+ fn._copyHelper(this, srcImage, sx, sy, sw, sh, dx, dy, dw, dh);
+ };
+
+ fn._copyHelper = (
+ dstImage,
+ srcImage,
+ sx,
+ sy,
+ sw,
+ sh,
+ dx,
+ dy,
+ dw,
+ dh
+ ) => {
+ const s = srcImage.canvas.width / srcImage.width;
+ // adjust coord system for 3D when renderer
+ // ie top-left = -width/2, -height/2
+ let sxMod = 0;
+ let syMod = 0;
+ if (srcImage._renderer && srcImage._renderer.isP3D) {
+ sxMod = srcImage.width / 2;
+ syMod = srcImage.height / 2;
+ }
+ if (dstImage._renderer && dstImage._renderer.isP3D) {
+ dstImage.push();
+ dstImage.resetMatrix();
+ dstImage.noLights();
+ dstImage.blendMode(dstImage.BLEND);
+ dstImage.imageMode(dstImage.CORNER);
+ dstImage._renderer.image(
+ srcImage,
+ sx + sxMod,
+ sy + syMod,
+ sw,
+ sh,
+ dx,
+ dy,
+ dw,
+ dh
+ );
+ dstImage.pop();
+ } else {
+ dstImage.drawingContext.drawImage(
+ srcImage.canvas,
+ s * (sx + sxMod),
+ s * (sy + syMod),
+ s * sw,
+ s * sh,
+ dx,
+ dy,
+ dw,
+ dh
+ );
+ }
+ };
+
+ /**
+ * Applies an image filter to the canvas.
+ *
+ * The preset options are:
+ *
+ * `INVERT`
+ * Inverts the colors in the image. No parameter is used.
+ *
+ * `GRAY`
+ * Converts the image to grayscale. No parameter is used.
+ *
+ * `THRESHOLD`
+ * Converts the image to black and white. Pixels with a grayscale value
+ * above a given threshold are converted to white. The rest are converted to
+ * black. The threshold must be between 0.0 (black) and 1.0 (white). If no
+ * value is specified, 0.5 is used.
+ *
+ * `OPAQUE`
+ * Sets the alpha channel to entirely opaque. No parameter is used.
+ *
+ * `POSTERIZE`
+ * Limits the number of colors in the image. Each color channel is limited to
+ * the number of colors specified. Values between 2 and 255 are valid, but
+ * results are most noticeable with lower values. The default value is 4.
+ *
+ * `BLUR`
+ * Blurs the image. The level of blurring is specified by a blur radius. Larger
+ * values increase the blur. The default value is 4. A gaussian blur is used
+ * in `P2D` mode. A box blur is used in `WEBGL` mode.
+ *
+ * `ERODE`
+ * Reduces the light areas. No parameter is used.
+ *
+ * `DILATE`
+ * Increases the light areas. No parameter is used.
+ *
+ * `filter()` uses WebGL in the background by default because it's faster.
+ * This can be disabled in `P2D` mode by adding a `false` argument, as in
+ * `filter(BLUR, false)`. This may be useful to keep computation off the GPU
+ * or to work around a lack of WebGL support.
+ *
+ * In WebgL mode, `filter()` can also use custom shaders. See
+ * createFilterShader() for more
+ * information.
+ *
+ *
+ * @method filter
+ * @param {(THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|BLUR|ERODE|DILATE|BLUR)} filterType either THRESHOLD, GRAY, OPAQUE, INVERT,
+ * POSTERIZE, BLUR, ERODE, DILATE or BLUR.
+ * @param {Number} [filterParam] parameter unique to each filter.
+ * @param {Boolean} [useWebGL=true] flag to control whether to use fast
+ * WebGL filters (GPU) or original image
+ * filters (CPU); defaults to `true`.
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Apply the INVERT filter.
+ * filter(INVERT);
+ *
+ * describe('A blue brick wall.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Apply the GRAY filter.
+ * filter(GRAY);
+ *
+ * describe('A brick wall drawn in grayscale.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Apply the THRESHOLD filter.
+ * filter(THRESHOLD);
+ *
+ * describe('A brick wall drawn in black and white.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Apply the OPAQUE filter.
+ * filter(OPAQUE);
+ *
+ * describe('A red brick wall.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Apply the POSTERIZE filter.
+ * filter(POSTERIZE, 3);
+ *
+ * describe('An image of a red brick wall drawn with limited color palette.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Apply the BLUR filter.
+ * filter(BLUR, 3);
+ *
+ * describe('A blurry image of a red brick wall.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Apply the DILATE filter.
+ * filter(DILATE);
+ *
+ * describe('A red brick wall with bright lines between each brick.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Apply the ERODE filter.
+ * filter(ERODE);
+ *
+ * describe('A red brick wall with faint lines between each brick.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/bricks.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Apply the BLUR filter.
+ * // Don't use WebGL.
+ * filter(BLUR, 3, false);
+ *
+ * describe('A blurry image of a red brick wall.');
+ * }
+ */
+
+ /**
+ * @method getFilterGraphicsLayer
+ * @private
+ * @returns {p5.Graphics}
+ */
+ fn.getFilterGraphicsLayer = function() {
+ return this._renderer.getFilterGraphicsLayer();
+ };
+
+ /**
+ * @method filter
+ * @param {(THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|BLUR|ERODE|DILATE|BLUR)} filterType
+ * @param {Number} [filterParam]
+ * @param {Boolean} [useWebGL=true]
+ */
+ /**
+ * @method filter
+ * @param {p5.Shader} shaderFilter shader that's been loaded, with the
+ * frag shader using a `tex0` uniform.
+ */
+ fn.filter = function(...args) {
+ // p5._validateParameters('filter', args);
+
+ let { shader, operation, value, useWebGL } = parseFilterArgs(...args);
+
+ // when passed a shader, use it directly
+ if (this._renderer.isP3D && shader) {
+ this._renderer.filter(shader);
+ return;
+ }
+
+ // when opting out of webgl, use old pixels method
+ if (!useWebGL && !this._renderer.isP3D) {
+ if (this.canvas !== undefined) {
+ Filters.apply(this.canvas, Filters[operation], value);
+ } else {
+ Filters.apply(this.elt, Filters[operation], value);
+ }
+ return;
+ }
+
+ if(!useWebGL && this._renderer.isP3D) {
+ console.warn('filter() with useWebGL=false is not supported in WEBGL');
+ }
+
+ // when this is a webgl renderer, apply constant shader filter
+ if (this._renderer.isP3D) {
+ this._renderer.filter(operation, value);
+ }
+
+ // when this is P2D renderer, create/use hidden webgl renderer
+ else {
+
+ if (shader) {
+ this._renderer.filterRenderer.setOperation(operation, value, shader);
+ } else {
+ this._renderer.filterRenderer.setOperation(operation, value);
+ }
+
+ this._renderer.filterRenderer.applyFilter();
+ }
+ };
+
+ function parseFilterArgs(...args) {
+ // args could be:
+ // - operation, value, [useWebGL]
+ // - operation, [useWebGL]
+ // - shader
+
+ let result = {
+ shader: undefined,
+ operation: undefined,
+ value: undefined,
+ useWebGL: true
+ };
+
+ if (args[0] instanceof p5.Shader) {
+ result.shader = args[0];
+ return result;
+ }
+ else {
+ result.operation = args[0];
+ }
+
+ if (args.length > 1 && typeof args[1] === 'number') {
+ result.value = args[1];
+ }
+
+ if (args[args.length-1] === false) {
+ result.useWebGL = false;
+ }
+ return result;
+ }
+
+ /**
+ * Gets a pixel or a region of pixels from the canvas.
+ *
+ * `get()` is easy to use but it's not as fast as
+ * pixels. Use pixels
+ * to read many pixel values.
+ *
+ * The version of `get()` with no parameters returns the entire canvas.
+ *
+ * The version of `get()` with two parameters interprets them as
+ * coordinates. It returns an array with the `[R, G, B, A]` values of the
+ * pixel at the given point.
+ *
+ * The version of `get()` with four parameters interprets them as coordinates
+ * and dimensions. It returns a subsection of the canvas as a
+ * p5.Image object. The first two parameters are the
+ * coordinates for the upper-left corner of the subsection. The last two
+ * parameters are the width and height of the subsection.
+ *
+ * Use p5.Image.get() to work directly with
+ * p5.Image objects.
+ *
+ * @method get
+ * @param {Number} x x-coordinate of the pixel.
+ * @param {Number} y y-coordinate of the pixel.
+ * @param {Number} w width of the subsection to be returned.
+ * @param {Number} h height of the subsection to be returned.
+ * @return {p5.Image} subsection as a p5.Image object.
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/rockies.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Get the entire canvas.
+ * let c = get();
+ *
+ * // Display half the canvas.
+ * image(c, 50, 0);
+ *
+ * describe('Two identical mountain landscapes shown side-by-side.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/rockies.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Get the color of a pixel.
+ * let c = get(50, 90);
+ *
+ * // Style the square with the pixel's color.
+ * fill(c);
+ * noStroke();
+ *
+ * // Display the square.
+ * square(25, 25, 50);
+ *
+ * describe('A mountain landscape with an olive green square in its center.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/rockies.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0);
+ *
+ * // Get a region of the image.
+ * let c = get(0, 0, 50, 50);
+ *
+ * // Display the region.
+ * image(c, 50, 50);
+ *
+ * describe('A mountain landscape drawn on top of another mountain landscape.');
+ * }
+ */
+ /**
+ * @method get
+ * @return {p5.Image} whole canvas as a p5.Image.
+ */
+ /**
+ * @method get
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Number[]} color of the pixel at (x, y) in array format `[R, G, B, A]`.
+ */
+ fn.get = function(x, y, w, h) {
+ // p5._validateParameters('get', arguments);
+ return this._renderer.get(...arguments);
+ };
+
+ /**
+ * Loads the current value of each pixel on the canvas into the
+ * pixels array.
+ *
+ * `loadPixels()` must be called before reading from or writing to
+ * pixels.
+ *
+ * @method loadPixels
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/rockies.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0, 100, 100);
+ *
+ * // Get the pixel density.
+ * let d = pixelDensity();
+ *
+ * // Calculate the halfway index in the pixels array.
+ * let halfImage = 4 * (d * width) * (d * height / 2);
+ *
+ * // Load the pixels array.
+ * loadPixels();
+ *
+ * // Copy the top half of the canvas to the bottom.
+ * for (let i = 0; i < halfImage; i += 1) {
+ * pixels[i + halfImage] = pixels[i];
+ * }
+ *
+ * // Update the canvas.
+ * updatePixels();
+ *
+ * describe('Two identical images of mountain landscapes, one on top of the other.');
+ * }
+ */
+ fn.loadPixels = function(...args) {
+ // p5._validateParameters('loadPixels', args);
+ return this._renderer.loadPixels();
+ };
+
+ /**
+ * Sets the color of a pixel or draws an image to the canvas.
+ *
+ * `set()` is easy to use but it's not as fast as
+ * pixels. Use pixels
+ * to set many pixel values.
+ *
+ * `set()` interprets the first two parameters as x- and y-coordinates. It
+ * interprets the last parameter as a grayscale value, a `[R, G, B, A]` pixel
+ * array, a p5.Color object, or a
+ * p5.Image object. If an image is passed, the first
+ * two parameters set the coordinates for the image's upper-left corner,
+ * regardless of the current imageMode().
+ *
+ * updatePixels() must be called after using
+ * `set()` for changes to appear.
+ *
+ * @method set
+ * @param {Number} x x-coordinate of the pixel.
+ * @param {Number} y y-coordinate of the pixel.
+ * @param {Number|Number[]|Object} c grayscale value | pixel array |
+ * p5.Color object | p5.Image to copy.
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Set four pixels to black.
+ * set(30, 20, 0);
+ * set(85, 20, 0);
+ * set(85, 75, 0);
+ * set(30, 75, 0);
+ *
+ * // Update the canvas.
+ * updatePixels();
+ *
+ * describe('Four black dots arranged in a square drawn on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create a p5.Color object.
+ * let black = color(0);
+ *
+ * // Set four pixels to black.
+ * set(30, 20, black);
+ * set(85, 20, black);
+ * set(85, 75, black);
+ * set(30, 75, black);
+ *
+ * // Update the canvas.
+ * updatePixels();
+ *
+ * describe('Four black dots arranged in a square drawn on a gray background.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(255);
+ *
+ * // Draw a horizontal color gradient.
+ * for (let x = 0; x < 100; x += 1) {
+ * for (let y = 0; y < 100; y += 1) {
+ * // Calculate the grayscale value.
+ * let c = map(x, 0, 100, 0, 255);
+ *
+ * // Set the pixel using the grayscale value.
+ * set(x, y, c);
+ * }
+ * }
+ *
+ * // Update the canvas.
+ * updatePixels();
+ *
+ * describe('A horiztonal color gradient from black to white.');
+ * }
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/rockies.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Use the image to set all pixels.
+ * set(0, 0, img);
+ *
+ * // Update the canvas.
+ * updatePixels();
+ *
+ * describe('An image of a mountain landscape.');
+ * }
+ */
+ fn.set = function(x, y, imgOrCol) {
+ this._renderer.set(x, y, imgOrCol);
+ };
+
+ /**
+ * Updates the canvas with the RGBA values in the
+ * pixels array.
+ *
+ * `updatePixels()` only needs to be called after changing values in the
+ * pixels array. Such changes can be made directly
+ * after calling loadPixels() or by calling
+ * set().
+ *
+ * @method updatePixels
+ * @param {Number} [x] x-coordinate of the upper-left corner of region
+ * to update.
+ * @param {Number} [y] y-coordinate of the upper-left corner of region
+ * to update.
+ * @param {Number} [w] width of region to update.
+ * @param {Number} [h] height of region to update.
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * // Load the image.
+ * img = await loadImage('assets/rockies.jpg');
+ *
+ * createCanvas(100, 100);
+ *
+ * // Display the image.
+ * image(img, 0, 0, 100, 100);
+ *
+ * // Get the pixel density.
+ * let d = pixelDensity();
+ *
+ * // Calculate the halfway index in the pixels array.
+ * let halfImage = 4 * (d * width) * (d * height / 2);
+ *
+ * // Load the pixels array.
+ * loadPixels();
+ *
+ * // Copy the top half of the canvas to the bottom.
+ * for (let i = 0; i < halfImage; i += 1) {
+ * pixels[i + halfImage] = pixels[i];
+ * }
+ *
+ * // Update the canvas.
+ * updatePixels();
+ *
+ * describe('Two identical images of mountain landscapes, one on top of the other.');
+ * }
+ */
+ fn.updatePixels = function(x, y, w, h) {
+ // p5._validateParameters('updatePixels', arguments);
+ // graceful fail - if loadPixels() or set() has not been called, pixel
+ // array will be empty, ignore call to updatePixels()
+ if (this.pixels.length === 0) {
+ return;
+ }
+ this._renderer.updatePixels(x, y, w, h);
+ };
+
+ /**
+ * An array containing the color of each pixel on the canvas.
+ *
+ * Colors are stored as numbers representing red, green, blue, and alpha
+ * (RGBA) values. `pixels` is a one-dimensional array for performance reasons.
+ *
+ * Each pixel occupies four elements in the `pixels` array, one for each RGBA
+ * value. For example, the pixel at coordinates (0, 0) stores its RGBA values
+ * at `pixels[0]`, `pixels[1]`, `pixels[2]`, and `pixels[3]`, respectively.
+ * The next pixel at coordinates (1, 0) stores its RGBA values at `pixels[4]`,
+ * `pixels[5]`, `pixels[6]`, and `pixels[7]`. And so on. The `pixels` array
+ * for a 100×100 canvas has 100 × 100 × 4 = 40,000 elements.
+ *
+ * Some displays use several smaller pixels to set the color at a single
+ * point. The pixelDensity() function returns
+ * the pixel density of the canvas. High density displays often have a
+ * pixelDensity() of 2. On such a display, the
+ * `pixels` array for a 100×100 canvas has 200 × 200 × 4 =
+ * 160,000 elements.
+ *
+ * Accessing the RGBA values for a point on the canvas requires a little math
+ * as shown below. The loadPixels() function
+ * must be called before accessing the `pixels` array. The
+ * updatePixels() function must be called
+ * after any changes are made.
+ *
+ * @property {Number[]} pixels
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ * background(128);
+ *
+ * // Load the pixels array.
+ * loadPixels();
+ *
+ * // Set the dot's coordinates.
+ * let x = 50;
+ * let y = 50;
+ *
+ * // Get the pixel density.
+ * let d = pixelDensity();
+ *
+ * // Set the pixel(s) at the center of the canvas black.
+ * for (let i = 0; i < d; i += 1) {
+ * for (let j = 0; j < d; j += 1) {
+ * let index = 4 * ((y * d + j) * width * d + (x * d + i));
+ * // Red.
+ * pixels[index] = 0;
+ * // Green.
+ * pixels[index + 1] = 0;
+ * // Blue.
+ * pixels[index + 2] = 0;
+ * // Alpha.
+ * pixels[index + 3] = 255;
+ * }
+ * }
+ *
+ * // Update the canvas.
+ * updatePixels();
+ *
+ * describe('A black dot in the middle of a gray rectangle.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Load the pixels array.
+ * loadPixels();
+ *
+ * // Get the pixel density.
+ * let d = pixelDensity();
+ *
+ * // Calculate the halfway index in the pixels array.
+ * let halfImage = 4 * (d * width) * (d * height / 2);
+ *
+ * // Make the top half of the canvas red.
+ * for (let i = 0; i < halfImage; i += 4) {
+ * // Red.
+ * pixels[i] = 255;
+ * // Green.
+ * pixels[i + 1] = 0;
+ * // Blue.
+ * pixels[i + 2] = 0;
+ * // Alpha.
+ * pixels[i + 3] = 255;
+ * }
+ *
+ * // Update the canvas.
+ * updatePixels();
+ *
+ * describe('A red rectangle drawn above a gray rectangle.');
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * // Create a p5.Color object.
+ * let pink = color(255, 102, 204);
+ *
+ * // Load the pixels array.
+ * loadPixels();
+ *
+ * // Get the pixel density.
+ * let d = pixelDensity();
+ *
+ * // Calculate the halfway index in the pixels array.
+ * let halfImage = 4 * (d * width) * (d * height / 2);
+ *
+ * // Make the top half of the canvas red.
+ * for (let i = 0; i < halfImage; i += 4) {
+ * pixels[i] = red(pink);
+ * pixels[i + 1] = green(pink);
+ * pixels[i + 2] = blue(pink);
+ * pixels[i + 3] = alpha(pink);
+ * }
+ *
+ * // Update the canvas.
+ * updatePixels();
+ *
+ * describe('A pink rectangle drawn above a gray rectangle.');
+ * }
+ */
+ }
+
+ if(typeof p5 !== 'undefined'){
+ pixels(p5, p5.prototype);
+ }
+
+ class MatrixInterface {
+ // Private field to store the matrix
+ #matrix = null;
+ constructor(...args) {
+ if (this.constructor === MatrixInterface) {
+ throw new Error("Class is of abstract type and can't be instantiated");
+ }
+ // TODO: don't check this at runtime but still at compile time
+ /*const methods = [
+ 'add',
+ 'setElement',
+ 'reset',
+ 'set',
+ 'get',
+ 'copy',
+ 'clone',
+ 'diagonal',
+ 'row',
+ 'column',
+ 'transpose',
+ 'mult',
+ 'multiplyVec',
+ 'invert',
+ 'createSubMatrix3x3',
+ 'inverseTranspose4x4',
+ 'apply',
+ 'scale',
+ 'rotate4x4',
+ 'translate',
+ 'rotateX',
+ 'rotateY',
+ 'rotateZ',
+ 'perspective',
+ 'ortho',
+ 'multiplyVec4',
+ 'multiplyPoint',
+ 'multiplyAndNormalizePoint',
+ 'multiplyDirection',
+ 'multiplyVec3'
+ ];
+
+ methods.forEach(method => {
+ if (this[method] === undefined) {
+ throw new Error(`${method}() method must be implemented`);
+ }
+ });*/
+ }
+ }
+
+ /**
+ * @module Math
+ */
+
+
+ const isPerfectSquare = arr => {
+ const sqDimention = Math.sqrt(arr.length);
+ if (sqDimention % 1 !== 0) {
+ throw new Error('Array length must be a perfect square.');
+ }
+ return true;
+ };
+
+ let GLMAT_ARRAY_TYPE = Array;
+ let isMatrixArray = x => Array.isArray(x);
+ if (typeof Float32Array !== 'undefined') {
+ GLMAT_ARRAY_TYPE = Float32Array;
+ isMatrixArray = x => Array.isArray(x) || x instanceof Float32Array;
+ }
+
+ class Matrix extends MatrixInterface {
+ matrix;
+ #sqDimention;
+
+ constructor(...args) {
+ super(...args);
+ // This is default behavior when object
+ // instantiated using createMatrix()
+ if (isMatrixArray(args[0]) && isPerfectSquare(args[0])) {
+ const sqDimention = Math.sqrt(args[0].length);
+ this.#sqDimention = sqDimention;
+ this.matrix = GLMAT_ARRAY_TYPE.from(args[0]);
+ } else if (typeof args[0] === 'number') {
+ this.#sqDimention = Number(args[0]);
+ this.matrix = this.#createIdentityMatrix(args[0]);
+ }
+ return this;
+ }
+
+ /**
+ * Returns the 3x3 matrix if the dimensions are 3x3, otherwise returns `undefined`.
+ *
+ * This method returns the matrix if its dimensions are 3x3.
+ * If the matrix is not 3x3, it returns `undefined`.
+ *
+ * @returns {Array|undefined} The 3x3 matrix or `undefined` if the matrix is not 3x3.
+ */
+ get mat3() {
+ if (this.#sqDimention === 3) {
+ return this.matrix;
+ } else {
+ return undefined;
+ }
+ }
+
+ /**
+ * Returns the 4x4 matrix if the dimensions are 4x4, otherwise returns `undefined`.
+ *
+ * This method returns the matrix if its dimensions are 4x4.
+ * If the matrix is not 4x4, it returns `undefined`.
+ *
+ * @returns {Array|undefined} The 4x4 matrix or `undefined` if the matrix is not 4x4.
+ */
+ get mat4() {
+ if (this.#sqDimention === 4) {
+ return this.matrix;
+ } else {
+ return undefined;
+ }
+ }
+
+ /**
+ * Adds the corresponding elements of the given matrix to this matrix, if the dimentions are the same.
+ *
+ * @param {Matrix} matrix - The matrix to add to this matrix. It must have the same dimensions as this matrix.
+ * @returns {Matrix} The resulting matrix after addition.
+ * @throws {Error} If the matrices do not have the same dimensions.
+ *
+ * @example
+ * // META:norender
+ * const matrix1 = new p5.Matrix([1, 2, 3]);
+ * const matrix2 = new p5.Matrix([4, 5, 6]);
+ * matrix1.add(matrix2); // matrix1 is now [5, 7, 9]
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix1 = new p5.Matrix([1, 2, 3, 4]);
+ * const matrix2 = new p5.Matrix([5, 6, 7, 8]);
+ * matrix1.add(matrix2);
+ * console.log(matrix1.matrix); // Output: [6, 8, 10, 12]
+ * }
+ */
+ add(matrix) {
+ if (this.matrix.length !== matrix.matrix.length) {
+ throw new Error('Matrices must be of the same dimension to add.');
+ }
+ for (let i = 0; i < this.matrix.length; i++) {
+ this.matrix[i] += matrix.matrix[i];
+ }
+ return this;
+ }
+
+ /**
+ * Sets the value of a specific element in the matrix in column-major order.
+ *
+ * A matrix is stored in column-major order, meaning elements are arranged column by column.
+ * This function allows you to update or change the value of a specific element
+ * in the matrix by specifying its index in the column-major order and the new value.
+ *
+ * Parameters:
+ * - `index` (number): The position in the matrix where the value should be set.
+ * Indices start from 0 and follow column-major order.
+ * - `value` (any): The new value you want to assign to the specified element.
+ *
+ * Example:
+ * If you have the following 3x3 matrix stored in column-major order:
+ * ```
+ * [
+ * 1, 4, 7, // Column 1
+ * 2, 5, 8, // Column 2
+ * 3, 6, 9 // Column 3
+ * ]
+ * ```
+ * Calling `setElement(4, 10)` will update the element at index 4
+ * (which corresponds to row 2, column 2 in row-major order) to `10`.
+ * The updated matrix will look like this:
+ * ```
+ * [
+ * 1, 4, 7,
+ * 2, 10, 8,
+ * 3, 6, 9
+ * ]
+ * ```
+ *
+ * This function is useful for modifying specific parts of the matrix without
+ * having to recreate the entire structure.
+ *
+ * @param {Number} index - The position in the matrix where the value should be set.
+ * Must be a non-negative integer less than the length of the matrix.
+ * @param {Number} value - The new value to be assigned to the specified position in the matrix.
+ * @returns {Matrix} The current instance of the Matrix, allowing for method chaining.
+ *
+ * @example
+ * // META:norender
+ * // Assuming matrix is an instance of Matrix with initial values [1, 2, 3, 4] matrix.setElement(2, 99);
+ * // Now the matrix values are [1, 2, 99, 4]
+ * function setup() {
+ * const matrix = new p5.Matrix([1, 2, 3, 4]);
+ * matrix.setElement(2, 99);
+ * console.log(matrix.matrix); // Output: [1, 2, 99, 4]
+ * }
+ */
+ setElement(index, value) {
+ if (index >= 0 && index < this.matrix.length) {
+ this.matrix[index] = value;
+ }
+ return this;
+ }
+
+ /**
+ * Resets the current matrix to an identity matrix.
+ *
+ * This method replaces the current matrix with an identity matrix of the same dimensions.
+ * An identity matrix is a square matrix with ones on the main diagonal and zeros elsewhere.
+ * This is useful for resetting transformations or starting fresh with a clean matrix.
+ *
+ * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining.
+ *
+ * @example
+ * // META:norender
+ * // Resetting a 4x4 matrix to an identity matrix
+ * const matrix = new p5.Matrix(4);
+ * matrix.scale(2, 2, 2); // Apply some transformations
+ * console.log(matrix.matrix); // Output: Transformed matrix
+ * matrix.reset(); // Reset to identity matrix
+ * console.log(matrix.matrix); // Output: Identity matrix
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4);
+ * matrix.scale(2, 2, 2); // Apply scaling transformation
+ * console.log("Before reset:", matrix.matrix);
+ * matrix.reset(); // Reset to identity matrix
+ * console.log("After reset:", matrix.matrix);
+ * }
+ */
+ reset() {
+ this.matrix = this.#createIdentityMatrix(this.#sqDimention);
+ return this;
+ }
+
+ /**
+ * Replace the entire contents of a NxN matrix.
+ *
+ * This method allows you to replace the values of the current matrix with
+ * those from another matrix, an array, or individual arguments. The input
+ * can be a `Matrix` instance, an array of numbers, or individual numbers
+ * that match the dimensions of the current matrix. The values are copied
+ * without referencing the source object, ensuring that the original input
+ * remains unchanged.
+ *
+ * If the input dimensions do not match the current matrix, an error will
+ * be thrown to ensure consistency.
+ *
+ * @param {Matrix|Float32Array|Number[]} [inMatrix] - The input matrix, array,
+ * or individual numbers to replace the current matrix values.
+ * @returns {Matrix} The current instance of the Matrix class, allowing for
+ * method chaining.
+ *
+ * @example
+ * // META:norender
+ * // Replacing the contents of a matrix with another matrix
+ * const matrix1 = new p5.Matrix([1, 2, 3, 4]);
+ * const matrix2 = new p5.Matrix([5, 6, 7, 8]);
+ * matrix1.set(matrix2);
+ * console.log(matrix1.matrix); // Output: [5, 6, 7, 8]
+ *
+ * // Replacing the contents of a matrix with an array
+ * const matrix = new p5.Matrix([1, 2, 3, 4]);
+ * matrix.set([9, 10, 11, 12]);
+ * console.log(matrix.matrix); // Output: [9, 10, 11, 12]
+ *
+ * // Replacing the contents of a matrix with individual numbers
+ * const matrix = new p5.Matrix(4); // Creates a 4x4 identity matrix
+ * matrix.set(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
+ * console.log(matrix.matrix); // Output: [1, 2, 3, ..., 16]
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix([1, 2, 3, 4]);
+ * console.log("Before set:", matrix.matrix);
+ * matrix.set([5, 6, 7, 8]);
+ * console.log("After set:", matrix.matrix); // Output: [5, 6, 7, 8]
+ * }
+ */
+ set(inMatrix) {
+ let refArray = GLMAT_ARRAY_TYPE.from([...arguments]);
+ if (inMatrix instanceof Matrix) {
+ refArray = GLMAT_ARRAY_TYPE.from(inMatrix.matrix);
+ } else if (isMatrixArray(inMatrix)) {
+ refArray = GLMAT_ARRAY_TYPE.from(inMatrix);
+ }
+ if (refArray.length !== this.matrix.length) {
+ p5._friendlyError(
+ `Expected same dimensions values but received different ${refArray.length}.`,
+ 'p5.Matrix.set'
+ );
+ return this;
+ }
+ this.matrix = refArray;
+ return this;
+ }
+
+ /**
+ * Gets a copy of the matrix, returns a p5.Matrix object.
+ *
+ * This method creates a new instance of the `Matrix` class and copies the
+ * current matrix values into it. The returned matrix is independent of the
+ * original, meaning changes to the copy will not affect the original matrix.
+ *
+ * This is useful when you need to preserve the current state of a matrix
+ * while performing operations on a duplicate.
+ *
+ * @return {p5.Matrix} A new instance of the `Matrix` class containing the
+ * same values as the original matrix.
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const originalMatrix = new p5.Matrix([1, 2, 3, 4]);
+ * const copiedMatrix = originalMatrix.get();
+ * console.log("Original Matrix:", originalMatrix.matrix); // Output: [1, 2, 3, 4]
+ * console.log("Copied Matrix:", copiedMatrix.matrix); // Output: [1, 2, 3, 4]
+ *
+ * // Modify the copied matrix
+ * copiedMatrix.setElement(2, 99);
+ * console.log("Modified Copied Matrix:", copiedMatrix.matrix); // Output: [1, 2, 99, 4]
+ * console.log("Original Matrix remains unchanged:", originalMatrix.matrix); // Output: [1, 2, 3, 4]
+ * }
+ */
+ get() {
+ return new Matrix(this.matrix); // TODO: Pass p5
+ }
+
+ /**
+ * Return a copy of this matrix.
+ * If this matrix is 4x4, a 4x4 matrix with exactly the same entries will be
+ * generated. The same is true if this matrix is 3x3 or any NxN matrix.
+ *
+ * This method is useful when you need to preserve the current state of a matrix
+ * while performing operations on a duplicate. The returned matrix is independent
+ * of the original, meaning changes to the copy will not affect the original matrix.
+ *
+ * @return {p5.Matrix} The result matrix.
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const originalMatrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const copiedMatrix = originalMatrix.copy();
+ * console.log("Original Matrix:", originalMatrix.matrix);
+ * console.log("Copied Matrix:", copiedMatrix.matrix);
+ *
+ * // Modify the copied matrix
+ * copiedMatrix.setElement(4, 99);
+ * console.log("Modified Copied Matrix:", copiedMatrix.matrix);
+ * console.log("Original Matrix remains unchanged:", originalMatrix.matrix);
+ * }
+ */
+ copy() {
+ return new Matrix(this.matrix);
+ }
+
+ /**
+ * Creates a copy of the current matrix instance.
+ * This method is useful when you need a duplicate of the matrix
+ * without modifying the original one.
+ *
+ * @returns {Matrix} A new matrix instance that is a copy of the current matrix.
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const originalMatrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const clonedMatrix = originalMatrix.clone();
+ * console.log("Original Matrix:", originalMatrix.matrix);
+ * console.log("Cloned Matrix:", clonedMatrix.matrix);
+ *
+ * // Modify the cloned matrix
+ * clonedMatrix.setElement(4, 99);
+ * console.log("Modified Cloned Matrix:", clonedMatrix.matrix);
+ * console.log("Original Matrix remains unchanged:", originalMatrix.matrix);
+ * }
+ */
+ clone() {
+ return this.copy();
+ }
+
+ /**
+ * Returns the diagonal elements of the matrix in the form of an array.
+ * A NxN matrix will return an array of length N.
+ *
+ * This method extracts the diagonal elements of the matrix, which are the
+ * elements where the row index equals the column index. For example, in a
+ * 3x3 matrix:
+ * ```
+ * [
+ * 1, 2, 3,
+ * 4, 5, 6,
+ * 7, 8, 9
+ * ]
+ * ```
+ * The diagonal elements are [1, 5, 9].
+ *
+ * This is useful for operations that require the main diagonal of a matrix,
+ * such as calculating the trace of a matrix or verifying if a matrix is diagonal.
+ *
+ * @return {Number[]} An array obtained by arranging the diagonal elements
+ * of the matrix in ascending order of index.
+ *
+ * @example
+ * // META:norender
+ * // Extracting the diagonal elements of a matrix
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const diagonal = matrix.diagonal(); // [1, 5, 9]
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const diagonal = matrix.diagonal();
+ * console.log("Diagonal elements:", diagonal); // Output: [1, 5, 9]
+ * }
+ */
+ diagonal() {
+ const diagonal = [];
+ for (let i = 0; i < this.#sqDimention; i++) {
+ diagonal.push(this.matrix[i * (this.#sqDimention + 1)]);
+ }
+ return diagonal;
+ }
+
+ /**
+ * This function is only for 3x3 matrices A function that returns a row vector of a NxN matrix.
+ *
+ * This method extracts a specific row from the matrix and returns it as a `p5.Vector`.
+ * The row is determined by the `columnIndex` parameter, which specifies the column
+ * index of the matrix. This is useful for operations that require working with
+ * individual rows of a matrix, such as row transformations or dot products.
+ *
+ * @param {Number} columnIndex - The index of the column to extract as a row vector.
+ * Must be a non-negative integer less than the matrix dimension.
+ * @return {p5.Vector} A `p5.Vector` representing the extracted row of the matrix.
+ *
+ * @example
+ * // META:norender
+ * // Extracting a row vector from a 3x3 matrix
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const rowVector = matrix.row(1); // Returns a vector [2, 5, 8]
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const rowVector = matrix.row(1); // Extract the second row (index 1)
+ * console.log("Row Vector:", rowVector.toString()); // Output: Row Vector: [2, 5, 8]
+ * }
+ */
+ row(columnIndex) {
+ const columnVector = [];
+ for (let i = 0; i < this.#sqDimention; i++) {
+ columnVector.push(this.matrix[i * this.#sqDimention + columnIndex]);
+ }
+ return new Vector(...columnVector);
+ }
+
+ /**
+ * A function that returns a column vector of a NxN matrix.
+ *
+ * This method extracts a specific column from the matrix and returns it as a `p5.Vector`.
+ * The column is determined by the `rowIndex` parameter, which specifies the row index
+ * of the matrix. This is useful for operations that require working with individual
+ * columns of a matrix, such as column transformations or dot products.
+ *
+ * @param {Number} rowIndex - The index of the row to extract as a column vector.
+ * Must be a non-negative integer less than the matrix dimension.
+ * @return {p5.Vector} A `p5.Vector` representing the extracted column of the matrix.
+ *
+ * @example
+ * // META:norender
+ * // Extracting a column vector from a 3x3 matrix
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const columnVector = matrix.column(1); // Returns a vector [4, 5, 6]
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const columnVector = matrix.column(1); // Extract the second column (index 1)
+ * console.log("Column Vector:", columnVector.toString()); // Output: Column Vector: [4, 5, 6]
+ * }
+ */
+ column(rowIndex) {
+ const rowVector = [];
+ for (let i = 0; i < this.#sqDimention; i++) {
+ rowVector.push(this.matrix[rowIndex * this.#sqDimention + i]);
+ }
+ return new Vector(...rowVector);
+ }
+
+ /**
+ * Transposes the given matrix `a` based on the square dimension of the matrix.
+ *
+ * This method rearranges the elements of the matrix such that the rows become columns
+ * and the columns become rows. It handles matrices of different dimensions (4x4, 3x3, NxN)
+ * by delegating to specific transpose methods for each case.
+ *
+ * If no argument is provided, the method transposes the current matrix instance.
+ * If an argument is provided, it transposes the given matrix `a` and updates the current matrix.
+ *
+ * @param {Array} [a] - The matrix to be transposed. It should be a 2D array where each sub-array represents a row.
+ * If omitted, the current matrix instance is transposed.
+ * @returns {Matrix} - The current instance of the Matrix class, allowing for method chaining.
+ *
+ * @example
+ * // META:norender
+ * // Transposing a 3x3 matrix
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * matrix.transpose();
+ * console.log(matrix.matrix); // Output: [1, 4, 7, 2, 5, 8, 3, 6, 9]
+ *
+ * // Transposing a 4x4 matrix
+ * const matrix4x4 = new p5.Matrix(4);
+ * matrix4x4.transpose();
+ * console.log(matrix4x4.matrix); // Output: Transposed 4x4 identity matrix
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * console.log("Before transpose:", matrix.matrix);
+ * matrix.transpose();
+ * console.log("After transpose:", matrix.matrix); // Output: [1, 4, 7, 2, 5, 8, 3, 6, 9]
+ * }
+ */
+ transpose(a) {
+ if (this.#sqDimention === 4) {
+ return this.#transpose4x4(a);
+ } else if (this.#sqDimention === 3) {
+ return this.#transpose3x3(a);
+ } else {
+ return this.#transposeNxN(a);
+ }
+ }
+
+ /**
+ * Multiplies the current matrix with another matrix or matrix-like array.
+ *
+ * This method supports several types of input:
+ * - Another Matrix instance
+ * - A matrix-like array (must be a perfect square, e.g., 4x4 or 3x3)
+ * - Multiple arguments that form a perfect square matrix
+ *
+ * If the input is the same as the current matrix, a copy is made to avoid modifying the original matrix.
+ *
+ * The method determines the appropriate multiplication strategy based on the dimensions of the current matrix
+ * and the input matrix. It supports 3x3, 4x4, and NxN matrices.
+ *
+ * @param {Matrix|Array|...number} multMatrix - The matrix or matrix-like array to multiply with.
+ * @returns {Matrix|undefined} The resulting matrix after multiplication, or undefined if the input is invalid.
+ * @chainable
+ *
+ * @example
+ * // META:norender
+ * // Multiplying two 3x3 matrices
+ * const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]);
+ * matrix1.mult(matrix2);
+ * console.log(matrix1.matrix); // Output: [30, 24, 18, 84, 69, 54, 138, 114, 90]
+ *
+ * // Multiplying a 4x4 matrix with another 4x4 matrix
+ * const matrix4x4_1 = new p5.Matrix(4); // Identity matrix
+ * const matrix4x4_2 = new p5.Matrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]);
+ * matrix4x4_1.mult(matrix4x4_2);
+ * console.log(matrix4x4_1.matrix); // Output: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]);
+ * console.log("Before multiplication:", matrix1.matrix);
+ * matrix1.mult(matrix2);
+ * console.log("After multiplication:", matrix1.matrix); // Output: [30, 24, 18, 84, 69, 54, 138, 114, 90]
+ * }
+ */
+ mult(multMatrix) {
+ let _src;
+ if (multMatrix === this || multMatrix === this.matrix) {
+ _src = this.copy().matrix; // only need to allocate in this rare case
+ } else if (multMatrix instanceof Matrix) {
+ _src = multMatrix.matrix;
+ } else if (isMatrixArray(multMatrix) && isPerfectSquare(multMatrix)) {
+ _src = multMatrix;
+ } else if (isPerfectSquare(Array.from(arguments))) {
+ _src = Array.from(arguments);
+ } else ;
+ if (this.#sqDimention === 4 && _src.length === 16) {
+ return this.#mult4x4(_src);
+ } else if (this.#sqDimention === 3 && _src.length === 9) {
+ return this.#mult3x3(_src);
+ } else {
+ return this.#multNxN(_src);
+ }
+ }
+
+ /**
+ * Takes a vector and returns the vector resulting from multiplying to that vector by this matrix from left. This function is only for 3x3 matrices.
+ *
+ * This method applies the current 3x3 matrix to a given vector, effectively
+ * transforming the vector using the matrix. The resulting vector is returned
+ * as a new vector or stored in the provided target vector.
+ *
+ * @param {p5.Vector} multVector - The vector to which this matrix applies.
+ * @param {p5.Vector} [target] - The vector to receive the result. If not provided,
+ * a copy of the input vector will be created and returned.
+ * @return {p5.Vector} - The transformed vector after applying the matrix.
+ *
+ * @example
+ * // META:norender
+ * // Multiplying a 3x3 matrix with a vector
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const vector = new p5.Vector(1, 2, 3);
+ * const result = matrix.multiplyVec(vector);
+ * console.log(result.toString()); // Output: Transformed vector
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const vector = new p5.Vector(1, 2, 3);
+ * const result = matrix.multiplyVec(vector);
+ * console.log("Original Vector:", vector.toString()); // Output : Original Vector: [1, 2, 3]
+ * console.log("Transformed Vector:", result.toString()); // Output : Transformed Vector: [30, 36, 42]
+ * }
+ */
+ multiplyVec(multVector, target) {
+ if (target === undefined) {
+ target = multVector.copy();
+ }
+ for (let i = 0; i < this.#sqDimention; i++) {
+ target.values[i] = this.row(i).dot(multVector);
+ }
+ return target;
+ }
+
+ /**
+ * Inverts a given matrix.
+ *
+ * This method inverts a matrix based on its dimensions. Currently, it supports
+ * 3x3 and 4x4 matrices. If the matrix dimension is greater than 4, an error is thrown.
+ *
+ * For 4x4 matrices, it uses a specialized algorithm to compute the inverse.
+ * For 3x3 matrices, it uses a different algorithm optimized for smaller matrices.
+ *
+ * If the matrix is singular (non-invertible), the method will return `null`.
+ *
+ * @param {Array} a - The matrix to be inverted. It should be a 2D array representing the matrix.
+ * @returns {Array|null} - The inverted matrix, or `null` if the matrix is singular.
+ * @throws {Error} - Throws an error if the matrix dimension is greater than 4.
+ *
+ * @example
+ * // META:norender
+ * // Inverting a 3x3 matrix
+ * const matrix = new p5.Matrix([1, 2, 3, 0, 1, 4, 5, 6, 0]);
+ * const invertedMatrix = matrix.invert();
+ * console.log(invertedMatrix.matrix); // Output: Inverted 3x3 matrix
+ *
+ * // Inverting a 4x4 matrix
+ * const matrix4x4 = new p5.Matrix(4); // Identity matrix
+ * matrix4x4.scale(2, 2, 2);
+ * const invertedMatrix4x4 = matrix4x4.invert();
+ * console.log(invertedMatrix4x4.matrix); // Output: Inverted 4x4 matrix
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ *
+ * const matrix = new p5.Matrix([1, 2, 3, 0, 1, 4, 5, 6, 0]);
+ * console.log("Original Matrix:", matrix.matrix);
+ * const invertedMatrix = matrix.invert();
+ * if (invertedMatrix) {
+ * console.log("Inverted Matrix:", invertedMatrix.matrix);
+ * } else {
+ * console.log("Matrix is singular and cannot be inverted.");
+ * }
+ * }
+ */
+ invert(a) {
+ if (this.#sqDimention === 4) {
+ return this.#invert4x4(a);
+ } else if (this.#sqDimention === 3) {
+ return this.#invert3x3(a);
+ } else {
+ throw new Error(
+ 'Invert is not implemented for N>4 at the moment, we are working on it'
+ );
+ }
+ }
+
+ /**
+ * Creates a 3x3 matrix whose entries are the top left 3x3 part and returns it. This function is only for 4x4 matrices.
+ *
+ * This method extracts the top-left 3x3 portion of a 4x4 matrix and creates a new
+ * 3x3 matrix from it. This is particularly useful in 3D graphics for operations
+ * that require only the rotational or scaling components of a transformation matrix.
+ *
+ * If the current matrix is not 4x4, an error is thrown to ensure the method is used
+ * correctly. The resulting 3x3 matrix is independent of the original matrix, meaning
+ * changes to the new matrix will not affect the original.
+ *
+ * @return {p5.Matrix} A new 3x3 matrix containing the top-left portion of the original 4x4 matrix.
+ * @throws {Error} If the current matrix is not 4x4.
+ *
+ * @example
+ * // META:norender
+ * // Extracting a 3x3 submatrix from a 4x4 matrix
+ * const matrix4x4 = new p5.Matrix(4); // Creates a 4x4 identity matrix
+ * matrix4x4.scale(2, 2, 2); // Apply scaling transformation
+ * const subMatrix3x3 = matrix4x4.createSubMatrix3x3();
+ * console.log("Original 4x4 Matrix:", matrix4x4.matrix);
+ * console.log("Extracted 3x3 Submatrix:", subMatrix3x3.matrix);
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix4x4 = new p5.Matrix(4); // Creates a 4x4 identity matrix
+ * matrix4x4.scale(2, 2, 2); // Apply scaling transformation
+ * console.log("Original 4x4 Matrix:", matrix4x4.matrix);
+ *
+ * const subMatrix3x3 = matrix4x4.createSubMatrix3x3();
+ * console.log("Extracted 3x3 Submatrix:", subMatrix3x3.matrix);
+ * }
+ */
+ createSubMatrix3x3() {
+ if (this.#sqDimention === 4) {
+ const result = new Matrix(3);
+ result.mat3[0] = this.matrix[0];
+ result.mat3[1] = this.matrix[1];
+ result.mat3[2] = this.matrix[2];
+ result.mat3[3] = this.matrix[4];
+ result.mat3[4] = this.matrix[5];
+ result.mat3[5] = this.matrix[6];
+ result.mat3[6] = this.matrix[8];
+ result.mat3[7] = this.matrix[9];
+ result.mat3[8] = this.matrix[10];
+ return result;
+ } else {
+ throw new Error('Matrix dimension must be 4 to create a 3x3 submatrix.');
+ }
+ }
+
+ /**
+ * Converts a 4×4 matrix to its 3×3 inverse transpose transform.
+ * This is commonly used in MVMatrix to NMatrix conversions, particularly
+ * in 3D graphics for transforming normal vectors.
+ *
+ * This method extracts the top-left 3×3 portion of a 4×4 matrix, inverts it,
+ * and then transposes the result. If the matrix is singular (non-invertible),
+ * the resulting matrix will be zeroed out.
+ *
+ * @param {p5.Matrix} mat4 - The 4×4 matrix to be converted.
+ * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining.
+ * @throws {Error} If the current matrix is not 3×3.
+ *
+ * @example
+ * // META:norender
+ * // Converting a 4×4 matrix to its 3×3 inverse transpose
+ * const mat4 = new p5.Matrix(4); // Create a 4×4 identity matrix
+ * mat4.scale(2, 2, 2); // Apply scaling transformation
+ * const mat3 = new p5.Matrix(3); // Create a 3×3 matrix
+ * mat3.inverseTranspose4x4(mat4);
+ * console.log("Converted 3×3 Matrix:", mat3.matrix);
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const mat4 = new p5.Matrix(4); // Create a 4×4 identity matrix
+ * mat4.scale(2, 2, 2); // Apply scaling transformation
+ * console.log("Original 4×4 Matrix:", mat4.matrix);
+ *
+ * const mat3 = new p5.Matrix(3); // Create a 3×3 matrix
+ * mat3.inverseTranspose4x4(mat4);
+ * console.log("Converted 3×3 Matrix:", mat3.matrix);
+ * }
+ */
+ inverseTranspose4x4({ mat4 }) {
+ if (this.#sqDimention !== 3) {
+ throw new Error('This function only works with 3×3 matrices.');
+ } else {
+ // Convert mat4 -> mat3 by extracting the top-left 3×3 portion
+ this.matrix[0] = mat4[0];
+ this.matrix[1] = mat4[1];
+ this.matrix[2] = mat4[2];
+ this.matrix[3] = mat4[4];
+ this.matrix[4] = mat4[5];
+ this.matrix[5] = mat4[6];
+ this.matrix[6] = mat4[8];
+ this.matrix[7] = mat4[9];
+ this.matrix[8] = mat4[10];
+ }
+
+ const inverse = this.invert();
+ // Check if inversion succeeded
+ if (inverse) {
+ inverse.transpose(this.matrix);
+ } else {
+ // In case of singularity, zero out the matrix
+ for (let i = 0; i < 9; i++) {
+ this.matrix[i] = 0;
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Applies a transformation matrix to the current matrix.
+ *
+ * This method multiplies the current matrix by another matrix, which can be provided
+ * in several forms: another Matrix instance, an array representing a matrix, or as
+ * individual arguments representing the elements of a 4x4 matrix.
+ *
+ * This operation is useful for combining transformations such as translation, rotation,
+ * scaling, and perspective projection into a single matrix. By applying a transformation
+ * matrix, you can modify the current matrix to represent a new transformation.
+ *
+ * @param {Matrix|Array|number} multMatrix - The matrix to multiply with. This can be:
+ * - An instance of the Matrix class.
+ * - An array of 16 numbers representing a 4x4 matrix.
+ * - 16 individual numbers representing the elements of a 4x4 matrix.
+ * @returns {Matrix} The current matrix after applying the transformation.
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * // Assuming `matrix` is an instance of Matrix
+ * const anotherMatrix = new p5.Matrix(4);
+ * const anotherMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
+ * matrix.apply(anotherMatrix);
+ *
+ * // Applying a transformation using an array
+ * const matrixArray = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
+ * matrix.apply(matrixArray);
+ *
+ * // Applying a transformation using individual arguments
+ * matrix.apply(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ *
+ *
+ * // Create a 4x4 identity matrix
+ * const matrix = new p5.Matrix(4);
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Create a scaling transformation matrix
+ * const scalingMatrix = new p5.Matrix([2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1]);
+ *
+ * // Apply the scaling transformation
+ * matrix.apply(scalingMatrix);
+ * console.log("After Scaling Transformation:", matrix.matrix);
+ *
+ * // Apply a translation transformation using an array
+ * const translationMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5, 5, 5, 1];
+ * matrix.apply(translationMatrix);
+ * console.log("After Translation Transformation:", matrix.matrix);
+ * }
+ */
+ apply(multMatrix) {
+ let _src;
+
+ if (multMatrix === this || multMatrix === this.matrix) {
+ _src = this.copy().matrix; // only need to allocate in this rare case
+ } else if (multMatrix instanceof Matrix) {
+ _src = multMatrix.matrix;
+ } else if (isMatrixArray(multMatrix)) {
+ _src = multMatrix;
+ } else if (arguments.length === 16) {
+ _src = arguments;
+ } else {
+ return; // nothing to do.
+ }
+
+ const mat4 = this.matrix;
+
+ // each row is used for the multiplier
+ const m0 = mat4[0];
+ const m4 = mat4[4];
+ const m8 = mat4[8];
+ const m12 = mat4[12];
+ mat4[0] = _src[0] * m0 + _src[1] * m4 + _src[2] * m8 + _src[3] * m12;
+ mat4[4] = _src[4] * m0 + _src[5] * m4 + _src[6] * m8 + _src[7] * m12;
+ mat4[8] = _src[8] * m0 + _src[9] * m4 + _src[10] * m8 + _src[11] * m12;
+ mat4[12] = _src[12] * m0 + _src[13] * m4 + _src[14] * m8 + _src[15] * m12;
+
+ const m1 = mat4[1];
+ const m5 = mat4[5];
+ const m9 = mat4[9];
+ const m13 = mat4[13];
+ mat4[1] = _src[0] * m1 + _src[1] * m5 + _src[2] * m9 + _src[3] * m13;
+ mat4[5] = _src[4] * m1 + _src[5] * m5 + _src[6] * m9 + _src[7] * m13;
+ mat4[9] = _src[8] * m1 + _src[9] * m5 + _src[10] * m9 + _src[11] * m13;
+ mat4[13] = _src[12] * m1 + _src[13] * m5 + _src[14] * m9 + _src[15] * m13;
+
+ const m2 = mat4[2];
+ const m6 = mat4[6];
+ const m10 = mat4[10];
+ const m14 = mat4[14];
+ mat4[2] = _src[0] * m2 + _src[1] * m6 + _src[2] * m10 + _src[3] * m14;
+ mat4[6] = _src[4] * m2 + _src[5] * m6 + _src[6] * m10 + _src[7] * m14;
+ mat4[10] = _src[8] * m2 + _src[9] * m6 + _src[10] * m10 + _src[11] * m14;
+ mat4[14] = _src[12] * m2 + _src[13] * m6 + _src[14] * m10 + _src[15] * m14;
+
+ const m3 = mat4[3];
+ const m7 = mat4[7];
+ const m11 = mat4[11];
+ const m15 = mat4[15];
+ mat4[3] = _src[0] * m3 + _src[1] * m7 + _src[2] * m11 + _src[3] * m15;
+ mat4[7] = _src[4] * m3 + _src[5] * m7 + _src[6] * m11 + _src[7] * m15;
+ mat4[11] = _src[8] * m3 + _src[9] * m7 + _src[10] * m11 + _src[11] * m15;
+ mat4[15] = _src[12] * m3 + _src[13] * m7 + _src[14] * m11 + _src[15] * m15;
+
+ return this;
+ }
+
+ /**
+ * Scales a p5.Matrix by scalars or a vector.
+ *
+ * This method applies a scaling transformation to the current matrix.
+ * Scaling is a transformation that enlarges or shrinks objects by a scale factor
+ * along the x, y, and z axes. The scale factors can be provided as individual
+ * numbers, an array, or a `p5.Vector`.
+ *
+ * If a `p5.Vector` or an array is provided, the x, y, and z components are extracted
+ * from it. If the z component is not provided, it defaults to 1 (no scaling along the z-axis).
+ *
+ * @param {p5.Vector|Float32Array|Number[]} s - The vector or scalars to scale by.
+ * Can be a `p5.Vector`, an array, or individual numbers.
+ * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining.
+ *
+ * @example
+ * // META:norender
+ * // Scaling a matrix by individual scalars
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * matrix.scale(2, 3, 4); // Scale by 2 along x, 3 along y, and 4 along z
+ * console.log(matrix.matrix);
+ *
+ * // Scaling a matrix by a p5.Vector
+ * const scaleVector = new p5.Vector(2, 3, 4);
+ * matrix.scale(scaleVector);
+ * console.log(matrix.matrix);
+ *
+ * // Scaling a matrix by an array
+ * const scaleArray = [2, 3, 4];
+ * matrix.scale(scaleArray);
+ * console.log(matrix.matrix);
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Scale the matrix by individual scalars
+ * matrix.scale(2, 3, 4);
+ * console.log("Scaled Matrix (2, 3, 4):", matrix.matrix);
+ *
+ * // Scale the matrix by a p5.Vector
+ * const scaleVector = new p5.Vector(1.5, 2.5, 3.5);
+ * matrix.scale(scaleVector);
+ * console.log("Scaled Matrix (Vector):", matrix.matrix);
+ *
+ * // Scale the matrix by an array
+ * const scaleArray = [0.5, 0.5, 0.5];
+ * matrix.scale(scaleArray);
+ * console.log("Scaled Matrix (Array):", matrix.matrix);
+ * }
+ */
+ scale(x, y, z) {
+ if (x instanceof Vector) {
+ // x is a vector, extract the components from it.
+ y = x.y;
+ z = x.z;
+ x = x.x; // must be last
+ } else if (x instanceof Array) {
+ // x is an array, extract the components from it.
+ y = x[1];
+ z = x[2];
+ x = x[0]; // must be last
+ }
+
+ this.matrix[0] *= x;
+ this.matrix[1] *= x;
+ this.matrix[2] *= x;
+ this.matrix[3] *= x;
+ this.matrix[4] *= y;
+ this.matrix[5] *= y;
+ this.matrix[6] *= y;
+ this.matrix[7] *= y;
+ this.matrix[8] *= z;
+ this.matrix[9] *= z;
+ this.matrix[10] *= z;
+ this.matrix[11] *= z;
+
+ return this;
+ }
+
+ /**
+ * Rotate the Matrix around a specified axis by a given angle.
+ *
+ * This method applies a rotation transformation to the matrix, modifying its orientation
+ * in 3D space. The rotation is performed around the provided axis, which can be defined
+ * as a `p5.Vector` or an array of numbers representing the x, y, and z components of the axis.
+ * Rotate our Matrix around an axis by the given angle.
+ * @param {Number} a The angle of rotation in radians.
+ * Angles in radians are a measure of rotation, where 2π radians
+ * represent a full circle (360 degrees). For example:
+ * - π/2 radians = 90 degrees (quarter turn)
+ * - π radians = 180 degrees (half turn)
+ * - 2π radians = 360 degrees (full turn)
+ * Use `Math.PI` for π or `p5`'s `PI` constant if using p5.js.
+ * @param {p5.Vector|Number[]} axis The axis or axes to rotate around.
+ * This defines the direction of the rotation.
+ * - If using a `p5.Vector`, it should represent
+ * the x, y, and z components of the axis.
+ * - If using an array, it should be in the form
+ * [x, y, z], where x, y, and z are numbers.
+ * For example:
+ * - [1, 0, 0] rotates around the x-axis.
+ * - [0, 1, 0] rotates around the y-axis.
+ * - [0, 0, 1] rotates around the z-axis. *
+ * @chainable
+ * inspired by Toji's gl-matrix lib, mat4 rotation
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]
+ *
+ * // Translate the matrix by a 3D vector
+ * matrix.rotate4x4(Math.PI, [1,0,0]);
+ * console.log("After rotation of PI degrees on vector [1,0,0]:", matrix.matrix.slice().toString()); // [1,0,0,0,0,-1,1.2246468525851679e-16,0,0,-1.2246468525851679e-16,-1,0,0,0,0,1]
+ * }
+ */
+ rotate4x4(a, x, y, z) {
+ if (x instanceof Vector) {
+ // x is a vector, extract the components from it.
+ y = x.y;
+ z = x.z;
+ x = x.x; //must be last
+ } else if (x instanceof Array) {
+ // x is an array, extract the components from it.
+ y = x[1];
+ z = x[2];
+ x = x[0]; //must be last
+ }
+
+ const len = Math.sqrt(x * x + y * y + z * z);
+ x *= 1 / len;
+ y *= 1 / len;
+ z *= 1 / len;
+
+ const a00 = this.matrix[0];
+ const a01 = this.matrix[1];
+ const a02 = this.matrix[2];
+ const a03 = this.matrix[3];
+ const a10 = this.matrix[4];
+ const a11 = this.matrix[5];
+ const a12 = this.matrix[6];
+ const a13 = this.matrix[7];
+ const a20 = this.matrix[8];
+ const a21 = this.matrix[9];
+ const a22 = this.matrix[10];
+ const a23 = this.matrix[11];
+
+ //sin,cos, and tan of respective angle
+ const sA = Math.sin(a);
+ const cA = Math.cos(a);
+ const tA = 1 - cA;
+ // Construct the elements of the rotation matrix
+ const b00 = x * x * tA + cA;
+ const b01 = y * x * tA + z * sA;
+ const b02 = z * x * tA - y * sA;
+ const b10 = x * y * tA - z * sA;
+ const b11 = y * y * tA + cA;
+ const b12 = z * y * tA + x * sA;
+ const b20 = x * z * tA + y * sA;
+ const b21 = y * z * tA - x * sA;
+ const b22 = z * z * tA + cA;
+
+ // rotation-specific matrix multiplication
+ this.matrix[0] = a00 * b00 + a10 * b01 + a20 * b02;
+ this.matrix[1] = a01 * b00 + a11 * b01 + a21 * b02;
+ this.matrix[2] = a02 * b00 + a12 * b01 + a22 * b02;
+ this.matrix[3] = a03 * b00 + a13 * b01 + a23 * b02;
+ this.matrix[4] = a00 * b10 + a10 * b11 + a20 * b12;
+ this.matrix[5] = a01 * b10 + a11 * b11 + a21 * b12;
+ this.matrix[6] = a02 * b10 + a12 * b11 + a22 * b12;
+ this.matrix[7] = a03 * b10 + a13 * b11 + a23 * b12;
+ this.matrix[8] = a00 * b20 + a10 * b21 + a20 * b22;
+ this.matrix[9] = a01 * b20 + a11 * b21 + a21 * b22;
+ this.matrix[10] = a02 * b20 + a12 * b21 + a22 * b22;
+ this.matrix[11] = a03 * b20 + a13 * b21 + a23 * b22;
+
+ return this;
+ }
+
+ /**
+ * Translates the current matrix by a given vector.
+ *
+ * This method applies a translation transformation to the current matrix.
+ * Translation moves the matrix by a specified amount along the x, y, and z axes.
+ * The input vector can be a 2D or 3D vector. If the z-component is not provided,
+ * it defaults to 0, meaning no translation along the z-axis.
+ *
+ * @param {Number[]} v - A vector representing the translation. It should be an array
+ * with two or three elements: [x, y, z]. The z-component is optional.
+ * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining.
+ *
+ * @example
+ * // META:norender
+ * // Translating a matrix by a 3D vector
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * matrix.translate([10, 20, 30]); // Translate by 10 units along x, 20 along y, and 30 along z
+ * console.log(matrix.matrix);
+ *
+ * // Translating a matrix by a 2D vector
+ * matrix.translate([5, 15]); // Translate by 5 units along x and 15 along y
+ * console.log(matrix.matrix);
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]
+ *
+ * // Translate the matrix by a 3D vector
+ * matrix.translate([10, 20, 30]);
+ * console.log("After 3D Translation (10, 20, 30):", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,10,20,30,1]
+ *
+ * // Translate the matrix by a 2D vector
+ * matrix.translate([5, 15]);
+ * console.log("After 2D Translation (5, 15):", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,15,35,30,1]
+ * }
+ */
+ translate(v) {
+ const x = v[0],
+ y = v[1],
+ z = v[2] || 0;
+ this.matrix[12] +=
+ this.matrix[0] * x + this.matrix[4] * y + this.matrix[8] * z;
+ this.matrix[13] +=
+ this.matrix[1] * x + this.matrix[5] * y + this.matrix[9] * z;
+ this.matrix[14] +=
+ this.matrix[2] * x + this.matrix[6] * y + this.matrix[10] * z;
+ this.matrix[15] +=
+ this.matrix[3] * x + this.matrix[7] * y + this.matrix[11] * z;
+ return this;
+ }
+
+ /**
+ * Rotates the matrix around the X-axis by a given angle.
+ *
+ * This method modifies the current matrix to apply a rotation transformation
+ * around the X-axis. The rotation angle is specified in radians.
+ *
+ * Rotating around the X-axis means that the Y and Z coordinates of the matrix
+ * are transformed while the X coordinates remain unchanged. This is commonly
+ * used in 3D graphics to create animations or transformations along the X-axis.
+ *
+ * @param {Number} a - The angle in radians to rotate the matrix by.
+ *
+ * @example
+ * // META:norender
+ * // Rotating a matrix around the X-axis
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * matrix.rotateX(Math.PI / 4); // Rotate 45 degrees around the X-axis
+ * console.log(matrix.matrix);
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Rotate the matrix 45 degrees (PI/4 radians) around the X-axis
+ * matrix.rotateX(Math.PI / 4);
+ * console.log("After Rotation (X-axis, 45 degrees):", matrix.matrix);
+ * }
+ */
+ rotateX(a) {
+ this.rotate4x4(a, 1, 0, 0);
+ }
+
+ /**
+ * Rotates the matrix around the Y-axis by a given angle.
+ *
+ * This method modifies the current matrix to apply a rotation transformation
+ * around the Y-axis. The rotation is performed in 3D space, and the angle
+ * is specified in radians. Rotating around the Y-axis means that the X and Z
+ * coordinates of the matrix are transformed while the Y coordinates remain
+ * unchanged. This is commonly used in 3D graphics to create animations or
+ * transformations along the Y-axis.
+ *
+ * @param {Number} a - The angle in radians to rotate the matrix by. Positive
+ * values rotate the matrix counterclockwise, and negative values rotate it
+ * clockwise.
+ *
+ * @example
+ * // META:norender
+ * // Rotating a matrix around the Y-axis
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * matrix.rotateY(Math.PI / 4); // Rotate 45 degrees around the Y-axis
+ * console.log(matrix.matrix);
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Rotate the matrix 45 degrees (PI/4 radians) around the Y-axis
+ * matrix.rotateY(Math.PI / 4);
+ * console.log("After Rotation (Y-axis, 45 degrees):", matrix.matrix);
+ * }
+ */
+ rotateY(a) {
+ this.rotate4x4(a, 0, 1, 0);
+ }
+
+ /**
+ * Rotates the matrix around the Z-axis by a given angle.
+ *
+ * This method modifies the current matrix to apply a rotation transformation
+ * around the Z-axis. The rotation is performed in a 4x4 matrix context, which
+ * is commonly used in 3D graphics to handle transformations. Rotating around
+ * the Z-axis means that the X and Y coordinates of the matrix are transformed
+ * while the Z coordinates remain unchanged.
+ *
+ * @param {Number} a - The angle in radians to rotate the matrix by. Positive
+ * values rotate the matrix counterclockwise, and negative values rotate it
+ * clockwise.
+ *
+ * @returns {Matrix} The current instance of the Matrix class, allowing for
+ * method chaining.
+ *
+ * @example
+ * // META:norender
+ * // Rotating a matrix around the Z-axis
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * matrix.rotateZ(Math.PI / 4); // Rotate 45 degrees around the Z-axis
+ * console.log(matrix.matrix);
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Rotate the matrix 45 degrees (PI/4 radians) around the Z-axis
+ * matrix.rotateZ(Math.PI / 4);
+ * console.log("After Rotation (Z-axis, 45 degrees):", matrix.matrix);
+ * }
+ */
+ rotateZ(a) {
+ this.rotate4x4(a, 0, 0, 1);
+ }
+
+ /**
+ * Sets the perspective projection matrix.
+ *
+ * This method modifies the current matrix to represent a perspective projection.
+ * Perspective projection is commonly used in 3D graphics to simulate the effect
+ * of objects appearing smaller as they move further away from the camera.
+ *
+ * The perspective matrix is defined by the field of view (fovy), aspect ratio,
+ * and the near and far clipping planes. The near and far clipping planes define
+ * the range of depth that will be rendered, with anything outside this range
+ * being clipped.
+ *
+ * @param {Number} fovy - The field of view in the y direction, in radians.
+ * @param {Number} aspect - The aspect ratio of the viewport (width / height).
+ * @param {Number} near - The distance to the near clipping plane. Must be greater than 0.
+ * @param {Number} far - The distance to the far clipping plane. Must be greater than the near value.
+ * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining.
+ *
+ * @example
+ * // META:norender
+ * // Setting a perspective projection matrix
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * matrix.perspective(Math.PI / 4, 1.5, 0.1, 100); // Set perspective projection
+ * console.log(matrix.matrix);
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Set a perspective projection with a 45-degree field of view,
+ * // an aspect ratio of 1.5, and near/far clipping planes at 0.1 and 100.
+ * matrix.perspective(Math.PI / 4, 1.5, 0.1, 100);
+ * console.log("Perspective Matrix:", matrix.matrix);
+ * }
+ */
+ perspective(fovy, aspect, near, far) {
+ const f = 1.0 / Math.tan(fovy / 2),
+ nf = 1 / (near - far);
+
+ this.matrix[0] = f / aspect;
+ this.matrix[1] = 0;
+ this.matrix[2] = 0;
+ this.matrix[3] = 0;
+ this.matrix[4] = 0;
+ this.matrix[5] = f;
+ this.matrix[6] = 0;
+ this.matrix[7] = 0;
+ this.matrix[8] = 0;
+ this.matrix[9] = 0;
+ this.matrix[10] = (far + near) * nf;
+ this.matrix[11] = -1;
+ this.matrix[12] = 0;
+ this.matrix[13] = 0;
+ this.matrix[14] = 2 * far * near * nf;
+ this.matrix[15] = 0;
+
+ return this;
+ }
+
+ /**
+ * Sets this matrix to an orthographic projection matrix.
+ *
+ * An orthographic projection matrix is used to create a 2D rendering
+ * of a 3D scene by projecting points onto a plane without perspective
+ * distortion. This method modifies the current matrix to represent
+ * the orthographic projection defined by the given parameters.
+ *
+ * @param {number} left - The coordinate for the left vertical clipping plane.
+ * @param {number} right - The coordinate for the right vertical clipping plane.
+ * @param {number} bottom - The coordinate for the bottom horizontal clipping plane.
+ * @param {number} top - The coordinate for the top horizontal clipping plane.
+ * @param {number} near - The distance to the near depth clipping plane. Must be positive.
+ * @param {number} far - The distance to the far depth clipping plane. Must be positive.
+ * @chainable
+ * @returns {Matrix} The current matrix instance, updated with the orthographic projection.
+ *
+ * @example
+ * // META:norender
+ * // Example using p5.js to demonstrate orthographic projection
+ * function setup() {
+ * let orthoMatrix = new p5.Matrix(4);
+ * console.log(orthoMatrix.matrix.toString()) // Output: 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1
+ * orthoMatrix.ortho(-200, 200, -200, 200, 0.1, 1000);
+ * console.log(orthoMatrix.matrix.toString()) // Output: [24 0.004999999888241291,0,0,0,0,0.004999999888241291,0,0,0,0,-0.0020002000965178013,0,0,0,-1.0002000331878662,1]
+ * applyMatrix(
+ * orthoMatrix.mat4[0], orthoMatrix.mat4[1], orthoMatrix.mat4[2], orthoMatrix.mat4[3],
+ * orthoMatrix.mat4[4], orthoMatrix.mat4[5], orthoMatrix.mat4[6], orthoMatrix.mat4[7],
+ * orthoMatrix.mat4[8], orthoMatrix.mat4[9], orthoMatrix.mat4[10], orthoMatrix.mat4[11],
+ * orthoMatrix.mat4[12], orthoMatrix.mat4[13], orthoMatrix.mat4[14], orthoMatrix.mat4[15]
+ * );
+ * console.log(orthoMatrix.matrix.toString()) // Output: [31 0.004999999888241291,0,0,0,0,0.004999999888241291,0,0,0,0,-0.0020002000965178013,0,0,0,-1.0002000331878662,1]
+ * }
+ */
+ ortho(left, right, bottom, top, near, far) {
+ const lr = 1 / (left - right),
+ bt = 1 / (bottom - top),
+ nf = 1 / (near - far);
+ this.matrix[0] = -2 * lr;
+ this.matrix[1] = 0;
+ this.matrix[2] = 0;
+ this.matrix[3] = 0;
+ this.matrix[4] = 0;
+ this.matrix[5] = -2 * bt;
+ this.matrix[6] = 0;
+ this.matrix[7] = 0;
+ this.matrix[8] = 0;
+ this.matrix[9] = 0;
+ this.matrix[10] = 2 * nf;
+ this.matrix[11] = 0;
+ this.matrix[12] = (left + right) * lr;
+ this.matrix[13] = (top + bottom) * bt;
+ this.matrix[14] = (far + near) * nf;
+ this.matrix[15] = 1;
+
+ return this;
+ }
+
+ /**
+ * Applies a matrix to a vector with x, y, z, w components and returns the result as an array.
+ *
+ * This method multiplies the current matrix by a 4D vector (x, y, z, w) and computes the resulting vector.
+ * It is commonly used in 3D graphics for transformations such as translation, rotation, scaling, and perspective projection.
+ *
+ * The resulting vector is returned as an array of four numbers, representing the transformed x, y, z, and w components.
+ *
+ * @param {Number} x - The x component of the vector.
+ * @param {Number} y - The y component of the vector.
+ * @param {Number} z - The z component of the vector.
+ * @param {Number} w - The w component of the vector.
+ * @returns {Number[]} An array containing the transformed [x, y, z, w] components.
+ *
+ * @example
+ * // META:norender
+ * // Applying a matrix to a 4D vector
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * const result = matrix.multiplyVec4(1, 2, 3, 1); // Transform the vector [1, 2, 3, 1]
+ * console.log(result); // Output: [1, 2, 3, 1] (unchanged for identity matrix)
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Apply the matrix to a 4D vector
+ * const result = matrix.multiplyVec4(1, 2, 3, 1);
+ * console.log("Transformed Vector:", result); // Output: [1, 2, 3, 1]
+ *
+ * // Modify the matrix (e.g., apply a translation)
+ * matrix.translate([5, 5, 5]);
+ * console.log("Modified Matrix:", matrix.matrix);
+ *
+ * // Apply the modified matrix to the same vector
+ * const transformedResult = matrix.multiplyVec4(1, 2, 3, 1);
+ * console.log("Transformed Vector after Translation:", transformedResult); // Output: [6, 7, 8, 1]
+ * }
+ */
+ multiplyVec4(x, y, z, w) {
+ const result = new Array(4);
+ const m = this.matrix;
+
+ result[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
+ result[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
+ result[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w;
+ result[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;
+
+ return result;
+ }
+
+ /**
+ * Applies a matrix to a vector. The fourth component is set to 1.
+ * Returns a vector consisting of the first
+ * through third components of the result.
+ *
+ * This method multiplies the current matrix by a 4D vector (x, y, z, 1),
+ * effectively transforming the vector using the matrix. The resulting
+ * vector is returned as a new `p5.Vector` instance.
+ *
+ * This is useful for applying transformations such as translation,
+ * rotation, scaling, or perspective projection to a point in 3D space.
+ *
+ * @param {p5.Vector} vector - The input vector to transform. It should
+ * have x, y, and z components.
+ * @return {p5.Vector} A new `p5.Vector` instance representing the transformed point.
+ *
+ * @example
+ * // META:norender
+ * // Applying a matrix to a 3D point
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * const point = new p5.Vector(1, 2, 3); // Define a 3D point
+ * const transformedPoint = matrix.multiplyPoint(point);
+ * console.log(transformedPoint.toString()); // Output: [1, 2, 3] (unchanged for identity matrix)
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Define a 3D point
+ * const point = new p5.Vector(1, 2, 3);
+ * console.log("Original Point:", point.toString());
+ *
+ * // Apply the matrix to the point
+ * const transformedPoint = matrix.multiplyPoint(point);
+ * console.log("Transformed Point:", transformedPoint.toString());
+ *
+ * // Modify the matrix (e.g., apply a translation)
+ * matrix.translate([5, 5, 5]);
+ * console.log("Modified Matrix:", matrix.matrix);
+ *
+ * // Apply the modified matrix to the same point
+ * const translatedPoint = matrix.multiplyPoint(point);
+ * console.log("Translated Point:", translatedPoint.toString()); // Output: [6, 7, 8]
+ * }
+ */
+ multiplyPoint({ x, y, z }) {
+ const array = this.multiplyVec4(x, y, z, 1);
+ return new Vector(array[0], array[1], array[2]);
+ }
+
+ /**
+ * Applies a matrix to a vector.
+ * The fourth component is set to 1.
+ * Returns the result of dividing the 1st to 3rd components
+ * of the result by the 4th component as a vector.
+ *
+ * This method multiplies the current matrix by a 4D vector (x, y, z, 1),
+ * effectively transforming the vector using the matrix. The resulting
+ * vector is normalized by dividing its x, y, and z components by the w component.
+ * This is useful for applying transformations such as perspective projection
+ * to a point in 3D space.
+ *
+ * @param {p5.Vector} vector - The input vector to transform. It should
+ * have x, y, and z components.
+ * @return {p5.Vector} A new `p5.Vector` instance representing the transformed and normalized point.
+ *
+ * @example
+ * // META:norender
+ * // Applying a matrix to a 3D point and normalizing it
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * const point = new p5.Vector(1, 2, 3); // Define a 3D point
+ * const transformedPoint = matrix.multiplyAndNormalizePoint(point);
+ * console.log(transformedPoint.toString()); // Output: [1, 2, 3] (unchanged for identity matrix)
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Define a 3D point
+ * const point = new p5.Vector(1, 2, 3);
+ * console.log("Original Point:", point.toString());
+ *
+ * // Apply the matrix to the point and normalize it
+ * const transformedPoint = matrix.multiplyAndNormalizePoint(point);
+ * console.log("Transformed and Normalized Point:", transformedPoint.toString());
+ *
+ * // Modify the matrix (e.g., apply a perspective transformation)
+ * matrix.perspective(Math.PI / 4, 1.5, 0.1, 100);
+ * console.log("Modified Matrix (Perspective):", matrix.matrix);
+ *
+ * // Apply the modified matrix to the same point
+ * const perspectivePoint = matrix.multiplyAndNormalizePoint(point);
+ * console.log("Point after Perspective Transformation:", perspectivePoint.toString());
+ * }
+ */
+ multiplyAndNormalizePoint({ x, y, z }) {
+ const array = this.multiplyVec4(x, y, z, 1);
+ array[0] /= array[3];
+ array[1] /= array[3];
+ array[2] /= array[3];
+ return new Vector(array[0], array[1], array[2]);
+ }
+
+ /**
+ * Applies a matrix to a vector.
+ * The fourth component is set to 0.
+ * Returns a vector consisting of the first
+ * through third components of the result.
+ *
+ * This method multiplies the current matrix by a 4D vector (x, y, z, 0),
+ * effectively transforming the direction vector using the matrix. The resulting
+ * vector is returned as a new `p5.Vector` instance. This is particularly useful
+ * for transforming direction vectors (e.g., normals) without applying translation.
+ *
+ * @param {p5.Vector} vector - The input vector to transform. It should
+ * have x, y, and z components.
+ * @return {p5.Vector} A new `p5.Vector` instance representing the transformed direction.
+ *
+ * @example
+ * // META:norender
+ * // Applying a matrix to a direction vector
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * const direction = new p5.Vector(1, 0, 0); // Define a direction vector
+ * const transformedDirection = matrix.multiplyDirection(direction);
+ * console.log(transformedDirection.toString()); // Output: [1, 0, 0] (unchanged for identity matrix)
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Define a direction vector
+ * const direction = new p5.Vector(1, 0, 0);
+ * console.log("Original Direction:", direction.toString());
+ *
+ * // Apply the matrix to the direction vector
+ * const transformedDirection = matrix.multiplyDirection(direction);
+ * console.log("Transformed Direction:", transformedDirection.toString());
+ *
+ * // Modify the matrix (e.g., apply a rotation)
+ * matrix.rotateY(Math.PI / 4); // Rotate 45 degrees around the Y-axis
+ * console.log("Modified Matrix (Rotation):", matrix.matrix);
+ *
+ * // Apply the modified matrix to the same direction vector
+ * const rotatedDirection = matrix.multiplyDirection(direction);
+ * console.log("Rotated Direction:", rotatedDirection.toString()); // Output: Rotated vector
+ * }
+ */
+ multiplyDirection({ x, y, z }) {
+ const array = this.multiplyVec4(x, y, z, 0);
+ return new Vector(array[0], array[1], array[2]);
+ }
+
+ /**
+ * Takes a vector and returns the vector resulting from multiplying. This function is only for 3x3 matrices.
+ * that vector by this matrix from the left.
+ *
+ * This method applies the current 3x3 matrix to a given vector, effectively
+ * transforming the vector using the matrix. The resulting vector is returned
+ * as a new vector or stored in the provided target vector.
+ *
+ * This is useful for operations such as transforming points or directions
+ * in 2D or 3D space using a 3x3 transformation matrix.
+ *
+ * @param {p5.Vector} multVector - The vector to which this matrix applies.
+ * @param {p5.Vector} [target] - The vector to receive the result. If not provided,
+ * a copy of the input vector will be created and returned.
+ * @return {p5.Vector} - The transformed vector after applying the matrix.
+ *
+ * @example
+ * // META:norender
+ * // Multiplying a 3x3 matrix with a vector
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const vector = new p5.Vector(1, 2, 3);
+ * const result = matrix.multiplyVec3(vector);
+ * console.log(result.toString()); // Output: Transformed vector
+ *
+ * @example
+ * // META:norender
+ * function setup() {
+ * // Create a 3x3 matrix
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * console.log("Original Matrix:", matrix.matrix);
+ *
+ * // Define a vector
+ * const vector = new p5.Vector(1, 2, 3);
+ * console.log("Original Vector:", vector.toString()); // Output: [1, 2, 3]
+ *
+ * // Apply the matrix to the vector
+ * const transformedVector = matrix.multiplyVec3(vector);
+ * console.log("Transformed Vector:", transformedVector.toString()); // Output: [30, 36, 42]
+ *
+ * // Modify the matrix (e.g., apply a scaling transformation)
+ * matrix.scale(2, 2, 2);
+ * console.log("Modified Matrix (Scaling):", matrix.matrix); // Output: [2, 4, 6, 8, 10, 12, 14, 16, 18]
+ *
+ * // Apply the modified matrix to the same vector
+ * const scaledVector = matrix.multiplyVec3(vector);
+ * console.log("Scaled Vector:", scaledVector.toString()); // Output: [60, 72, 84]
+ * }
+ */
+ multiplyVec3(multVector, target) {
+ if (target === undefined) {
+ target = multVector.copy();
+ }
+ target.x = this.row(0).dot(multVector);
+ target.y = this.row(1).dot(multVector);
+ target.z = this.row(2).dot(multVector);
+ return target;
+ }
+
+ // ====================
+ // PRIVATE
+ /**
+ * Creates identity matrix
+ * This method updates the current matrix with the result of the multiplication.
+ *
+ * @private
+ */
+ #createIdentityMatrix(dimension) {
+ // This it to prevent loops in the most common 3x3 and 4x4 cases
+ // TODO: check performance if it actually helps
+ if (dimension === 3)
+ return new GLMAT_ARRAY_TYPE([1, 0, 0, 0, 1, 0, 0, 0, 1]);
+ if (dimension === 4)
+ return new GLMAT_ARRAY_TYPE([
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]);
+ const identityMatrix = new GLMAT_ARRAY_TYPE(dimension * dimension).fill(0);
+ for (let i = 0; i < dimension; i++) {
+ identityMatrix[i * dimension + i] = 1;
+ }
+ return identityMatrix;
+ }
+
+ /**
+ * Multiplies the current 4x4 matrix with another 4x4 matrix.
+ * This method updates the current matrix with the result of the multiplication.
+ *
+ * @private
+ * @param {number[]} _src - A 16-element array representing the 4x4 matrix to multiply with.
+ *
+ * @returns {this} The current instance with the updated matrix.
+ *
+ * @example
+ * // META:norender
+ * // Assuming `matrix` is an instance of the Matrix class
+ * const srcMatrix = [
+ * 1, 0, 0, 0,
+ * 0, 1, 0, 0,
+ * 0, 0, 1, 0,
+ * 0, 0, 0, 1
+ * ];
+ * matrix.#mult4x4(srcMatrix);
+ */
+ #mult4x4(_src) {
+ // each row is used for the multiplier
+ let b0 = this.matrix[0],
+ b1 = this.matrix[1],
+ b2 = this.matrix[2],
+ b3 = this.matrix[3];
+ this.matrix[0] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12];
+ this.matrix[1] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13];
+ this.matrix[2] =
+ b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14];
+ this.matrix[3] =
+ b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15];
+
+ b0 = this.matrix[4];
+ b1 = this.matrix[5];
+ b2 = this.matrix[6];
+ b3 = this.matrix[7];
+ this.matrix[4] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12];
+ this.matrix[5] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13];
+ this.matrix[6] =
+ b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14];
+ this.matrix[7] =
+ b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15];
+
+ b0 = this.matrix[8];
+ b1 = this.matrix[9];
+ b2 = this.matrix[10];
+ b3 = this.matrix[11];
+ this.matrix[8] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12];
+ this.matrix[9] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13];
+ this.matrix[10] =
+ b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14];
+ this.matrix[11] =
+ b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15];
+
+ b0 = this.matrix[12];
+ b1 = this.matrix[13];
+ b2 = this.matrix[14];
+ b3 = this.matrix[15];
+ this.matrix[12] =
+ b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12];
+ this.matrix[13] =
+ b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13];
+ this.matrix[14] =
+ b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14];
+ this.matrix[15] =
+ b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15];
+
+ return this;
+ }
+
+ /**
+ * @param {p5.Matrix|Float32Array|Number[]} multMatrix The matrix
+ * we want to multiply by
+ * @private
+ * @chainable
+ */
+ #multNxN(multMatrix) {
+ if (multMatrix.length !== this.matrix.length) {
+ throw new Error('Matrices must be of the same dimension to multiply.');
+ }
+ const result = new GLMAT_ARRAY_TYPE(this.matrix.length).fill(0);
+ for (let i = 0; i < this.#sqDimention; i++) {
+ for (let j = 0; j < this.#sqDimention; j++) {
+ for (let k = 0; k < this.#sqDimention; k++) {
+ result[i * this.#sqDimention + j] +=
+ this.matrix[i * this.#sqDimention + k] *
+ multMatrix[k * this.#sqDimention + j];
+ }
+ }
+ }
+ this.matrix = result;
+ return this;
+ }
+
+ /**
+ * This function is only for 3x3 matrices.
+ * multiply two mat3s. It is an operation to multiply the 3x3 matrix of
+ * the argument from the right. Arguments can be a 3x3 p5.Matrix,
+ * a Float32Array of length 9, or a javascript array of length 9.
+ * In addition, it can also be done by enumerating 9 numbers.
+ *
+ * @param {p5.Matrix|Float32Array|Number[]} multMatrix The matrix
+ * we want to multiply by
+ * @private
+ * @chainable
+ */
+ #mult3x3(_src) {
+ // each row is used for the multiplier
+ let b0 = this.mat3[0];
+ let b1 = this.mat3[1];
+ let b2 = this.mat3[2];
+ this.mat3[0] = b0 * _src[0] + b1 * _src[3] + b2 * _src[6];
+ this.mat3[1] = b0 * _src[1] + b1 * _src[4] + b2 * _src[7];
+ this.mat3[2] = b0 * _src[2] + b1 * _src[5] + b2 * _src[8];
+
+ b0 = this.mat3[3];
+ b1 = this.mat3[4];
+ b2 = this.mat3[5];
+ this.mat3[3] = b0 * _src[0] + b1 * _src[3] + b2 * _src[6];
+ this.mat3[4] = b0 * _src[1] + b1 * _src[4] + b2 * _src[7];
+ this.mat3[5] = b0 * _src[2] + b1 * _src[5] + b2 * _src[8];
+
+ b0 = this.mat3[6];
+ b1 = this.mat3[7];
+ b2 = this.mat3[8];
+ this.mat3[6] = b0 * _src[0] + b1 * _src[3] + b2 * _src[6];
+ this.mat3[7] = b0 * _src[1] + b1 * _src[4] + b2 * _src[7];
+ this.mat3[8] = b0 * _src[2] + b1 * _src[5] + b2 * _src[8];
+
+ return this;
+ }
+
+ /**
+ * Transposes a square matrix in place.
+ * This method swaps the rows and columns of the matrix, effectively flipping it over its diagonal.
+ *
+ * @private
+ * @returns {Matrix} The current instance of the Matrix, with the transposed values.
+ */
+ #transposeNxN() {
+ const n = this.#sqDimention;
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < n; j++) {
+ this.matrix[i * n + j] = this.matrix[j * n + i];
+ }
+ }
+ return this;
+ }
+
+ /**
+ * transpose according to a given matrix
+ * @param {p5.Matrix|Float32Array|Number[]} a the matrix to be
+ * based on to transpose
+ * @private
+ * @chainable
+ */
+ #transpose4x4(a) {
+ console.log('====> 4x4');
+ let a01, a02, a03, a12, a13, a23;
+ if (a instanceof Matrix) {
+ a01 = a.matrix[1];
+ a02 = a.matrix[2];
+ a03 = a.matrix[3];
+ a12 = a.matrix[6];
+ a13 = a.matrix[7];
+ a23 = a.matrix[11];
+
+ this.matrix[0] = a.matrix[0];
+ this.matrix[1] = a.matrix[4];
+ this.matrix[2] = a.matrix[8];
+ this.matrix[3] = a.matrix[12];
+ this.matrix[4] = a01;
+ this.matrix[5] = a.matrix[5];
+ this.matrix[6] = a.matrix[9];
+ this.matrix[7] = a.matrix[13];
+ this.matrix[8] = a02;
+ this.matrix[9] = a12;
+ this.matrix[10] = a.matrix[10];
+ this.matrix[11] = a.matrix[14];
+ this.matrix[12] = a03;
+ this.matrix[13] = a13;
+ this.matrix[14] = a23;
+ this.matrix[15] = a.matrix[15];
+ } else if (isMatrixArray(a)) {
+ a01 = a[1];
+ a02 = a[2];
+ a03 = a[3];
+ a12 = a[6];
+ a13 = a[7];
+ a23 = a[11];
+
+ this.matrix[0] = a[0];
+ this.matrix[1] = a[4];
+ this.matrix[2] = a[8];
+ this.matrix[3] = a[12];
+ this.matrix[4] = a01;
+ this.matrix[5] = a[5];
+ this.matrix[6] = a[9];
+ this.matrix[7] = a[13];
+ this.matrix[8] = a02;
+ this.matrix[9] = a12;
+ this.matrix[10] = a[10];
+ this.matrix[11] = a[14];
+ this.matrix[12] = a03;
+ this.matrix[13] = a13;
+ this.matrix[14] = a23;
+ this.matrix[15] = a[15];
+ }
+ return this;
+ }
+
+ /**
+ * This function is only for 3x3 matrices.
+ * transposes a 3×3 p5.Matrix by a mat3
+ * If there is an array of arguments, the matrix obtained by transposing
+ * the 3x3 matrix generated based on that array is set.
+ * If no arguments, it transposes itself and returns it.
+ *
+ * @param {Number[]} mat3 1-dimensional array
+ * @private
+ * @chainable
+ */
+ #transpose3x3(mat3) {
+ if (mat3 === undefined) {
+ mat3 = this.mat3;
+ }
+ const a01 = mat3[1];
+ const a02 = mat3[2];
+ const a12 = mat3[5];
+ this.mat3[0] = mat3[0];
+ this.mat3[1] = mat3[3];
+ this.mat3[2] = mat3[6];
+ this.mat3[3] = a01;
+ this.mat3[4] = mat3[4];
+ this.mat3[5] = mat3[7];
+ this.mat3[6] = a02;
+ this.mat3[7] = a12;
+ this.mat3[8] = mat3[8];
+
+ return this;
+ }
+
+ /**
+ * Only 4x4 becasuse determinant is only 4x4 currently
+ * invert matrix according to a give matrix
+ * @param {p5.Matrix|Float32Array|Number[]} a the matrix to be
+ * based on to invert
+ * @private
+ * @chainable
+ */
+ #invert4x4(a) {
+ let a00, a01, a02, a03, a10, a11, a12, a13;
+ let a20, a21, a22, a23, a30, a31, a32, a33;
+ if (a instanceof Matrix) {
+ a00 = a.matrix[0];
+ a01 = a.matrix[1];
+ a02 = a.matrix[2];
+ a03 = a.matrix[3];
+ a10 = a.matrix[4];
+ a11 = a.matrix[5];
+ a12 = a.matrix[6];
+ a13 = a.matrix[7];
+ a20 = a.matrix[8];
+ a21 = a.matrix[9];
+ a22 = a.matrix[10];
+ a23 = a.matrix[11];
+ a30 = a.matrix[12];
+ a31 = a.matrix[13];
+ a32 = a.matrix[14];
+ a33 = a.matrix[15];
+ } else if (isMatrixArray(a)) {
+ a00 = a[0];
+ a01 = a[1];
+ a02 = a[2];
+ a03 = a[3];
+ a10 = a[4];
+ a11 = a[5];
+ a12 = a[6];
+ a13 = a[7];
+ a20 = a[8];
+ a21 = a[9];
+ a22 = a[10];
+ a23 = a[11];
+ a30 = a[12];
+ a31 = a[13];
+ a32 = a[14];
+ a33 = a[15];
+ }
+ const b00 = a00 * a11 - a01 * a10;
+ const b01 = a00 * a12 - a02 * a10;
+ const b02 = a00 * a13 - a03 * a10;
+ const b03 = a01 * a12 - a02 * a11;
+ const b04 = a01 * a13 - a03 * a11;
+ const b05 = a02 * a13 - a03 * a12;
+ const b06 = a20 * a31 - a21 * a30;
+ const b07 = a20 * a32 - a22 * a30;
+ const b08 = a20 * a33 - a23 * a30;
+ const b09 = a21 * a32 - a22 * a31;
+ const b10 = a21 * a33 - a23 * a31;
+ const b11 = a22 * a33 - a23 * a32;
+
+ // Calculate the determinant
+ let det =
+ b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+
+ if (!det) {
+ return null;
+ }
+ det = 1.0 / det;
+
+ this.matrix[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
+ this.matrix[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
+ this.matrix[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
+ this.matrix[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
+ this.matrix[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
+ this.matrix[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
+ this.matrix[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
+ this.matrix[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
+ this.matrix[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
+ this.matrix[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
+ this.matrix[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
+ this.matrix[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
+ this.matrix[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
+ this.matrix[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
+ this.matrix[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
+ this.matrix[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
+
+ return this;
+ }
+
+ /**
+ * Inverts a 3×3 matrix
+ * @chainable
+ * @private
+ */
+ #invert3x3() {
+ const a00 = this.mat3[0];
+ const a01 = this.mat3[1];
+ const a02 = this.mat3[2];
+ const a10 = this.mat3[3];
+ const a11 = this.mat3[4];
+ const a12 = this.mat3[5];
+ const a20 = this.mat3[6];
+ const a21 = this.mat3[7];
+ const a22 = this.mat3[8];
+ const b01 = a22 * a11 - a12 * a21;
+ const b11 = -a22 * a10 + a12 * a20;
+ const b21 = a21 * a10 - a11 * a20;
+
+ // Calculate the determinant
+ let det = a00 * b01 + a01 * b11 + a02 * b21;
+ if (!det) {
+ return null;
+ }
+ det = 1.0 / det;
+ this.mat3[0] = b01 * det;
+ this.mat3[1] = (-a22 * a01 + a02 * a21) * det;
+ this.mat3[2] = (a12 * a01 - a02 * a11) * det;
+ this.mat3[3] = b11 * det;
+ this.mat3[4] = (a22 * a00 - a02 * a20) * det;
+ this.mat3[5] = (-a12 * a00 + a02 * a10) * det;
+ this.mat3[6] = b21 * det;
+ this.mat3[7] = (-a21 * a00 + a01 * a20) * det;
+ this.mat3[8] = (a11 * a00 - a01 * a10) * det;
+ return this;
+ }
+
+ /**
+ * inspired by Toji's mat4 determinant
+ * @return {Number} Determinant of our 4×4 matrix
+ * @private
+ */
+ #determinant4x4() {
+ if (this.#sqDimention !== 4) {
+ throw new Error(
+ 'Determinant is only implemented for 4x4 matrices. We are working on it.'
+ );
+ }
+
+ const d00 =
+ this.matrix[0] * this.matrix[5] - this.matrix[1] * this.matrix[4],
+ d01 = this.matrix[0] * this.matrix[6] - this.matrix[2] * this.matrix[4],
+ d02 = this.matrix[0] * this.matrix[7] - this.matrix[3] * this.matrix[4],
+ d03 = this.matrix[1] * this.matrix[6] - this.matrix[2] * this.matrix[5],
+ d04 = this.matrix[1] * this.matrix[7] - this.matrix[3] * this.matrix[5],
+ d05 = this.matrix[2] * this.matrix[7] - this.matrix[3] * this.matrix[6],
+ d06 = this.matrix[8] * this.matrix[13] - this.matrix[9] * this.matrix[12],
+ d07 =
+ this.matrix[8] * this.matrix[14] - this.matrix[10] * this.matrix[12],
+ d08 =
+ this.matrix[8] * this.matrix[15] - this.matrix[11] * this.matrix[12],
+ d09 =
+ this.matrix[9] * this.matrix[14] - this.matrix[10] * this.matrix[13],
+ d10 =
+ this.matrix[9] * this.matrix[15] - this.matrix[11] * this.matrix[13],
+ d11 =
+ this.matrix[10] * this.matrix[15] - this.matrix[11] * this.matrix[14];
+
+ // Calculate the determinant
+ return (
+ d00 * d11 - d01 * d10 + d02 * d09 + d03 * d08 - d04 * d07 + d05 * d06
+ );
+ }
+
+ /**
+ * PRIVATE
+ */
+ // matrix methods adapted from:
+ // https://developer.mozilla.org/en-US/docs/Web/WebGL/
+ // gluPerspective
+ //
+ // function _makePerspective(fovy, aspect, znear, zfar){
+ // const ymax = znear * Math.tan(fovy * Math.PI / 360.0);
+ // const ymin = -ymax;
+ // const xmin = ymin * aspect;
+ // const xmax = ymax * aspect;
+ // return _makeFrustum(xmin, xmax, ymin, ymax, znear, zfar);
+ // }
+
+ ////
+ //// glFrustum
+ ////
+ //function _makeFrustum(left, right, bottom, top, znear, zfar){
+ // const X = 2*znear/(right-left);
+ // const Y = 2*znear/(top-bottom);
+ // const A = (right+left)/(right-left);
+ // const B = (top+bottom)/(top-bottom);
+ // const C = -(zfar+znear)/(zfar-znear);
+ // const D = -2*zfar*znear/(zfar-znear);
+ // const frustrumMatrix =[
+ // X, 0, A, 0,
+ // 0, Y, B, 0,
+ // 0, 0, C, D,
+ // 0, 0, -1, 0
+ //];
+ //return frustrumMatrix;
+ // }
+
+ // function _setMVPMatrices(){
+ ////an identity matrix
+ ////@TODO use the p5.Matrix class to abstract away our MV matrices and
+ ///other math
+ //const _mvMatrix =
+ //[
+ // 1.0,0.0,0.0,0.0,
+ // 0.0,1.0,0.0,0.0,
+ // 0.0,0.0,1.0,0.0,
+ // 0.0,0.0,0.0,1.0
+ //];
+ }
+
+ /**
+ * @module Math
+ * @requires constants
+ * @todo see methods below needing further implementation.
+ * future consideration: implement SIMD optimizations
+ * when browser compatibility becomes available
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/
+ * Reference/Global_Objects/SIMD
+ */
+ // import { MatrixNumjs as Matrix } from './Matrices/MatrixNumjs'
+
+ function matrix(p5, fn) {
+ /**
+ * A class to describe a matrix
+ * for model and view matrix manipulation in the p5js webgl renderer.
+ * The `Matrix` class represents a mathematical matrix and provides various methods for matrix operations.
+ *
+ * The `Matrix` class represents a mathematical matrix and provides various methods for matrix operations.
+ * This class extends the `MatrixInterface` and includes methods for creating, manipulating, and performing
+ * operations on matrices. It supports both 3x3 and 4x4 matrices, as well as general NxN matrices.
+ * @private
+ * @class p5.Matrix
+ * @param {Array} [mat4] column-major array literal of our 4×4 matrix
+ * @example
+ * // Creating a 3x3 matrix from an array using column major arrangement
+ * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ *
+ * // Creating a 4x4 identity matrix
+ * const identityMatrix = new p5.Matrix(4);
+ *
+ * // Adding two matrices
+ * const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]);
+ * matrix1.add(matrix2); // matrix1 is now [10, 10, 10, 10, 10, 10, 10, 10, 10]
+ *
+ * // Setting an element in the matrix
+ * matrix.setElement(0, 10); // matrix is now [10, 2, 3, 4, 5, 6, 7, 8, 9]
+ *
+ * // Resetting the matrix to an identity matrix
+ * matrix.reset();
+ *
+ * // Getting the diagonal elements of the matrix
+ * const diagonal = matrix.diagonal(); // [1, 1, 1]
+ *
+ * // Transposing the matrix
+ * matrix.transpose();
+ *
+ * // Multiplying two matrices
+ * matrix1.mult(matrix2);
+ *
+ * // Inverting the matrix
+ * matrix.invert();
+ *
+ * // Scaling the matrix
+ * matrix.scale(2, 2, 2);
+ *
+ * // Rotating the matrix around an axis
+ * matrix.rotate4x4(Math.PI / 4, 1, 0, 0);
+ *
+ * // Applying a perspective transformation
+ * matrix.perspective(Math.PI / 4, 1, 0.1, 100);
+ *
+ * // Applying an orthographic transformation
+ * matrix.ortho(-1, 1, -1, 1, 0.1, 100);
+ *
+ * // Multiplying a vector by the matrix
+ * const vector = new Vector(1, 2, 3);
+ * const result = matrix.multiplyPoint(vector);
+ *
+ * @example
+ * // META:norender
+ * // p5.js script example
+ * function setup() {
+ *
+ * // Create a 4x4 identity matrix
+ * const matrix = new p5.Matrix(4);
+ * console.log("Original p5.Matrix:", matrix.matrix.toString()); // Output: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ *
+ * // Add two matrices
+ * const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ * const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]);
+ * matrix1.add(matrix2);
+ * console.log("After Addition:", matrix1.matrix.toString()); // Output: [10, 10, 10, 10, 10, 10, 10, 10, 10]
+ *
+ * // Reset the matrix to an identity matrix
+ * matrix.reset();
+ * console.log("Reset p5.Matrix:", matrix.matrix.toString()); // [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ *
+ * // Apply a scaling transformation
+ * matrix.scale(2, 2, 2);
+ * console.log("Scaled p5.Matrix:", matrix.matrix.toString()); // [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1]
+ *
+ * // Apply a rotation around the X-axis
+ * matrix.rotate4x4(Math.PI / 4, 1, 0, 0);
+ * console.log("Rotated p5.Matrix (X-axis):", matrix.matrix.toString()); // [2, 0, 0, 0, 0, 1.4142135381698608, 1.4142135381698608, 0, 0, -1.4142135381698608, 1.4142135381698608, 0, 0, 0, 0, 1]
+ *
+ * // Apply a perspective transformation
+ * matrix.perspective(Math.PI / 4, 1, 0.1, 100);
+ * console.log("Perspective p5.Matrix:", matrix.matrix.toString());// [2.4142136573791504, 0, 0, 0, 0, 2.4142136573791504, 0, 0, 0, 0, -1.0020020008087158, -1, 0, 0, -0.20020020008087158, 0]
+ *
+ * // Multiply a vector by the matrix
+ * const vector = new p5.Vector(1, 2, 3);
+ * const transformedVector = matrix.multiplyPoint(vector);
+ * console.log("Transformed Vector:", transformedVector.toString());
+ * }
+ */
+ p5.Matrix = Matrix;
+ }
+
+ if (typeof p5 !== 'undefined') {
+ matrix(p5);
+ }
+
+ class DataArray {
+ constructor(initialLength = 128) {
+ this.length = 0;
+ this.data = new Float32Array(initialLength);
+ this.initialLength = initialLength;
+ }
+
+ /**
+ * Returns a Float32Array window sized to the exact length of the data
+ */
+ dataArray() {
+ return this.subArray(0, this.length);
+ }
+
+ /**
+ * A "soft" clear, which keeps the underlying storage size the same, but
+ * empties the contents of its dataArray()
+ */
+ clear() {
+ this.length = 0;
+ }
+
+ /**
+ * Can be used to scale a DataArray back down to fit its contents.
+ */
+ rescale() {
+ if (this.length < this.data.length / 2) {
+ // Find the power of 2 size that fits the data
+ const targetLength = 1 << Math.ceil(Math.log2(this.length));
+ const newData = new Float32Array(targetLength);
+ newData.set(this.data.subarray(0, this.length), 0);
+ this.data = newData;
+ }
+ }
+
+ /**
+ * A full reset, which allocates a new underlying Float32Array at its initial
+ * length
+ */
+ reset() {
+ this.clear();
+ this.data = new Float32Array(this.initialLength);
+ }
+
+ /**
+ * Adds values to the DataArray, expanding its internal storage to
+ * accommodate the new items.
+ */
+ push(...values) {
+ this.ensureLength(this.length + values.length);
+ this.data.set(values, this.length);
+ this.length += values.length;
+ }
+
+ /**
+ * Returns a copy of the data from the index `from`, inclusive, to the index
+ * `to`, exclusive
+ */
+ slice(from, to) {
+ return this.data.slice(from, Math.min(to, this.length));
+ }
+
+ /**
+ * Returns a mutable Float32Array window from the index `from`, inclusive, to
+ * the index `to`, exclusive
+ */
+ subArray(from, to) {
+ return this.data.subarray(from, Math.min(to, this.length));
+ }
+
+ /**
+ * Expand capacity of the internal storage until it can fit a target size
+ */
+ ensureLength(target) {
+ while (this.data.length < target) {
+ const newData = new Float32Array(this.data.length * 2);
+ newData.set(this.data, 0);
+ this.data = newData;
+ }
+ }
+ }
+ function dataArray(p5, fn){
+ /**
+ * An internal class to store data that will be sent to a p5.RenderBuffer.
+ * Those need to eventually go into a Float32Array, so this class provides a
+ * variable-length array container backed by a Float32Array so that it can be
+ * sent to the GPU without allocating a new array each frame.
+ *
+ * Like a C++ vector, its fixed-length Float32Array backing its contents will
+ * double in size when it goes over its capacity.
+ *
+ * @example
+ * // Initialize storage with a capacity of 4
+ * const storage = new DataArray(4);
+ * console.log(storage.data.length); // 4
+ * console.log(storage.length); // 0
+ * console.log(storage.dataArray()); // Empty Float32Array
+ *
+ * storage.push(1, 2, 3, 4, 5, 6);
+ * console.log(storage.data.length); // 8
+ * console.log(storage.length); // 6
+ * console.log(storage.dataArray()); // Float32Array{1, 2, 3, 4, 5, 6}
+ */
+ p5.DataArray = DataArray;
+ }
+
+ if(typeof p5 !== 'undefined'){
+ dataArray(p5);
+ }
+
+ /**
+ * @module Shape
+ * @submodule 3D Primitives
+ * @for p5
+ * @requires core
+ * @requires p5.Geometry
+ */
+
+
+ class Geometry {
+ constructor(detailX, detailY, callback, renderer) {
+ this.renderer = renderer;
+ this.vertices = [];
+
+ this.boundingBoxCache = null;
+
+
+ //an array containing every vertex for stroke drawing
+ this.lineVertices = new DataArray();
+
+ // The tangents going into or out of a vertex on a line. Along a straight
+ // line segment, both should be equal. At an endpoint, one or the other
+ // will not exist and will be all 0. In joins between line segments, they
+ // may be different, as they will be the tangents on either side of the join.
+ this.lineTangentsIn = new DataArray();
+ this.lineTangentsOut = new DataArray();
+
+ // When drawing lines with thickness, entries in this buffer represent which
+ // side of the centerline the vertex will be placed. The sign of the number
+ // will represent the side of the centerline, and the absolute value will be
+ // used as an enum to determine which part of the cap or join each vertex
+ // represents. See the doc comments for _addCap and _addJoin for diagrams.
+ this.lineSides = new DataArray();
+
+ this.vertexNormals = [];
+
+ this.faces = [];
+
+ this.uvs = [];
+ // a 2D array containing edge connectivity pattern for create line vertices
+ //based on faces for most objects;
+ this.edges = [];
+ this.vertexColors = [];
+
+ // One color per vertex representing the stroke color at that vertex
+ this.vertexStrokeColors = [];
+
+ this.userVertexProperties = {};
+
+ // One color per line vertex, generated automatically based on
+ // vertexStrokeColors in _edgesToVertices()
+ this.lineVertexColors = new DataArray();
+ this.detailX = detailX !== undefined ? detailX : 1;
+ this.detailY = detailY !== undefined ? detailY : 1;
+ this.dirtyFlags = {};
+
+ this._hasFillTransparency = undefined;
+ this._hasStrokeTransparency = undefined;
+
+ this.gid = `_p5_Geometry_${Geometry.nextId}`;
+ Geometry.nextId++;
+ if (callback instanceof Function) {
+ callback.call(this);
+ }
+ }
+
+ /**
+ * Calculates the position and size of the smallest box that contains the geometry.
+ *
+ * A bounding box is the smallest rectangular prism that contains the entire
+ * geometry. It's defined by the box's minimum and maximum coordinates along
+ * each axis, as well as the size (length) and offset (center).
+ *
+ * Calling `myGeometry.calculateBoundingBox()` returns an object with four
+ * properties that describe the bounding box:
+ *
+ * ```js
+ * // Get myGeometry's bounding box.
+ * let bbox = myGeometry.calculateBoundingBox();
+ *
+ * // Print the bounding box to the console.
+ * console.log(bbox);
+ *
+ * // {
+ * // // The minimum coordinate along each axis.
+ * // min: { x: -1, y: -2, z: -3 },
+ * //
+ * // // The maximum coordinate along each axis.
+ * // max: { x: 1, y: 2, z: 3},
+ * //
+ * // // The size (length) along each axis.
+ * // size: { x: 2, y: 4, z: 6},
+ * //
+ * // // The offset (center) along each axis.
+ * // offset: { x: 0, y: 0, z: 0}
+ * // }
+ * ```
+ *
+ * @returns {Object} bounding box of the geometry.
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let particles;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a new p5.Geometry object with random spheres.
+ * particles = buildGeometry(createParticles);
+ *
+ * describe('Ten white spheres placed randomly against a gray background. A box encloses the spheres.');
+ * }
+ *
+ * function draw() {
+ * background(50);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Style the particles.
+ * noStroke();
+ * fill(255);
+ *
+ * // Draw the particles.
+ * model(particles);
+ *
+ * // Calculate the bounding box.
+ * let bbox = particles.calculateBoundingBox();
+ *
+ * // Translate to the bounding box's center.
+ * translate(bbox.offset.x, bbox.offset.y, bbox.offset.z);
+ *
+ * // Style the bounding box.
+ * stroke(255);
+ * noFill();
+ *
+ * // Draw the bounding box.
+ * box(bbox.size.x, bbox.size.y, bbox.size.z);
+ * }
+ *
+ * function createParticles() {
+ * for (let i = 0; i < 10; i += 1) {
+ * // Calculate random coordinates.
+ * let x = randomGaussian(0, 15);
+ * let y = randomGaussian(0, 15);
+ * let z = randomGaussian(0, 15);
+ *
+ * push();
+ * // Translate to the particle's coordinates.
+ * translate(x, y, z);
+ * // Draw the particle.
+ * sphere(3);
+ * pop();
+ * }
+ * }
+ */
+ calculateBoundingBox() {
+ if (this.boundingBoxCache) {
+ return this.boundingBoxCache; // Return cached result if available
+ }
+
+ let minVertex = new Vector(
+ Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
+ let maxVertex = new Vector(
+ Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE);
+
+ for (let i = 0; i < this.vertices.length; i++) {
+ let vertex = this.vertices[i];
+ minVertex.x = Math.min(minVertex.x, vertex.x);
+ minVertex.y = Math.min(minVertex.y, vertex.y);
+ minVertex.z = Math.min(minVertex.z, vertex.z);
+
+ maxVertex.x = Math.max(maxVertex.x, vertex.x);
+ maxVertex.y = Math.max(maxVertex.y, vertex.y);
+ maxVertex.z = Math.max(maxVertex.z, vertex.z);
+ }
+ // Calculate size and offset properties
+ let size = new Vector(maxVertex.x - minVertex.x,
+ maxVertex.y - minVertex.y, maxVertex.z - minVertex.z);
+ let offset = new Vector((minVertex.x + maxVertex.x) / 2,
+ (minVertex.y + maxVertex.y) / 2, (minVertex.z + maxVertex.z) / 2);
+
+ // Cache the result for future access
+ this.boundingBoxCache = {
+ min: minVertex,
+ max: maxVertex,
+ size: size,
+ offset: offset
+ };
+
+ return this.boundingBoxCache;
+ }
+
+ reset() {
+ // Notify renderer that geometry is being reset (for buffer cleanup)
+ this.renderer?.onReset?.(this);
+
+ this._hasFillTransparency = undefined;
+ this._hasStrokeTransparency = undefined;
+
+ this.lineVertices.clear();
+ this.lineTangentsIn.clear();
+ this.lineTangentsOut.clear();
+ this.lineSides.clear();
+
+ this.vertices.length = 0;
+ this.edges.length = 0;
+ this.vertexColors.length = 0;
+ this.vertexStrokeColors.length = 0;
+ this.lineVertexColors.clear();
+ this.vertexNormals.length = 0;
+ this.uvs.length = 0;
+
+ for (const propName in this.userVertexProperties){
+ this.userVertexProperties[propName].delete();
+ }
+ this.userVertexProperties = {};
+
+ this.dirtyFlags = {};
+ }
+
+ hasFillTransparency() {
+ if (this._hasFillTransparency === undefined) {
+ this._hasFillTransparency = false;
+ for (let i = 0; i < this.vertexColors.length; i += 4) {
+ if (this.vertexColors[i + 3] < 1) {
+ this._hasFillTransparency = true;
+ break;
+ }
+ }
+ }
+ return this._hasFillTransparency;
+ }
+ hasStrokeTransparency() {
+ if (this._hasStrokeTransparency === undefined) {
+ this._hasStrokeTransparency = false;
+ for (let i = 0; i < this.lineVertexColors.length; i += 4) {
+ if (this.lineVertexColors[i + 3] < 1) {
+ this._hasStrokeTransparency = true;
+ break;
+ }
+ }
+ }
+ return this._hasStrokeTransparency;
+ }
+
+ /**
+ * Removes the geometry’s internal colors.
+ *
+ * `p5.Geometry` objects can be created with "internal colors" assigned to
+ * vertices or the entire shape. When a geometry has internal colors,
+ * fill() has no effect. Calling
+ * `myGeometry.clearColors()` allows the
+ * fill() function to apply color to the geometry.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Create a p5.Geometry object.
+ * // Set its internal color to red.
+ * let myGeometry = buildGeometry(function() {
+ * fill(255, 0, 0);
+ * plane(20);
+ * });
+ *
+ * // Style the shape.
+ * noStroke();
+ *
+ * // Draw the p5.Geometry object (center).
+ * model(myGeometry);
+ *
+ * // Translate the origin to the bottom-right.
+ * translate(25, 25, 0);
+ *
+ * // Try to fill the geometry with green.
+ * fill(0, 255, 0);
+ *
+ * // Draw the geometry again (bottom-right).
+ * model(myGeometry);
+ *
+ * // Clear the geometry's colors.
+ * myGeometry.clearColors();
+ *
+ * // Fill the geometry with blue.
+ * fill(0, 0, 255);
+ *
+ * // Translate the origin up.
+ * translate(0, -50, 0);
+ *
+ * // Draw the geometry again (top-right).
+ * model(myGeometry);
+ *
+ * describe(
+ * 'Three squares drawn against a gray background. Red squares are at the center and the bottom-right. A blue square is at the top-right.'
+ * );
+ * }
+ */
+ clearColors() {
+ this.vertexColors = [];
+ return this;
+ }
+
+ /**
+ * The `saveObj()` function exports `p5.Geometry` objects as
+ * 3D models in the Wavefront .obj file format.
+ * This way, you can use the 3D shapes you create in p5.js in other software
+ * for rendering, animation, 3D printing, or more.
+ *
+ * The exported .obj file will include the faces and vertices of the `p5.Geometry`,
+ * as well as its texture coordinates and normals, if it has them.
+ *
+ * @method saveObj
+ * @param {String} [fileName='model.obj'] The name of the file to save the model as.
+ * If not specified, the default file name will be 'model.obj'.
+ * @example
+ * let myModel;
+ * let saveBtn;
+ * function setup() {
+ * createCanvas(200, 200, WEBGL);
+ * myModel = buildGeometry(function()) {
+ * for (let i = 0; i < 5; i++) {
+ * push();
+ * translate(
+ * random(-75, 75),
+ * random(-75, 75),
+ * random(-75, 75)
+ * );
+ * sphere(random(5, 50));
+ * pop();
+ * }
+ * });
+ *
+ * saveBtn = createButton('Save .obj');
+ * saveBtn.mousePressed(() => myModel.saveObj());
+ *
+ * describe('A few spheres rotating in space');
+ * }
+ *
+ * function draw() {
+ * background(0);
+ * noStroke();
+ * lights();
+ * rotateX(millis() * 0.001);
+ * rotateY(millis() * 0.002);
+ * model(myModel);
+ * }
+ */
+ saveObj(fileName = 'model.obj') {
+ let objStr= '';
+
+
+ // Vertices
+ this.vertices.forEach(v => {
+ objStr += `v ${v.x} ${v.y} ${v.z}\n`;
+ });
+
+ // Texture Coordinates (UVs)
+ if (this.uvs && this.uvs.length > 0) {
+ for (let i = 0; i < this.uvs.length; i += 2) {
+ objStr += `vt ${this.uvs[i]} ${this.uvs[i + 1]}\n`;
+ }
+ }
+
+ // Vertex Normals
+ if (this.vertexNormals && this.vertexNormals.length > 0) {
+ this.vertexNormals.forEach(n => {
+ objStr += `vn ${n.x} ${n.y} ${n.z}\n`;
+ });
+
+ }
+ // Faces, obj vertex indices begin with 1 and not 0
+ // texture coordinate (uvs) and vertexNormal indices
+ // are indicated with trailing ints vertex/normal/uv
+ // ex 1/1/1 or 2//2 for vertices without uvs
+ this.faces.forEach(face => {
+ let faceStr = 'f';
+ face.forEach(index =>{
+ faceStr += ' ';
+ faceStr += index + 1;
+ if (this.vertexNormals.length > 0 || this.uvs.length > 0) {
+ faceStr += '/';
+ if (this.uvs.length > 0) {
+ faceStr += index + 1;
+ }
+ faceStr += '/';
+ if (this.vertexNormals.length > 0) {
+ faceStr += index + 1;
+ }
+ }
+ });
+ objStr += faceStr + '\n';
+ });
+
+ const blob = new Blob([objStr], { type: 'text/plain' });
+ downloadFile(blob, fileName , 'obj');
+
+ }
+
+ /**
+ * The `saveStl()` function exports `p5.Geometry` objects as
+ * 3D models in the STL stereolithography file format.
+ * This way, you can use the 3D shapes you create in p5.js in other software
+ * for rendering, animation, 3D printing, or more.
+ *
+ * The exported .stl file will include the faces, vertices, and normals of the `p5.Geometry`.
+ *
+ * By default, this method saves a text-based .stl file. Alternatively, you can save a more compact
+ * but less human-readable binary .stl file by passing `{ binary: true }` as a second parameter.
+ *
+ * @method saveStl
+ * @param {String} [fileName='model.stl'] The name of the file to save the model as.
+ * If not specified, the default file name will be 'model.stl'.
+ * @param {Object} [options] Optional settings.
+ * @param {Boolean} [options.binary=false] Whether or not a binary .stl file is saved.
+ * @example
+ * let myModel;
+ * let saveBtn1;
+ * let saveBtn2;
+ * function setup() {
+ * createCanvas(200, 200, WEBGL);
+ * myModel = buildGeometry(function() {
+ * for (let i = 0; i < 5; i++) {
+ * push();
+ * translate(
+ * random(-75, 75),
+ * random(-75, 75),
+ * random(-75, 75)
+ * );
+ * sphere(random(5, 50));
+ * pop();
+ * }
+ * });
+ *
+ * saveBtn1 = createButton('Save .stl');
+ * saveBtn1.mousePressed(function() {
+ * myModel.saveStl();
+ * });
+ * saveBtn2 = createButton('Save binary .stl');
+ * saveBtn2.mousePressed(function() {
+ * myModel.saveStl('model.stl', { binary: true });
+ * });
+ *
+ * describe('A few spheres rotating in space');
+ * }
+ *
+ * function draw() {
+ * background(0);
+ * noStroke();
+ * lights();
+ * rotateX(millis() * 0.001);
+ * rotateY(millis() * 0.002);
+ * model(myModel);
+ * }
+ */
+ saveStl(fileName = 'model.stl', { binary = false } = {}){
+ let modelOutput;
+ let name = fileName.substring(0, fileName.lastIndexOf('.'));
+ let faceNormals = [];
+ for (let f of this.faces) {
+ const U = Vector.sub(this.vertices[f[1]], this.vertices[f[0]]);
+ const V = Vector.sub(this.vertices[f[2]], this.vertices[f[0]]);
+ const nx = U.y * V.z - U.z * V.y;
+ const ny = U.z * V.x - U.x * V.z;
+ const nz = U.x * V.y - U.y * V.x;
+ faceNormals.push(new Vector(nx, ny, nz).normalize());
+ }
+ if (binary) {
+ let offset = 80;
+ const bufferLength =
+ this.faces.length * 2 + this.faces.length * 3 * 4 * 4 + 80 + 4;
+ const arrayBuffer = new ArrayBuffer(bufferLength);
+ modelOutput = new DataView(arrayBuffer);
+ modelOutput.setUint32(offset, this.faces.length, true);
+ offset += 4;
+ for (const [key, f] of Object.entries(this.faces)) {
+ const norm = faceNormals[key];
+ modelOutput.setFloat32(offset, norm.x, true);
+ offset += 4;
+ modelOutput.setFloat32(offset, norm.y, true);
+ offset += 4;
+ modelOutput.setFloat32(offset, norm.z, true);
+ offset += 4;
+ for (let vertexIndex of f) {
+ const vert = this.vertices[vertexIndex];
+ modelOutput.setFloat32(offset, vert.x, true);
+ offset += 4;
+ modelOutput.setFloat32(offset, vert.y, true);
+ offset += 4;
+ modelOutput.setFloat32(offset, vert.z, true);
+ offset += 4;
+ }
+ modelOutput.setUint16(offset, 0, true);
+ offset += 2;
+ }
+ } else {
+ modelOutput = 'solid ' + name + '\n';
+
+ for (const [key, f] of Object.entries(this.faces)) {
+ const norm = faceNormals[key];
+ modelOutput +=
+ ' facet norm ' + norm.x + ' ' + norm.y + ' ' + norm.z + '\n';
+ modelOutput += ' outer loop' + '\n';
+ for (let vertexIndex of f) {
+ const vert = this.vertices[vertexIndex];
+ modelOutput +=
+ ' vertex ' + vert.x + ' ' + vert.y + ' ' + vert.z + '\n';
+ }
+ modelOutput += ' endloop' + '\n';
+ modelOutput += ' endfacet' + '\n';
+ }
+ modelOutput += 'endsolid ' + name + '\n';
+ }
+ const blob = new Blob([modelOutput], { type: 'text/plain' });
+ downloadFile(blob, fileName, 'stl');
+ }
+
+ /**
+ * Flips the geometry’s texture u-coordinates.
+ *
+ * In order for texture() to work, the geometry
+ * needs a way to map the points on its surface to the pixels in a rectangular
+ * image that's used as a texture. The geometry's vertex at coordinates
+ * `(x, y, z)` maps to the texture image's pixel at coordinates `(u, v)`.
+ *
+ * The myGeometry.uvs array stores the
+ * `(u, v)` coordinates for each vertex in the order it was added to the
+ * geometry. Calling `myGeometry.flipU()` flips a geometry's u-coordinates
+ * so that the texture appears mirrored horizontally.
+ *
+ * For example, a plane's four vertices are added clockwise starting from the
+ * top-left corner. Here's how calling `myGeometry.flipU()` would change a
+ * plane's texture coordinates:
+ *
+ * ```js
+ * // Print the original texture coordinates.
+ * // Output: [0, 0, 1, 0, 0, 1, 1, 1]
+ * console.log(myGeometry.uvs);
+ *
+ * // Flip the u-coordinates.
+ * myGeometry.flipU();
+ *
+ * // Print the flipped texture coordinates.
+ * // Output: [1, 0, 0, 0, 1, 1, 0, 1]
+ * console.log(myGeometry.uvs);
+ *
+ * // Notice the swaps:
+ * // Top vertices: [0, 0, 1, 0] --> [1, 0, 0, 0]
+ * // Bottom vertices: [0, 1, 1, 1] --> [1, 1, 0, 1]
+ * ```
+ *
+ * @for p5.Geometry
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * img = await loadImage('assets/laDefense.jpg');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Create p5.Geometry objects.
+ * let geom1 = buildGeometry(createShape);
+ * let geom2 = buildGeometry(createShape);
+ *
+ * // Flip geom2's U texture coordinates.
+ * geom2.flipU();
+ *
+ * // Left (original).
+ * push();
+ * translate(-25, 0, 0);
+ * texture(img);
+ * noStroke();
+ * model(geom1);
+ * pop();
+ *
+ * // Right (flipped).
+ * push();
+ * translate(25, 0, 0);
+ * texture(img);
+ * noStroke();
+ * model(geom2);
+ * pop();
+ *
+ * describe(
+ * 'Two photos of a ceiling on a gray background. The photos are mirror images of each other.'
+ * );
+ * }
+ *
+ * function createShape() {
+ * plane(40);
+ * }
+ */
+ flipU() {
+ this.uvs = this.uvs.flat().map((val, index) => {
+ if (index % 2 === 0) {
+ return 1 - val;
+ } else {
+ return val;
+ }
+ });
+ }
+
+ /**
+ * Flips the geometry’s texture v-coordinates.
+ *
+ * In order for texture() to work, the geometry
+ * needs a way to map the points on its surface to the pixels in a rectangular
+ * image that's used as a texture. The geometry's vertex at coordinates
+ * `(x, y, z)` maps to the texture image's pixel at coordinates `(u, v)`.
+ *
+ * The myGeometry.uvs array stores the
+ * `(u, v)` coordinates for each vertex in the order it was added to the
+ * geometry. Calling `myGeometry.flipV()` flips a geometry's v-coordinates
+ * so that the texture appears mirrored vertically.
+ *
+ * For example, a plane's four vertices are added clockwise starting from the
+ * top-left corner. Here's how calling `myGeometry.flipV()` would change a
+ * plane's texture coordinates:
+ *
+ * ```js
+ * // Print the original texture coordinates.
+ * // Output: [0, 0, 1, 0, 0, 1, 1, 1]
+ * console.log(myGeometry.uvs);
+ *
+ * // Flip the v-coordinates.
+ * myGeometry.flipV();
+ *
+ * // Print the flipped texture coordinates.
+ * // Output: [0, 1, 1, 1, 0, 0, 1, 0]
+ * console.log(myGeometry.uvs);
+ *
+ * // Notice the swaps:
+ * // Left vertices: [0, 0] <--> [1, 0]
+ * // Right vertices: [1, 0] <--> [1, 1]
+ * ```
+ *
+ * @method flipV
+ * @for p5.Geometry
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * img = await loadImage('assets/laDefense.jpg');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Create p5.Geometry objects.
+ * let geom1 = buildGeometry(createShape);
+ * let geom2 = buildGeometry(createShape);
+ *
+ * // Flip geom2's V texture coordinates.
+ * geom2.flipV();
+ *
+ * // Left (original).
+ * push();
+ * translate(-25, 0, 0);
+ * texture(img);
+ * noStroke();
+ * model(geom1);
+ * pop();
+ *
+ * // Right (flipped).
+ * push();
+ * translate(25, 0, 0);
+ * texture(img);
+ * noStroke();
+ * model(geom2);
+ * pop();
+ *
+ * describe(
+ * 'Two photos of a ceiling on a gray background. The photos are mirror images of each other.'
+ * );
+ * }
+ *
+ * function createShape() {
+ * plane(40);
+ * }
+ */
+ flipV() {
+ this.uvs = this.uvs.flat().map((val, index) => {
+ if (index % 2 === 0) {
+ return val;
+ } else {
+ return 1 - val;
+ }
+ });
+ }
+
+ /**
+ * Computes the geometry's faces using its vertices.
+ *
+ * All 3D shapes are made by connecting sets of points called *vertices*. A
+ * geometry's surface is formed by connecting vertices to form triangles that
+ * are stitched together. Each triangular patch on the geometry's surface is
+ * called a *face*. `myGeometry.computeFaces()` performs the math needed to
+ * define each face based on the distances between vertices.
+ *
+ * The geometry's vertices are stored as p5.Vector
+ * objects in the myGeometry.vertices
+ * array. The geometry's first vertex is the
+ * p5.Vector object at `myGeometry.vertices[0]`,
+ * its second vertex is `myGeometry.vertices[1]`, its third vertex is
+ * `myGeometry.vertices[2]`, and so on.
+ *
+ * Calling `myGeometry.computeFaces()` fills the
+ * myGeometry.faces array with three-element
+ * arrays that list the vertices that form each face. For example, a geometry
+ * made from a rectangle has two faces because a rectangle is made by joining
+ * two triangles. myGeometry.faces for a
+ * rectangle would be the two-dimensional array
+ * `[[0, 1, 2], [2, 1, 3]]`. The first face, `myGeometry.faces[0]`, is the
+ * array `[0, 1, 2]` because it's formed by connecting
+ * `myGeometry.vertices[0]`, `myGeometry.vertices[1]`,and
+ * `myGeometry.vertices[2]`. The second face, `myGeometry.faces[1]`, is the
+ * array `[2, 1, 3]` because it's formed by connecting
+ * `myGeometry.vertices[2]`, `myGeometry.vertices[1]`, and
+ * `myGeometry.vertices[3]`.
+ *
+ * Note: `myGeometry.computeFaces()` only works when geometries have four or more vertices.
+ *
+ * @chainable
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = new p5.Geometry();
+ *
+ * // Create p5.Vector objects to position the vertices.
+ * let v0 = createVector(-40, 0, 0);
+ * let v1 = createVector(0, -40, 0);
+ * let v2 = createVector(0, 40, 0);
+ * let v3 = createVector(40, 0, 0);
+ *
+ * // Add the vertices to myGeometry's vertices array.
+ * myGeometry.vertices.push(v0, v1, v2, v3);
+ *
+ * // Compute myGeometry's faces array.
+ * myGeometry.computeFaces();
+ *
+ * describe('A red square drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Style the shape.
+ * noStroke();
+ * fill(255, 0, 0);
+ *
+ * // Draw the p5.Geometry object.
+ * model(myGeometry);
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object using a callback function.
+ * myGeometry = new p5.Geometry(1, 1, createShape);
+ *
+ * describe('A red square drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Style the shape.
+ * noStroke();
+ * fill(255, 0, 0);
+ *
+ * // Draw the p5.Geometry object.
+ * model(myGeometry);
+ * }
+ *
+ * function createShape() {
+ * // Create p5.Vector objects to position the vertices.
+ * let v0 = createVector(-40, 0, 0);
+ * let v1 = createVector(0, -40, 0);
+ * let v2 = createVector(0, 40, 0);
+ * let v3 = createVector(40, 0, 0);
+ *
+ * // Add the vertices to the p5.Geometry object's vertices array.
+ * this.vertices.push(v0, v1, v2, v3);
+ *
+ * // Compute the faces array.
+ * this.computeFaces();
+ * }
+ */
+ computeFaces() {
+ this.faces.length = 0;
+ const sliceCount = this.detailX + 1;
+ let a, b, c, d;
+ for (let i = 0; i < this.detailY; i++) {
+ for (let j = 0; j < this.detailX; j++) {
+ a = i * sliceCount + j; // + offset;
+ b = i * sliceCount + j + 1; // + offset;
+ c = (i + 1) * sliceCount + j + 1; // + offset;
+ d = (i + 1) * sliceCount + j; // + offset;
+ this.faces.push([a, b, d]);
+ this.faces.push([d, b, c]);
+ }
+ }
+ return this;
+ }
+
+ _getFaceNormal(faceId) {
+ //This assumes that vA->vB->vC is a counter-clockwise ordering
+ const face = this.faces[faceId];
+ const vA = this.vertices[face[0]];
+ const vB = this.vertices[face[1]];
+ const vC = this.vertices[face[2]];
+ const ab = Vector.sub(vB, vA);
+ const ac = Vector.sub(vC, vA);
+ const n = Vector.cross(ab, ac);
+ const ln = Vector.mag(n);
+ let sinAlpha = ln / (Vector.mag(ab) * Vector.mag(ac));
+ if (sinAlpha === 0 || isNaN(sinAlpha)) {
+ console.warn(
+ 'p5.Geometry.prototype._getFaceNormal:',
+ 'face has colinear sides or a repeated vertex'
+ );
+ return n;
+ }
+ if (sinAlpha > 1) sinAlpha = 1; // handle float rounding error
+ return n.mult(Math.asin(sinAlpha) / ln);
+ }
+ /**
+ * Calculates the normal vector for each vertex on the geometry.
+ *
+ * All 3D shapes are made by connecting sets of points called *vertices*. A
+ * geometry's surface is formed by connecting vertices to create triangles
+ * that are stitched together. Each triangular patch on the geometry's
+ * surface is called a *face*. `myGeometry.computeNormals()` performs the
+ * math needed to orient each face. Orientation is important for lighting
+ * and other effects.
+ *
+ * A face's orientation is defined by its *normal vector* which points out
+ * of the face and is normal (perpendicular) to the surface. Calling
+ * `myGeometry.computeNormals()` first calculates each face's normal vector.
+ * Then it calculates the normal vector for each vertex by averaging the
+ * normal vectors of the faces surrounding the vertex. The vertex normals
+ * are stored as p5.Vector objects in the
+ * myGeometry.vertexNormals array.
+ *
+ * The first parameter, `shadingType`, is optional. Passing the constant
+ * `FLAT`, as in `myGeometry.computeNormals(FLAT)`, provides neighboring
+ * faces with their own copies of the vertices they share. Surfaces appear
+ * tiled with flat shading. Passing the constant `SMOOTH`, as in
+ * `myGeometry.computeNormals(SMOOTH)`, makes neighboring faces reuse their
+ * shared vertices. Surfaces appear smoother with smooth shading. By
+ * default, `shadingType` is `FLAT`.
+ *
+ * The second parameter, `options`, is also optional. If an object with a
+ * `roundToPrecision` property is passed, as in
+ * `myGeometry.computeNormals(SMOOTH, { roundToPrecision: 5 })`, it sets the
+ * number of decimal places to use for calculations. By default,
+ * `roundToPrecision` uses 3 decimal places.
+ *
+ * @param {(FLAT|SMOOTH)} [shadingType=FLAT] shading type. either FLAT or SMOOTH. Defaults to `FLAT`.
+ * @param {Object} [options] shading options.
+ * @chainable
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = buildGeometry(function() {
+ * torus();
+ * });
+ *
+ * // Compute the vertex normals.
+ * myGeometry.computeNormals();
+ *
+ * describe(
+ * "A white torus drawn on a dark gray background. Red lines extend outward from the torus' vertices."
+ * );
+ * }
+ *
+ * function draw() {
+ * background(50);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Rotate the coordinate system.
+ * rotateX(1);
+ *
+ * // Style the helix.
+ * stroke(0);
+ *
+ * // Display the helix.
+ * model(myGeometry);
+ *
+ * // Style the normal vectors.
+ * stroke(255, 0, 0);
+ *
+ * // Iterate over the vertices and vertexNormals arrays.
+ * for (let i = 0; i < myGeometry.vertices.length; i += 1) {
+ *
+ * // Get the vertex p5.Vector object.
+ * let v = myGeometry.vertices[i];
+ *
+ * // Get the vertex normal p5.Vector object.
+ * let n = myGeometry.vertexNormals[i];
+ *
+ * // Calculate a point along the vertex normal.
+ * let p = p5.Vector.mult(n, 5);
+ *
+ * // Draw the vertex normal as a red line.
+ * push();
+ * translate(v);
+ * line(0, 0, 0, p.x, p.y, p.z);
+ * pop();
+ * }
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object using a callback function.
+ * myGeometry = new p5.Geometry();
+ *
+ * // Create p5.Vector objects to position the vertices.
+ * let v0 = createVector(-40, 0, 0);
+ * let v1 = createVector(0, -40, 0);
+ * let v2 = createVector(0, 40, 0);
+ * let v3 = createVector(40, 0, 0);
+ *
+ * // Add the vertices to the p5.Geometry object's vertices array.
+ * myGeometry.vertices.push(v0, v1, v2, v3);
+ *
+ * // Compute the faces array.
+ * myGeometry.computeFaces();
+ *
+ * // Compute the surface normals.
+ * myGeometry.computeNormals();
+ *
+ * describe('A red square drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Add a white point light.
+ * pointLight(255, 255, 255, 0, 0, 10);
+ *
+ * // Style the p5.Geometry object.
+ * noStroke();
+ * fill(255, 0, 0);
+ *
+ * // Draw the p5.Geometry object.
+ * model(myGeometry);
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = buildGeometry(createShape);
+ *
+ * // Compute normals using default (FLAT) shading.
+ * myGeometry.computeNormals(FLAT);
+ *
+ * describe('A white, helical structure drawn on a dark gray background. Its faces appear faceted.');
+ * }
+ *
+ * function draw() {
+ * background(50);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Rotate the coordinate system.
+ * rotateX(1);
+ *
+ * // Style the helix.
+ * noStroke();
+ *
+ * // Display the helix.
+ * model(myGeometry);
+ * }
+ *
+ * function createShape() {
+ * // Create a helical shape.
+ * beginShape();
+ * for (let i = 0; i < TWO_PI * 3; i += 0.5) {
+ * let x = 30 * cos(i);
+ * let y = 30 * sin(i);
+ * let z = map(i, 0, TWO_PI * 3, -40, 40);
+ * vertex(x, y, z);
+ * }
+ * endShape();
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = buildGeometry(createShape);
+ *
+ * // Compute normals using smooth shading.
+ * myGeometry.computeNormals(SMOOTH);
+ *
+ * describe('A white, helical structure drawn on a dark gray background.');
+ * }
+ *
+ * function draw() {
+ * background(50);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Rotate the coordinate system.
+ * rotateX(1);
+ *
+ * // Style the helix.
+ * noStroke();
+ *
+ * // Display the helix.
+ * model(myGeometry);
+ * }
+ *
+ * function createShape() {
+ * // Create a helical shape.
+ * beginShape();
+ * for (let i = 0; i < TWO_PI * 3; i += 0.5) {
+ * let x = 30 * cos(i);
+ * let y = 30 * sin(i);
+ * let z = map(i, 0, TWO_PI * 3, -40, 40);
+ * vertex(x, y, z);
+ * }
+ * endShape();
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = buildGeometry(createShape);
+ *
+ * // Create an options object.
+ * let options = { roundToPrecision: 5 };
+ *
+ * // Compute normals using smooth shading.
+ * myGeometry.computeNormals(SMOOTH, options);
+ *
+ * describe('A white, helical structure drawn on a dark gray background.');
+ * }
+ *
+ * function draw() {
+ * background(50);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Rotate the coordinate system.
+ * rotateX(1);
+ *
+ * // Style the helix.
+ * noStroke();
+ *
+ * // Display the helix.
+ * model(myGeometry);
+ * }
+ *
+ * function createShape() {
+ * // Create a helical shape.
+ * beginShape();
+ * for (let i = 0; i < TWO_PI * 3; i += 0.5) {
+ * let x = 30 * cos(i);
+ * let y = 30 * sin(i);
+ * let z = map(i, 0, TWO_PI * 3, -40, 40);
+ * vertex(x, y, z);
+ * }
+ * endShape();
+ * }
+ */
+ computeNormals(shadingType = FLAT, { roundToPrecision = 3 } = {}) {
+ const vertexNormals = this.vertexNormals;
+ let vertices = this.vertices;
+ const faces = this.faces;
+ let iv;
+
+ if (shadingType === SMOOTH) {
+ const vertexIndices = {};
+ const uniqueVertices = [];
+
+ const power = Math.pow(10, roundToPrecision);
+ const rounded = val => Math.round(val * power) / power;
+ const getKey = vert =>
+ `${rounded(vert.x)},${rounded(vert.y)},${rounded(vert.z)}`;
+
+ // loop through each vertex and add uniqueVertices
+ for (let i = 0; i < vertices.length; i++) {
+ const vertex = vertices[i];
+ const key = getKey(vertex);
+ if (vertexIndices[key] === undefined) {
+ vertexIndices[key] = uniqueVertices.length;
+ uniqueVertices.push(vertex);
+ }
+ }
+
+ // update face indices to use the deduplicated vertex indices
+ faces.forEach(face => {
+ for (let fv = 0; fv < 3; ++fv) {
+ const originalVertexIndex = face[fv];
+ const originalVertex = vertices[originalVertexIndex];
+ const key = getKey(originalVertex);
+ face[fv] = vertexIndices[key];
+ }
+ });
+
+ // update edge indices to use the deduplicated vertex indices
+ this.edges.forEach(edge => {
+ for (let ev = 0; ev < 2; ++ev) {
+ const originalVertexIndex = edge[ev];
+ const originalVertex = vertices[originalVertexIndex];
+ const key = getKey(originalVertex);
+ edge[ev] = vertexIndices[key];
+ }
+ });
+
+ // update the deduplicated vertices
+ this.vertices = vertices = uniqueVertices;
+ }
+
+ // initialize the vertexNormals array with empty vectors
+ vertexNormals.length = 0;
+ for (iv = 0; iv < vertices.length; ++iv) {
+ vertexNormals.push(new Vector());
+ }
+
+ // loop through all the faces adding its normal to the normal
+ // of each of its vertices
+ faces.forEach((face, f) => {
+ const faceNormal = this._getFaceNormal(f);
+
+ // all three vertices get the normal added
+ for (let fv = 0; fv < 3; ++fv) {
+ const vertexIndex = face[fv];
+ vertexNormals[vertexIndex].add(faceNormal);
+ }
+ });
+
+ // normalize the normals
+ for (iv = 0; iv < vertices.length; ++iv) {
+ vertexNormals[iv].normalize();
+ }
+
+ return this;
+ }
+
+ /**
+ * Averages the vertex normals. Used in curved
+ * surfaces
+ * @private
+ * @chainable
+ */
+ averageNormals() {
+ for (let i = 0; i <= this.detailY; i++) {
+ const offset = this.detailX + 1;
+ let temp = Vector.add(
+ this.vertexNormals[i * offset],
+ this.vertexNormals[i * offset + this.detailX]
+ );
+
+ temp = Vector.div(temp, 2);
+ this.vertexNormals[i * offset] = temp;
+ this.vertexNormals[i * offset + this.detailX] = temp;
+ }
+ return this;
+ }
+
+ /**
+ * Averages pole normals. Used in spherical primitives
+ * @private
+ * @chainable
+ */
+ averagePoleNormals() {
+ //average the north pole
+ let sum = new Vector(0, 0, 0);
+ for (let i = 0; i < this.detailX; i++) {
+ sum.add(this.vertexNormals[i]);
+ }
+ sum = Vector.div(sum, this.detailX);
+
+ for (let i = 0; i < this.detailX; i++) {
+ this.vertexNormals[i] = sum;
+ }
+
+ //average the south pole
+ sum = new Vector(0, 0, 0);
+ for (
+ let i = this.vertices.length - 1;
+ i > this.vertices.length - 1 - this.detailX;
+ i--
+ ) {
+ sum.add(this.vertexNormals[i]);
+ }
+ sum = Vector.div(sum, this.detailX);
+
+ for (
+ let i = this.vertices.length - 1;
+ i > this.vertices.length - 1 - this.detailX;
+ i--
+ ) {
+ this.vertexNormals[i] = sum;
+ }
+ return this;
+ }
+
+ /**
+ * Create a 2D array for establishing stroke connections
+ * @private
+ * @chainable
+ */
+ _makeTriangleEdges() {
+ this.edges.length = 0;
+
+ for (let j = 0; j < this.faces.length; j++) {
+ this.edges.push([this.faces[j][0], this.faces[j][1]]);
+ this.edges.push([this.faces[j][1], this.faces[j][2]]);
+ this.edges.push([this.faces[j][2], this.faces[j][0]]);
+ }
+
+ return this;
+ }
+
+ /**
+ * @example
+ * let tetrahedron;
+ * function setup() {
+ * createCanvas(200, 200, WEBGL);
+ * describe('A rotating tetrahedron');
+ *
+ * tetrahedron = new p5.Geometry();
+ *
+ * // Give each geometry a unique gid
+ * tetrahedron.gid = 'tetrahedron';
+ *
+ * // Add four points of the tetrahedron
+ *
+ * let radius = 50;
+ * // A 2D triangle:
+ * tetrahedron.vertices.push(createVector(radius, 0, 0));
+ * tetrahedron.vertices.push(createVector(radius, 0, 0).rotate(TWO_PI / 3));
+ * tetrahedron.vertices.push(createVector(radius, 0, 0).rotate(TWO_PI * 2 / 3));
+ * // Add a tip in the z axis:
+ * tetrahedron.vertices.push(createVector(0, 0, radius));
+ *
+ * // Create the four faces by connecting the sets of three points
+ * tetrahedron.faces.push([0, 1, 2]);
+ * tetrahedron.faces.push([0, 1, 3]);
+ * tetrahedron.faces.push([0, 2, 3]);
+ * tetrahedron.faces.push([1, 2, 3]);
+ * tetrahedron.makeEdgesFromFaces();
+ * }
+ *
+ * function draw() {
+ * background(200);
+ * strokeWeight(2);
+ * orbitControl();
+ * rotateY(millis() * 0.001);
+ * model(tetrahedron);
+ * }
+ */
+ makeEdgesFromFaces() {
+ this._makeTriangleEdges();
+ }
+
+ /**
+ * Converts each line segment into the vertices and vertex attributes needed
+ * to turn the line into a polygon on screen. This will include:
+ * - Two triangles line segment to create a rectangle
+ * - Two triangles per endpoint to create a stroke cap rectangle. A fragment
+ * shader is responsible for displaying the appropriate cap style within
+ * that rectangle.
+ * - Four triangles per join between adjacent line segments, creating a quad on
+ * either side of the join, perpendicular to the lines. A vertex shader will
+ * discard the quad in the "elbow" of the join, and a fragment shader will
+ * display the appropriate join style within the remaining quad.
+ *
+ * @private
+ * @chainable
+ */
+ _edgesToVertices() {
+ this.lineVertices.clear();
+ this.lineTangentsIn.clear();
+ this.lineTangentsOut.clear();
+ this.lineSides.clear();
+
+ const potentialCaps = new Map();
+ const connected = new Set();
+ let lastValidDir;
+ for (let i = 0; i < this.edges.length; i++) {
+ const prevEdge = this.edges[i - 1];
+ const currEdge = this.edges[i];
+ const isPoint = currEdge[0] === currEdge[1];
+ const begin = this.vertices[currEdge[0]];
+ const end = this.vertices[currEdge[1]];
+ const prevColor = (this.vertexStrokeColors.length > 0 && prevEdge)
+ ? this.vertexStrokeColors.slice(
+ prevEdge[1] * 4,
+ (prevEdge[1] + 1) * 4
+ )
+ : [0, 0, 0, 0];
+ const fromColor = this.vertexStrokeColors.length > 0
+ ? this.vertexStrokeColors.slice(
+ currEdge[0] * 4,
+ (currEdge[0] + 1) * 4
+ )
+ : [0, 0, 0, 0];
+ const toColor = this.vertexStrokeColors.length > 0
+ ? this.vertexStrokeColors.slice(
+ currEdge[1] * 4,
+ (currEdge[1] + 1) * 4
+ )
+ : [0, 0, 0, 0];
+ const dir = isPoint
+ ? new Vector(0, 1, 0)
+ : end
+ .copy()
+ .sub(begin)
+ .normalize();
+ const dirOK = dir.magSq() > 0;
+ if (dirOK) {
+ this._addSegment(begin, end, fromColor, toColor, dir);
+ }
+ if (!this.renderer?._simpleLines) {
+ if (i > 0 && prevEdge[1] === currEdge[0]) {
+ if (!connected.has(currEdge[0])) {
+ connected.add(currEdge[0]);
+ potentialCaps.delete(currEdge[0]);
+ // Add a join if this segment shares a vertex with the previous. Skip
+ // actually adding join vertices if either the previous segment or this
+ // one has a length of 0.
+ //
+ // Don't add a join if the tangents point in the same direction, which
+ // would mean the edges line up exactly, and there is no need for a join.
+ if (lastValidDir && dirOK && dir.dot(lastValidDir) < 1 - 1e-8) {
+ this._addJoin(begin, lastValidDir, dir, fromColor);
+ }
+ }
+ } else if (isPoint) {
+ this._addCap(begin, dir.copy().mult(-1), fromColor);
+ this._addCap(begin, dir, fromColor);
+ } else {
+ // Start a new line
+ if (dirOK && !connected.has(currEdge[0])) {
+ const existingCap = potentialCaps.get(currEdge[0]);
+ if (existingCap) {
+ this._addJoin(
+ begin,
+ existingCap.dir,
+ dir,
+ fromColor
+ );
+ potentialCaps.delete(currEdge[0]);
+ connected.add(currEdge[0]);
+ } else {
+ potentialCaps.set(currEdge[0], {
+ point: begin,
+ dir: dir.copy().mult(-1),
+ color: fromColor
+ });
+ }
+ }
+ if (!isPoint && lastValidDir && !connected.has(prevEdge[1])) {
+ const existingCap = potentialCaps.get(prevEdge[1]);
+ if (existingCap) {
+ this._addJoin(
+ this.vertices[prevEdge[1]],
+ lastValidDir,
+ existingCap.dir.copy().mult(-1),
+ prevColor
+ );
+ potentialCaps.delete(prevEdge[1]);
+ connected.add(prevEdge[1]);
+ } else {
+ // Close off the last segment with a cap
+ potentialCaps.set(prevEdge[1], {
+ point: this.vertices[prevEdge[1]],
+ dir: lastValidDir,
+ color: prevColor
+ });
+ }
+ lastValidDir = undefined;
+ }
+ }
+
+ if (i === this.edges.length - 1 && !connected.has(currEdge[1])) {
+ const existingCap = potentialCaps.get(currEdge[1]);
+ if (existingCap) {
+ this._addJoin(
+ end,
+ dir,
+ existingCap.dir.copy().mult(-1),
+ toColor
+ );
+ potentialCaps.delete(currEdge[1]);
+ connected.add(currEdge[1]);
+ } else {
+ potentialCaps.set(currEdge[1], {
+ point: end,
+ dir,
+ color: toColor
+ });
+ }
+ }
+
+ if (dirOK) {
+ lastValidDir = dir;
+ }
+ }
+ }
+ for (const { point, dir, color } of potentialCaps.values()) {
+ this._addCap(point, dir, color);
+ }
+ return this;
+ }
+
+ /**
+ * Adds the vertices and vertex attributes for two triangles making a rectangle
+ * for a straight line segment. A vertex shader is responsible for picking
+ * proper coordinates on the screen given the centerline positions, the tangent,
+ * and the side of the centerline each vertex belongs to. Sides follow the
+ * following scheme:
+ *
+ * -1 -1
+ * o-------------o
+ * | |
+ * o-------------o
+ * 1 1
+ *
+ * @private
+ * @chainable
+ */
+ _addSegment(
+ begin,
+ end,
+ fromColor,
+ toColor,
+ dir
+ ) {
+ const a = begin.array();
+ const b = end.array();
+ const dirArr = dir.array();
+ this.lineSides.push(1, 1, -1, 1, -1, -1);
+ for (const tangents of [this.lineTangentsIn, this.lineTangentsOut]) {
+ for (let i = 0; i < 6; i++) {
+ tangents.push(...dirArr);
+ }
+ }
+ this.lineVertices.push(...a, ...b, ...a, ...b, ...b, ...a);
+ if (!this.renderer?._simpleLines) {
+ this.lineVertexColors.push(
+ ...fromColor,
+ ...toColor,
+ ...fromColor,
+ ...toColor,
+ ...toColor,
+ ...fromColor
+ );
+ }
+ return this;
+ }
+
+ /**
+ * Adds the vertices and vertex attributes for two triangles representing the
+ * stroke cap of a line. A fragment shader is responsible for displaying the
+ * appropriate cap style within the rectangle they make.
+ *
+ * The lineSides buffer will include the following values for the points on
+ * the cap rectangle:
+ *
+ * -1 -2
+ * -----------o---o
+ * | |
+ * -----------o---o
+ * 1 2
+ * @private
+ * @chainable
+ */
+ _addCap(point, tangent, color) {
+ const ptArray = point.array();
+ const tanInArray = tangent.array();
+ const tanOutArray = [0, 0, 0];
+ for (let i = 0; i < 6; i++) {
+ this.lineVertices.push(...ptArray);
+ this.lineTangentsIn.push(...tanInArray);
+ this.lineTangentsOut.push(...tanOutArray);
+ this.lineVertexColors.push(...color);
+ }
+ this.lineSides.push(-1, 2, -2, 1, 2, -1);
+ return this;
+ }
+
+ /**
+ * Adds the vertices and vertex attributes for four triangles representing a
+ * join between two adjacent line segments. This creates a quad on either side
+ * of the shared vertex of the two line segments, with each quad perpendicular
+ * to the lines. A vertex shader will discard all but the quad in the "elbow" of
+ * the join, and a fragment shader will display the appropriate join style
+ * within the remaining quad.
+ *
+ * The lineSides buffer will include the following values for the points on
+ * the join rectangles:
+ *
+ * -1 -2
+ * -------------o----o
+ * | |
+ * 1 o----o----o -3
+ * | | 0 |
+ * --------o----o |
+ * 2| 3 |
+ * | |
+ * | |
+ * @private
+ * @chainable
+ */
+ _addJoin(
+ point,
+ fromTangent,
+ toTangent,
+ color
+ ) {
+ const ptArray = point.array();
+ const tanInArray = fromTangent.array();
+ const tanOutArray = toTangent.array();
+ for (let i = 0; i < 12; i++) {
+ this.lineVertices.push(...ptArray);
+ this.lineTangentsIn.push(...tanInArray);
+ this.lineTangentsOut.push(...tanOutArray);
+ this.lineVertexColors.push(...color);
+ }
+ this.lineSides.push(-1, -3, -2, -1, 0, -3);
+ this.lineSides.push(3, 1, 2, 3, 0, 1);
+ return this;
+ }
+
+ /**
+ * Transforms the geometry's vertices to fit snugly within a 100×100×100 box
+ * centered at the origin.
+ *
+ * Calling `myGeometry.normalize()` translates the geometry's vertices so that
+ * they're centered at the origin `(0, 0, 0)`. Then it scales the vertices so
+ * that they fill a 100×100×100 box. As a result, small geometries will grow
+ * and large geometries will shrink.
+ *
+ * Note: `myGeometry.normalize()` only works when called in the
+ * setup() function.
+ *
+ * @chainable
+ *
+ * @example
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a very small torus.
+ * myGeometry = buildGeometry(function() {;
+ * torus(1, 0.25);
+ * });
+ *
+ * // Normalize the torus so its vertices fill
+ * // the range [-100, 100].
+ * myGeometry.normalize();
+ *
+ * describe('A white torus rotates slowly against a dark gray background.');
+ * }
+ *
+ * function draw() {
+ * background(50);
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Style the torus.
+ * noStroke();
+ *
+ * // Draw the torus.
+ * model(myGeometry);
+ * }
+ */
+ normalize() {
+ if (this.vertices.length > 0) {
+ // Find the corners of our bounding box
+ const maxPosition = this.vertices[0].copy();
+ const minPosition = this.vertices[0].copy();
+
+ for (let i = 0; i < this.vertices.length; i++) {
+ maxPosition.x = Math.max(maxPosition.x, this.vertices[i].x);
+ minPosition.x = Math.min(minPosition.x, this.vertices[i].x);
+ maxPosition.y = Math.max(maxPosition.y, this.vertices[i].y);
+ minPosition.y = Math.min(minPosition.y, this.vertices[i].y);
+ maxPosition.z = Math.max(maxPosition.z, this.vertices[i].z);
+ minPosition.z = Math.min(minPosition.z, this.vertices[i].z);
+ }
+
+ const center = Vector.lerp(maxPosition, minPosition, 0.5);
+ const dist = Vector.sub(maxPosition, minPosition);
+ const longestDist = Math.max(Math.max(dist.x, dist.y), dist.z);
+ const scale = 200 / longestDist;
+
+ for (let i = 0; i < this.vertices.length; i++) {
+ this.vertices[i].sub(center);
+ this.vertices[i].mult(scale);
+ }
+ }
+ return this;
+ }
+
+ /** Sets the shader's vertex property or attribute variables.
+ *
+ * A vertex property, or vertex attribute, is a variable belonging to a vertex in a shader. p5.js provides some
+ * default properties, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are
+ * set using vertex(), normal()
+ * and fill() respectively. Custom properties can also
+ * be defined within beginShape() and
+ * endShape().
+ *
+ * The first parameter, `propertyName`, is a string with the property's name.
+ * This is the same variable name which should be declared in the shader, as in
+ * `in vec3 aProperty`, similar to .`setUniform()`.
+ *
+ * The second parameter, `data`, is the value assigned to the shader variable. This value
+ * will be pushed directly onto the Geometry object. There should be the same number
+ * of custom property values as vertices, this method should be invoked once for each
+ * vertex.
+ *
+ * The `data` can be a Number or an array of numbers. Tn the shader program the type
+ * can be declared according to the WebGL specification. Common types include `float`,
+ * `vec2`, `vec3`, `vec4` or matrices.
+ *
+ * See also the global vertexProperty() function.
+ *
+ * @example
+ * let geo;
+ *
+ * function cartesianToSpherical(x, y, z) {
+ * let r = sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2));
+ * let theta = acos(z / r);
+ * let phi = atan2(y, x);
+ * return { theta, phi };
+ * }
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Modify the material shader to display roughness.
+ * const myShader = baseMaterialShader().modify({
+ * vertexDeclarations:`in float aRoughness;
+ * out float vRoughness;`,
+ * fragmentDeclarations: 'in float vRoughness;',
+ * 'void afterVertex': `() {
+ * vRoughness = aRoughness;
+ * }`,
+ * 'vec4 combineColors': `(ColorComponents components) {
+ * vec4 color = vec4(0.);
+ * color.rgb += components.diffuse * components.baseColor * (1.0-vRoughness);
+ * color.rgb += components.ambient * components.ambientColor;
+ * color.rgb += components.specular * components.specularColor * (1.0-vRoughness);
+ * color.a = components.opacity;
+ * return color;
+ * }`
+ * });
+ *
+ * // Create the Geometry object.
+ * geo = buildGeometry(function() {
+ * fill('hotpink');
+ * sphere(45, 50, 50);
+ * });
+ *
+ * // Set the roughness value for every vertex.
+ * for (let v of geo.vertices){
+ *
+ * // convert coordinates to spherical coordinates
+ * let spherical = cartesianToSpherical(v.x, v.y, v.z);
+ *
+ * // Set the custom roughness vertex property.
+ * let roughness = noise(spherical.theta*5, spherical.phi*5);
+ * geo.vertexProperty('aRoughness', roughness);
+ * }
+ *
+ * // Use the custom shader.
+ * shader(myShader);
+ *
+ * describe('A rough pink sphere rotating on a blue background.');
+ * }
+ *
+ * function draw() {
+ * // Set some styles and lighting
+ * background('lightblue');
+ * noStroke();
+ *
+ * specularMaterial(255,125,100);
+ * shininess(2);
+ *
+ * directionalLight('white', -1, 1, -1);
+ * ambientLight(320);
+ *
+ * rotateY(millis()*0.001);
+ *
+ * // Draw the geometry
+ * model(geo);
+ * }
+ *
+ * @param {String} propertyName the name of the vertex property.
+ * @param {Number|Number[]} data the data tied to the vertex property.
+ * @param {Number} [size] optional size of each unit of data.
+ */
+ vertexProperty(propertyName, data, size){
+ let prop;
+ if (!this.userVertexProperties[propertyName]){
+ prop = this.userVertexProperties[propertyName] =
+ this._userVertexPropertyHelper(propertyName, data, size);
+ }
+ prop = this.userVertexProperties[propertyName];
+ if (size){
+ prop.pushDirect(data);
+ } else {
+ prop.setCurrentData(data);
+ prop.pushCurrentData();
+ }
+ }
+
+ _userVertexPropertyHelper(propertyName, data, size){
+ const geometryInstance = this;
+ const prop = this.userVertexProperties[propertyName] = {
+ name: propertyName,
+ dataSize: size ? size : data.length ? data.length : 1,
+ geometry: geometryInstance,
+ // Getters
+ getName(){
+ return this.name;
+ },
+ getCurrentData(){
+ if (this.currentData === undefined) {
+ this.currentData = new Array(this.getDataSize()).fill(0);
+ }
+ return this.currentData;
+ },
+ getDataSize() {
+ return this.dataSize;
+ },
+ getSrcName() {
+ const src = this.name.concat('Src');
+ return src;
+ },
+ getDstName() {
+ const dst = this.name.concat('Buffer');
+ return dst;
+ },
+ getSrcArray() {
+ const srcName = this.getSrcName();
+ return this.geometry[srcName];
+ },
+ //Setters
+ setCurrentData(data) {
+ // if (size != this.getDataSize()){
+ // p5._friendlyError(`Custom vertex property '${this.name}' has been set with various data sizes. You can change it's name, or if it was an accident, set '${this.name}' to have the same number of inputs each time!`, 'vertexProperty()');
+ // }
+ this.currentData = data;
+ },
+ // Utilities
+ pushCurrentData(){
+ const data = this.getCurrentData();
+ this.pushDirect(data);
+ },
+ pushDirect(data) {
+ if (data.length){
+ this.getSrcArray().push(...data);
+ } else {
+ this.getSrcArray().push(data);
+ }
+ },
+ resetSrcArray(){
+ this.geometry[this.getSrcName()] = [];
+ },
+ delete() {
+ const srcName = this.getSrcName();
+ delete this.geometry[srcName];
+ delete this;
+ }
+ };
+ this[prop.getSrcName()] = [];
+ return this.userVertexProperties[propertyName];
+ }
+ }
+ /**
+ * Keeps track of how many custom geometry objects have been made so that each
+ * can be assigned a unique ID.
+ */
+ Geometry.nextId = 0;
+
+ function geometry(p5, fn){
+ /**
+ * A class to describe a 3D shape.
+ *
+ * Each `p5.Geometry` object represents a 3D shape as a set of connected
+ * points called *vertices*. All 3D shapes are made by connecting vertices to
+ * form triangles that are stitched together. Each triangular patch on the
+ * geometry's surface is called a *face*. The geometry stores information
+ * about its vertices and faces for use with effects such as lighting and
+ * texture mapping.
+ *
+ * The first parameter, `detailX`, is optional. If a number is passed, as in
+ * `new p5.Geometry(24)`, it sets the number of triangle subdivisions to use
+ * along the geometry's x-axis. By default, `detailX` is 1.
+ *
+ * The second parameter, `detailY`, is also optional. If a number is passed,
+ * as in `new p5.Geometry(24, 16)`, it sets the number of triangle
+ * subdivisions to use along the geometry's y-axis. By default, `detailX` is
+ * 1.
+ *
+ * The third parameter, `callback`, is also optional. If a function is passed,
+ * as in `new p5.Geometry(24, 16, createShape)`, it will be called once to add
+ * vertices to the new 3D shape.
+ *
+ * @class p5.Geometry
+ * @param {Integer} [detailX] number of vertices along the x-axis.
+ * @param {Integer} [detailY] number of vertices along the y-axis.
+ * @param {Function} [callback] function to call once the geometry is created.
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = new p5.Geometry();
+ *
+ * // Create p5.Vector objects to position the vertices.
+ * let v0 = createVector(-40, 0, 0);
+ * let v1 = createVector(0, -40, 0);
+ * let v2 = createVector(40, 0, 0);
+ *
+ * // Add the vertices to the p5.Geometry object's vertices array.
+ * myGeometry.vertices.push(v0, v1, v2);
+ *
+ * describe('A white triangle drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Draw the p5.Geometry object.
+ * model(myGeometry);
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object using a callback function.
+ * myGeometry = new p5.Geometry(1, 1, createShape);
+ *
+ * describe('A white triangle drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Draw the p5.Geometry object.
+ * model(myGeometry);
+ * }
+ *
+ * function createShape() {
+ * // Create p5.Vector objects to position the vertices.
+ * let v0 = createVector(-40, 0, 0);
+ * let v1 = createVector(0, -40, 0);
+ * let v2 = createVector(40, 0, 0);
+ *
+ * // "this" refers to the p5.Geometry object being created.
+ *
+ * // Add the vertices to the p5.Geometry object's vertices array.
+ * this.vertices.push(v0, v1, v2);
+ *
+ * // Add an array to list which vertices belong to the face.
+ * // Vertices are listed in clockwise "winding" order from
+ * // left to top to right.
+ * this.faces.push([0, 1, 2]);
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object using a callback function.
+ * myGeometry = new p5.Geometry(1, 1, createShape);
+ *
+ * describe('A white triangle drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Draw the p5.Geometry object.
+ * model(myGeometry);
+ * }
+ *
+ * function createShape() {
+ * // Create p5.Vector objects to position the vertices.
+ * let v0 = createVector(-40, 0, 0);
+ * let v1 = createVector(0, -40, 0);
+ * let v2 = createVector(40, 0, 0);
+ *
+ * // "this" refers to the p5.Geometry object being created.
+ *
+ * // Add the vertices to the p5.Geometry object's vertices array.
+ * this.vertices.push(v0, v1, v2);
+ *
+ * // Add an array to list which vertices belong to the face.
+ * // Vertices are listed in clockwise "winding" order from
+ * // left to top to right.
+ * this.faces.push([0, 1, 2]);
+ *
+ * // Compute the surface normals to help with lighting.
+ * this.computeNormals();
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * // Adapted from Paul Wheeler's wonderful p5.Geometry tutorial.
+ * // https://www.paulwheeler.us/articles/custom-3d-geometry-in-p5js/
+ * // CC-BY-SA 4.0
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the p5.Geometry object.
+ * // Set detailX to 48 and detailY to 2.
+ * // >>> try changing them.
+ * myGeometry = new p5.Geometry(48, 2, createShape);
+ * }
+ *
+ * function draw() {
+ * background(50);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Style the p5.Geometry object.
+ * strokeWeight(0.2);
+ *
+ * // Draw the p5.Geometry object.
+ * model(myGeometry);
+ * }
+ *
+ * function createShape() {
+ * // "this" refers to the p5.Geometry object being created.
+ *
+ * // Define the Möbius strip with a few parameters.
+ * let spread = 0.1;
+ * let radius = 30;
+ * let stripWidth = 15;
+ * let xInterval = 4 * PI / this.detailX;
+ * let yOffset = -stripWidth / 2;
+ * let yInterval = stripWidth / this.detailY;
+ *
+ * for (let j = 0; j <= this.detailY; j += 1) {
+ * // Calculate the "vertical" point along the strip.
+ * let v = yOffset + yInterval * j;
+ *
+ * for (let i = 0; i <= this.detailX; i += 1) {
+ * // Calculate the angle of rotation around the strip.
+ * let u = i * xInterval;
+ *
+ * // Calculate the coordinates of the vertex.
+ * let x = (radius + v * cos(u / 2)) * cos(u) - sin(u / 2) * 2 * spread;
+ * let y = (radius + v * cos(u / 2)) * sin(u);
+ * if (u < TWO_PI) {
+ * y += sin(u) * spread;
+ * } else {
+ * y -= sin(u) * spread;
+ * }
+ * let z = v * sin(u / 2) + sin(u / 4) * 4 * spread;
+ *
+ * // Create a p5.Vector object to position the vertex.
+ * let vert = createVector(x, y, z);
+ *
+ * // Add the vertex to the p5.Geometry object's vertices array.
+ * this.vertices.push(vert);
+ * }
+ * }
+ *
+ * // Compute the faces array.
+ * this.computeFaces();
+ *
+ * // Compute the surface normals to help with lighting.
+ * this.computeNormals();
+ * }
+ */
+ p5.Geometry = Geometry;
+
+ /**
+ * An array with the geometry's vertices.
+ *
+ * The geometry's vertices are stored as
+ * p5.Vector objects in the `myGeometry.vertices`
+ * array. The geometry's first vertex is the
+ * p5.Vector object at `myGeometry.vertices[0]`,
+ * its second vertex is `myGeometry.vertices[1]`, its third vertex is
+ * `myGeometry.vertices[2]`, and so on.
+ *
+ * @property vertices
+ * @for p5.Geometry
+ * @name vertices
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = new p5.Geometry();
+ *
+ * // Create p5.Vector objects to position the vertices.
+ * let v0 = createVector(-40, 0, 0);
+ * let v1 = createVector(0, -40, 0);
+ * let v2 = createVector(40, 0, 0);
+ *
+ * // Add the vertices to the p5.Geometry object's vertices array.
+ * myGeometry.vertices.push(v0, v1, v2);
+ *
+ * describe('A white triangle drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Draw the p5.Geometry object.
+ * model(myGeometry);
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = buildGeometry(function() {
+ * torus(30, 15, 10, 8);
+ * });
+ *
+ * describe('A white torus rotates slowly against a dark gray background. Red spheres mark its vertices.');
+ * }
+ *
+ * function draw() {
+ * background(50);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Rotate the coordinate system.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Style the p5.Geometry object.
+ * fill(255);
+ * stroke(0);
+ *
+ * // Display the p5.Geometry object.
+ * model(myGeometry);
+ *
+ * // Style the vertices.
+ * fill(255, 0, 0);
+ * noStroke();
+ *
+ * // Iterate over the vertices array.
+ * for (let v of myGeometry.vertices) {
+ * // Draw a sphere to mark the vertex.
+ * push();
+ * translate(v);
+ * sphere(2.5);
+ * pop();
+ * }
+ * }
+ */
+
+ /**
+ * An array with the vectors that are normal to the geometry's vertices.
+ *
+ * A face's orientation is defined by its *normal vector* which points out
+ * of the face and is normal (perpendicular) to the surface. Calling
+ * `myGeometry.computeNormals()` first calculates each face's normal
+ * vector. Then it calculates the normal vector for each vertex by
+ * averaging the normal vectors of the faces surrounding the vertex. The
+ * vertex normals are stored as p5.Vector
+ * objects in the `myGeometry.vertexNormals` array.
+ *
+ * @property vertexNormals
+ * @name vertexNormals
+ * @for p5.Geometry
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = buildGeometry(function() {
+ * torus(30, 15, 10, 8);
+ * });
+ *
+ * // Compute the vertex normals.
+ * myGeometry.computeNormals();
+ *
+ * describe(
+ * 'A white torus rotates against a dark gray background. Red lines extend outward from its vertices.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(50);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Rotate the coordinate system.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Style the p5.Geometry object.
+ * stroke(0);
+ *
+ * // Display the p5.Geometry object.
+ * model(myGeometry);
+ *
+ * // Style the normal vectors.
+ * stroke(255, 0, 0);
+ *
+ * // Iterate over the vertices and vertexNormals arrays.
+ * for (let i = 0; i < myGeometry.vertices.length; i += 1) {
+ *
+ * // Get the vertex p5.Vector object.
+ * let v = myGeometry.vertices[i];
+ *
+ * // Get the vertex normal p5.Vector object.
+ * let n = myGeometry.vertexNormals[i];
+ *
+ * // Calculate a point along the vertex normal.
+ * let p = p5.Vector.mult(n, 8);
+ *
+ * // Draw the vertex normal as a red line.
+ * push();
+ * translate(v);
+ * line(0, 0, 0, p.x, p.y, p.z);
+ * pop();
+ * }
+ * }
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = new p5.Geometry();
+ *
+ * // Create p5.Vector objects to position the vertices.
+ * let v0 = createVector(-40, 0, 0);
+ * let v1 = createVector(0, -40, 0);
+ * let v2 = createVector(0, 40, 0);
+ * let v3 = createVector(40, 0, 0);
+ *
+ * // Add the vertices to the p5.Geometry object's vertices array.
+ * myGeometry.vertices.push(v0, v1, v2, v3);
+ *
+ * // Compute the faces array.
+ * myGeometry.computeFaces();
+ *
+ * // Compute the surface normals.
+ * myGeometry.computeNormals();
+ *
+ * describe('A red square drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Add a white point light.
+ * pointLight(255, 255, 255, 0, 0, 10);
+ *
+ * // Style the p5.Geometry object.
+ * noStroke();
+ * fill(255, 0, 0);
+ *
+ * // Display the p5.Geometry object.
+ * model(myGeometry);
+ * }
+ */
+
+ /**
+ * An array that lists which of the geometry's vertices form each of its
+ * faces.
+ *
+ * All 3D shapes are made by connecting sets of points called *vertices*. A
+ * geometry's surface is formed by connecting vertices to form triangles
+ * that are stitched together. Each triangular patch on the geometry's
+ * surface is called a *face*.
+ *
+ * The geometry's vertices are stored as
+ * p5.Vector objects in the
+ * myGeometry.vertices array. The
+ * geometry's first vertex is the p5.Vector
+ * object at `myGeometry.vertices[0]`, its second vertex is
+ * `myGeometry.vertices[1]`, its third vertex is `myGeometry.vertices[2]`,
+ * and so on.
+ *
+ * For example, a geometry made from a rectangle has two faces because a
+ * rectangle is made by joining two triangles. `myGeometry.faces` for a
+ * rectangle would be the two-dimensional array `[[0, 1, 2], [2, 1, 3]]`.
+ * The first face, `myGeometry.faces[0]`, is the array `[0, 1, 2]` because
+ * it's formed by connecting `myGeometry.vertices[0]`,
+ * `myGeometry.vertices[1]`,and `myGeometry.vertices[2]`. The second face,
+ * `myGeometry.faces[1]`, is the array `[2, 1, 3]` because it's formed by
+ * connecting `myGeometry.vertices[2]`, `myGeometry.vertices[1]`,and
+ * `myGeometry.vertices[3]`.
+ *
+ * @property faces
+ * @name faces
+ * @for p5.Geometry
+ *
+ * @example
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * let myGeometry;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Geometry object.
+ * myGeometry = buildGeometry(function() {
+ * sphere();
+ * });
+ *
+ * describe("A sphere drawn on a gray background. The sphere's surface is a grayscale patchwork of triangles.");
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Turn on the lights.
+ * lights();
+ *
+ * // Style the p5.Geometry object.
+ * noStroke();
+ *
+ * // Set a random seed.
+ * randomSeed(1234);
+ *
+ * // Iterate over the faces array.
+ * for (let face of myGeometry.faces) {
+ *
+ * // Style the face.
+ * let g = random(0, 255);
+ * fill(g);
+ *
+ * // Draw the face.
+ * beginShape();
+ * // Iterate over the vertices that form the face.
+ * for (let f of face) {
+ * // Get the vertex's p5.Vector object.
+ * let v = myGeometry.vertices[f];
+ * vertex(v.x, v.y, v.z);
+ * }
+ * endShape();
+ *
+ * }
+ * }
+ */
+
+ /**
+ * An array that lists the texture coordinates for each of the geometry's
+ * vertices.
+ *
+ * In order for texture() to work, the geometry
+ * needs a way to map the points on its surface to the pixels in a
+ * rectangular image that's used as a texture. The geometry's vertex at
+ * coordinates `(x, y, z)` maps to the texture image's pixel at coordinates
+ * `(u, v)`.
+ *
+ * The `myGeometry.uvs` array stores the `(u, v)` coordinates for each
+ * vertex in the order it was added to the geometry. For example, the
+ * first vertex, `myGeometry.vertices[0]`, has its `(u, v)` coordinates
+ * stored at `myGeometry.uvs[0]` and `myGeometry.uvs[1]`.
+ *
+ * @property uvs
+ * @name uvs
+ * @for p5.Geometry
+ *
+ * @example
+ * let img;
+ *
+ * async function setup() {
+ * img = await loadImage('assets/laDefense.jpg');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Create p5.Geometry objects.
+ * let geom1 = buildGeometry(createShape);
+ * let geom2 = buildGeometry(createShape);
+ *
+ * // Left (original).
+ * push();
+ * translate(-25, 0, 0);
+ * texture(img);
+ * noStroke();
+ * model(geom1);
+ * pop();
+ *
+ * // Set geom2's texture coordinates.
+ * geom2.uvs = [0.25, 0.25, 0.75, 0.25, 0.25, 0.75, 0.75, 0.75];
+ *
+ * // Right (zoomed in).
+ * push();
+ * translate(25, 0, 0);
+ * texture(img);
+ * noStroke();
+ * model(geom2);
+ * pop();
+ *
+ * describe(
+ * 'Two photos of a ceiling on a gray background. The photo on the right zooms in to the center of the photo.'
+ * );
+ * }
+ *
+ * function createShape() {
+ * plane(40);
+ * }
+ */
+
+ /**
+ * A unique identifier for this geometry. The renderer will use this to cache resources.
+ *
+ * @property {String} gid
+ * @for p5.Geometry
+ */
+ }
+
+ if(typeof p5 !== 'undefined'){
+ geometry(p5);
+ }
+
+ /**
+ * @private
+ * A class responsible for converting successive WebGL draw calls into a single
+ * `p5.Geometry` that can be reused and drawn with `model()`.
+ */
+ class GeometryBuilder {
+ constructor(renderer) {
+ this.renderer = renderer;
+ renderer._pInst.push();
+ this.identityMatrix = new Matrix(4);
+ renderer.states.setValue('uModelMatrix', new Matrix(4));
+ this.geometry = new Geometry(
+ undefined,
+ undefined,
+ undefined,
+ this.renderer
+ );
+ this.geometry.gid = `_p5_GeometryBuilder_${GeometryBuilder.nextGeometryId}`;
+ GeometryBuilder.nextGeometryId++;
+ this.hasTransform = false;
+ }
+
+ /**
+ * @private
+ * Applies the current transformation matrix to each vertex.
+ */
+ transformVertices(vertices) {
+ if (!this.hasTransform) return vertices;
+
+ return vertices.map(v =>
+ this.renderer.states.uModelMatrix.multiplyPoint(v)
+ );
+ }
+
+ /**
+ * @private
+ * Applies the current normal matrix to each normal.
+ */
+ transformNormals(normals) {
+ if (!this.hasTransform) return normals;
+
+ return normals.map(
+ v => this.renderer.scratchMat3.multiplyVec(v) // this is a vec3
+ );
+ }
+
+ /**
+ * @private
+ * Adds a p5.Geometry to the builder's combined geometry, flattening
+ * transformations.
+ */
+ addGeometry(input) {
+ this.hasTransform = !this.renderer.states.uModelMatrix.mat4
+ .every((v, i) => v === this.identityMatrix.mat4[i]);
+
+ if (this.hasTransform) {
+ this.renderer.scratchMat3.inverseTranspose4x4(
+ this.renderer.states.uModelMatrix
+ );
+ }
+
+ let startIdx = this.geometry.vertices.length;
+ this.geometry.vertices.push(...this.transformVertices(input.vertices));
+ this.geometry.vertexNormals.push(
+ ...this.transformNormals(input.vertexNormals)
+ );
+ this.geometry.uvs.push(...input.uvs);
+
+ const inputUserVertexProps = input.userVertexProperties;
+ const builtUserVertexProps = this.geometry.userVertexProperties;
+ const numPreviousVertices =
+ this.geometry.vertices.length - input.vertices.length;
+
+ for (const propName in builtUserVertexProps){
+ if (propName in inputUserVertexProps){
+ continue;
+ }
+ const prop = builtUserVertexProps[propName];
+ const size = prop.getDataSize();
+ const numMissingValues = size * input.vertices.length;
+ const missingValues = Array(numMissingValues).fill(0);
+ prop.pushDirect(missingValues);
+ }
+ for (const propName in inputUserVertexProps){
+ const prop = inputUserVertexProps[propName];
+ const data = prop.getSrcArray();
+ const size = prop.getDataSize();
+ if (numPreviousVertices > 0 && !(propName in builtUserVertexProps)){
+ const numMissingValues = size * numPreviousVertices;
+ const missingValues = Array(numMissingValues).fill(0);
+ this.geometry.vertexProperty(propName, missingValues, size);
+ }
+ this.geometry.vertexProperty(propName, data, size);
+ }
+
+ if (this.renderer.states.fillColor) {
+ this.geometry.faces.push(
+ ...input.faces.map(f => f.map(idx => idx + startIdx))
+ );
+ }
+ if (this.renderer.states.strokeColor) {
+ this.geometry.edges.push(
+ ...input.edges.map(edge => edge.map(idx => idx + startIdx))
+ );
+ }
+ const vertexColors = [...input.vertexColors];
+ while (vertexColors.length < input.vertices.length * 4) {
+ vertexColors.push(...this.renderer.states.curFillColor);
+ }
+ this.geometry.vertexColors.push(...vertexColors);
+ }
+
+ /**
+ * Adds geometry from the renderer's immediate mode into the builder's
+ * combined geometry.
+ */
+ addImmediate(geometry, shapeMode, { validateFaces = false } = {}) {
+ const faces = [];
+
+ if (this.renderer.states.fillColor) {
+ if (
+ shapeMode === TRIANGLE_STRIP ||
+ shapeMode === QUAD_STRIP
+ ) {
+ for (let i = 2; i < geometry.vertices.length; i++) {
+ if (i % 2 === 0) {
+ faces.push([i, i - 1, i - 2]);
+ } else {
+ faces.push([i, i - 2, i - 1]);
+ }
+ }
+ } else if (shapeMode === TRIANGLE_FAN) {
+ for (let i = 2; i < geometry.vertices.length; i++) {
+ faces.push([0, i - 1, i]);
+ }
+ } else if (shapeMode === TRIANGLES) {
+ for (let i = 0; i < geometry.vertices.length; i += 3) {
+ if (
+ !validateFaces ||
+ geometry.vertices[i].copy().sub(geometry.vertices[i+1])
+ .cross(geometry.vertices[i].copy().sub(geometry.vertices[i+2]))
+ .magSq() > 0
+ ) {
+ faces.push([i, i + 1, i + 2]);
+ }
+ }
+ }
+ }
+ this.addGeometry(Object.assign({}, geometry, { faces }));
+ }
+
+ /**
+ * Adds geometry from the renderer's retained mode into the builder's
+ * combined geometry.
+ */
+ addRetained(geometry) {
+ this.addGeometry(geometry);
+ }
+
+ /**
+ * Cleans up the state of the renderer and returns the combined geometry that
+ * was built.
+ * @returns p5.Geometry The flattened, combined geometry
+ */
+ finish() {
+ this.renderer._pInst.pop();
+ return this.geometry;
+ }
+ }
+
+ /**
+ * Keeps track of how many custom geometry objects have been made so that each
+ * can be assigned a unique ID.
+ */
+ GeometryBuilder.nextGeometryId = 0;
+
+ /**
+ * @module Math
+ * @submodule Quaternion
+ */
+
+
+ class Quat {
+ constructor(w, x, y, z) {
+ this.w = w;
+ this.vec = new Vector(x, y, z);
+ }
+
+ /**
+ * Returns a Quaternion for the
+ * axis angle representation of the rotation
+ *
+ * @method fromAxisAngle
+ * @param {Number} [angle] Angle with which the points needs to be rotated
+ * @param {Number} [x] x component of the axis vector
+ * @param {Number} [y] y component of the axis vector
+ * @param {Number} [z] z component of the axis vector
+ * @chainable
+ */
+ static fromAxisAngle(angle, x, y, z) {
+ const w = Math.cos(angle/2);
+ const vec = new Vector(x, y, z).normalize().mult(Math.sin(angle/2));
+ return new Quat(w, vec.x, vec.y, vec.z);
+ }
+
+ conjugate() {
+ return new Quat(this.w, -this.vec.x, -this.vec.y, -this.vec.z);
+ }
+
+ /**
+ * Multiplies a quaternion with other quaternion.
+ * @method mult
+ * @param {p5.Quat} [quat] quaternion to multiply with the quaternion calling the method.
+ * @chainable
+ */
+ multiply(quat) {
+
+ return new Quat(
+ this.w * quat.w - this.vec.x * quat.vec.x - this.vec.y * quat.vec.y - this.vec.z - quat.vec.z,
+ this.w * quat.vec.x + this.vec.x * quat.w + this.vec.y * quat.vec.z - this.vec.z * quat.vec.y,
+ this.w * quat.vec.y - this.vec.x * quat.vec.z + this.vec.y * quat.w + this.vec.z * quat.vec.x,
+ this.w * quat.vec.z + this.vec.x * quat.vec.y - this.vec.y * quat.vec.x + this.vec.z * quat.w
+ );
+
+ }
+
+ /**
+ * This is similar to quaternion multiplication
+ * but when multipying vector with quaternion
+ * the multiplication can be simplified to the below formula.
+ * This was taken from the below stackexchange link
+ * https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion/50545#50545
+ * @private
+ * @param {p5.Vector} [p] vector to rotate on the axis quaternion
+ */
+ rotateVector(p) {
+ return Vector.mult( p, this.w*this.w - this.vec.dot(this.vec) )
+ .add( Vector.mult( this.vec, 2 * p.dot(this.vec) ) )
+ .add( Vector.mult( this.vec, 2 * this.w ).cross( p ) )
+ .clampToZero();
+ }
+
+ /**
+ * Rotates the Quaternion by the quaternion passed
+ * which contains the axis of roation and angle of rotation
+ *
+ * @method rotateBy
+ * @param {p5.Quat} [axesQuat] axis quaternion which contains
+ * the axis of rotation and angle of rotation
+ * @chainable
+ */
+ rotateBy(axesQuat) {
+ return axesQuat.multiply(this).multiply(axesQuat.conjugate()).
+ vec.clampToZero();
+ }
+ }
+
+ function quat(p5, fn){
+ /**
+ * A class to describe a Quaternion
+ * for vector rotations in the p5js webgl renderer.
+ * Please refer the following link for details on the implementation
+ * https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html
+ * @class p5.Quat
+ * @constructor
+ * @param {Number} [w] Scalar part of the quaternion
+ * @param {Number} [x] x component of imaginary part of quaternion
+ * @param {Number} [y] y component of imaginary part of quaternion
+ * @param {Number} [z] z component of imaginary part of quaternion
+ * @private
+ */
+ p5.Quat = Quat;
+ }
+
+ if(typeof p5 !== 'undefined'){
+ quat(p5);
+ }
+
+ /**
+ * @module 3D
+ * @submodule Camera
+ * @requires core
+ */
+
+
+ class Camera {
+ constructor(renderer) {
+ this._renderer = renderer;
+
+ this.cameraType = 'default';
+ this.useLinePerspective = true;
+ this.cameraMatrix = new Matrix(4);
+ this.projMatrix = new Matrix(4);
+ this.yScale = 1;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // Camera Projection Methods
+ ////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Sets a perspective projection for the camera.
+ *
+ * In a perspective projection, shapes that are further from the camera appear
+ * smaller than shapes that are near the camera. This technique, called
+ * foreshortening, creates realistic 3D scenes. It’s applied by default in new
+ * `p5.Camera` objects.
+ *
+ * `myCamera.perspective()` changes the camera’s perspective by changing its
+ * viewing frustum. The frustum is the volume of space that’s visible to the
+ * camera. The frustum’s shape is a pyramid with its top cut off. The camera
+ * is placed where the top of the pyramid should be and points towards the
+ * base of the pyramid. It views everything within the frustum.
+ *
+ * The first parameter, `fovy`, is the camera’s vertical field of view. It’s
+ * an angle that describes how tall or narrow a view the camera has. For
+ * example, calling `myCamera.perspective(0.5)` sets the camera’s vertical
+ * field of view to 0.5 radians. By default, `fovy` is calculated based on the
+ * sketch’s height and the camera’s default z-coordinate, which is 800. The
+ * formula for the default `fovy` is `2 * atan(height / 2 / 800)`.
+ *
+ * The second parameter, `aspect`, is the camera’s aspect ratio. It’s a number
+ * that describes the ratio of the top plane’s width to its height. For
+ * example, calling `myCamera.perspective(0.5, 1.5)` sets the camera’s field
+ * of view to 0.5 radians and aspect ratio to 1.5, which would make shapes
+ * appear thinner on a square canvas. By default, `aspect` is set to
+ * `width / height`.
+ *
+ * The third parameter, `near`, is the distance from the camera to the near
+ * plane. For example, calling `myCamera.perspective(0.5, 1.5, 100)` sets the
+ * camera’s field of view to 0.5 radians, its aspect ratio to 1.5, and places
+ * the near plane 100 pixels from the camera. Any shapes drawn less than 100
+ * pixels from the camera won’t be visible. By default, `near` is set to
+ * `0.1 * 800`, which is 1/10th the default distance between the camera and
+ * the origin.
+ *
+ * The fourth parameter, `far`, is the distance from the camera to the far
+ * plane. For example, calling `myCamera.perspective(0.5, 1.5, 100, 10000)`
+ * sets the camera’s field of view to 0.5 radians, its aspect ratio to 1.5,
+ * places the near plane 100 pixels from the camera, and places the far plane
+ * 10,000 pixels from the camera. Any shapes drawn more than 10,000 pixels
+ * from the camera won’t be visible. By default, `far` is set to `10 * 800`,
+ * which is 10 times the default distance between the camera and the origin.
+ *
+ * @for p5.Camera
+ * @param {Number} [fovy] camera frustum vertical field of view. Defaults to
+ * `2 * atan(height / 2 / 800)`.
+ * @param {Number} [aspect] camera frustum aspect ratio. Defaults to
+ * `width / height`.
+ * @param {Number} [near] distance from the camera to the near clipping plane.
+ * Defaults to `0.1 * 800`.
+ * @param {Number} [far] distance from the camera to the far clipping plane.
+ * Defaults to `10 * 800`.
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * cam2 = createCamera();
+ *
+ * // Place it at the top-right.
+ * cam2.camera(400, -400, 800);
+ *
+ * // Set its fovy to 0.2.
+ * // Set its aspect to 1.5.
+ * // Set its near to 600.
+ * // Set its far to 1200.
+ * cam2.perspective(0.2, 1.5, 600, 1200);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe('A white cube on a gray background. The camera toggles between a frontal view and a skewed aerial view when the user double-clicks.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
+ * }
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * cam2 = createCamera();
+ *
+ * // Place it at the top-right.
+ * cam2.camera(400, -400, 800);
+ *
+ * // Set its fovy to 0.2.
+ * // Set its aspect to 1.5.
+ * // Set its near to 600.
+ * // Set its far to 1200.
+ * cam2.perspective(0.2, 1.5, 600, 1200);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe('A white cube moves left and right on a gray background. The camera toggles between a frontal and a skewed aerial view when the user double-clicks.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Translate the origin left and right.
+ * let x = 100 * sin(frameCount * 0.01);
+ * translate(x, 0, 0);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
+ * }
+ */
+ perspective(fovy, aspect, near, far) {
+ const range = this._renderer.zClipRange();
+ this.cameraType = arguments.length > 0 ? 'custom' : 'default';
+ if (typeof fovy === 'undefined') {
+ fovy = this.defaultCameraFOV;
+ // this avoids issue where setting angleMode(DEGREES) before calling
+ // perspective leads to a smaller than expected FOV (because
+ // _computeCameraDefaultSettings computes in radians)
+ this.cameraFOV = fovy;
+ } else {
+ this.cameraFOV = this._renderer._pInst._toRadians(fovy);
+ }
+ if (typeof aspect === 'undefined') {
+ aspect = this.defaultAspectRatio;
+ }
+ if (typeof near === 'undefined') {
+ near = this.defaultCameraNear;
+ }
+ if (typeof far === 'undefined') {
+ far = this.defaultCameraFar;
+ }
+
+ if (near <= 0.0001) {
+ near = 0.01;
+ console.log(
+ 'Avoid perspective near plane values close to or below 0. ' +
+ 'Setting value to 0.01.'
+ );
+ }
+
+ if (far < near) {
+ console.log(
+ 'Perspective far plane value is less than near plane value. ' +
+ 'Nothing will be shown.'
+ );
+ }
+
+ this.aspectRatio = aspect;
+ this.cameraNear = near;
+ this.cameraFar = far;
+
+ this.projMatrix = new Matrix(4);
+
+ const f = 1.0 / Math.tan(this.cameraFOV / 2);
+ const nf = 1.0 / (this.cameraNear - this.cameraFar);
+
+ let A, B;
+ if (range[0] === 0) {
+ // WebGPU clip space, z in [0, 1]
+ A = far / (near - far);
+ B = (far * near) / (near - far);
+ } else {
+ // WebGL clip space, z in [-1, 1]
+ A = (far + near) * nf;
+ B = (2 * far * near) * nf;
+ }
+
+ this.projMatrix.set(f / aspect, 0, 0, 0,
+ 0, -f * this.yScale, 0, 0,
+ 0, 0, A, -1,
+ 0, 0, B, 0);
+
+ if (this._isActive()) {
+ this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
+ this._renderer.states.uPMatrix.set(this.projMatrix);
+ }
+ }
+
+ /**
+ * Sets an orthographic projection for the camera.
+ *
+ * In an orthographic projection, shapes with the same size always appear the
+ * same size, regardless of whether they are near or far from the camera.
+ *
+ * `myCamera.ortho()` changes the camera’s perspective by changing its viewing
+ * frustum from a truncated pyramid to a rectangular prism. The frustum is the
+ * volume of space that’s visible to the camera. The camera is placed in front
+ * of the frustum and views everything within the frustum. `myCamera.ortho()`
+ * has six optional parameters to define the viewing frustum.
+ *
+ * The first four parameters, `left`, `right`, `bottom`, and `top`, set the
+ * coordinates of the frustum’s sides, bottom, and top. For example, calling
+ * `myCamera.ortho(-100, 100, 200, -200)` creates a frustum that’s 200 pixels
+ * wide and 400 pixels tall. By default, these dimensions are set based on
+ * the sketch’s width and height, as in
+ * `myCamera.ortho(-width / 2, width / 2, -height / 2, height / 2)`.
+ *
+ * The last two parameters, `near` and `far`, set the distance of the
+ * frustum’s near and far plane from the camera. For example, calling
+ * `myCamera.ortho(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s
+ * 200 pixels wide, 400 pixels tall, starts 50 pixels from the camera, and
+ * ends 1,000 pixels from the camera. By default, `near` and `far` are set to
+ * 0 and `max(width, height) + 800`, respectively.
+ *
+ * @for p5.Camera
+ * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 2`.
+ * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 2`.
+ * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 2`.
+ * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 2`.
+ * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to 0.
+ * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `max(width, height) + 800`.
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * cam2 = createCamera();
+ *
+ * // Apply an orthographic projection.
+ * cam2.ortho();
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe('A row of white cubes against a gray background. The camera toggles between a perspective and an orthographic projection when the user double-clicks.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -40);
+ * box(10);
+ * }
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
+ * }
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * cam2 = createCamera();
+ *
+ * // Apply an orthographic projection.
+ * cam2.ortho();
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe('A row of white cubes slither like a snake against a gray background. The camera toggles between a perspective and an orthographic projection when the user double-clicks.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * push();
+ * // Calculate the box's coordinates.
+ * let x = 10 * sin(frameCount * 0.02 + i * 0.6);
+ * let z = -40 * i;
+ * // Translate the origin.
+ * translate(x, 0, z);
+ * // Draw the box.
+ * box(10);
+ * pop();
+ * }
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
+ * }
+ */
+ ortho(left, right, bottom, top, near, far) {
+ const source = this.fbo || this._renderer;
+ if (left === undefined) left = -source.width / 2;
+ if (right === undefined) right = +source.width / 2;
+ if (bottom === undefined) bottom = -source.height / 2;
+ if (top === undefined) top = +source.height / 2;
+ if (near === undefined) near = 0;
+ if (far === undefined) far = Math.max(source.width, source.height) + 800;
+ this.cameraNear = near;
+ this.cameraFar = far;
+ const w = right - left;
+ const h = top - bottom;
+ const d = far - near;
+ const x = 2 / w;
+ const y = 2 / h * this.yScale;
+ const z = -2 / d;
+ const tx = -(right + left) / w;
+ const ty = -(top + bottom) / h;
+ const tz = -(far + near) / d;
+ this.projMatrix = new Matrix(4);
+
+ this.projMatrix.set(x, 0, 0, 0,
+ 0, -y, 0, 0,
+ 0, 0, z, 0,
+ tx, ty, tz, 1);
+
+ if (this._isActive()) {
+ this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
+ this._renderer.states.uPMatrix.set(this.projMatrix);
+ }
+ this.cameraType = 'custom';
+ }
+ /**
+ * Sets the camera's frustum.
+ *
+ * In a frustum projection, shapes that are further from the camera appear
+ * smaller than shapes that are near the camera. This technique, called
+ * foreshortening, creates realistic 3D scenes.
+ *
+ * `myCamera.frustum()` changes the camera’s perspective by changing its
+ * viewing frustum. The frustum is the volume of space that’s visible to the
+ * camera. The frustum’s shape is a pyramid with its top cut off. The camera
+ * is placed where the top of the pyramid should be and points towards the
+ * base of the pyramid. It views everything within the frustum.
+ *
+ * The first four parameters, `left`, `right`, `bottom`, and `top`, set the
+ * coordinates of the frustum’s sides, bottom, and top. For example, calling
+ * `myCamera.frustum(-100, 100, 200, -200)` creates a frustum that’s 200
+ * pixels wide and 400 pixels tall. By default, these coordinates are set
+ * based on the sketch’s width and height, as in
+ * `myCamera.frustum(-width / 20, width / 20, height / 20, -height / 20)`.
+ *
+ * The last two parameters, `near` and `far`, set the distance of the
+ * frustum’s near and far plane from the camera. For example, calling
+ * `myCamera.frustum(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s
+ * 200 pixels wide, 400 pixels tall, starts 50 pixels from the camera, and ends
+ * 1,000 pixels from the camera. By default, near is set to `0.1 * 800`, which
+ * is 1/10th the default distance between the camera and the origin. `far` is
+ * set to `10 * 800`, which is 10 times the default distance between the
+ * camera and the origin.
+ *
+ * @for p5.Camera
+ * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 20`.
+ * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 20`.
+ * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 20`.
+ * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 20`.
+ * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to `0.1 * 800`.
+ * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `10 * 800`.
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * cam2 = createCamera();
+ *
+ * // Adjust the frustum.
+ * // Center it.
+ * // Set its width and height to 20 pixels.
+ * // Place its near plane 300 pixels from the camera.
+ * // Place its far plane 350 pixels from the camera.
+ * cam2.frustum(-10, 10, -10, 10, 300, 350);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe(
+ * 'A row of white cubes against a gray background. The camera zooms in on one cube when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 600);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -40);
+ * box(10);
+ * }
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
+ * }
+ */
+ frustum(left, right, bottom, top, near, far) {
+ if (left === undefined) left = -this._renderer.width * 0.05;
+ if (right === undefined) right = +this._renderer.width * 0.05;
+ if (bottom === undefined) bottom = +this._renderer.height * 0.05;
+ if (top === undefined) top = -this._renderer.height * 0.05;
+ if (near === undefined) near = this.defaultCameraNear;
+ if (far === undefined) far = this.defaultCameraFar;
+
+ this.cameraNear = near;
+ this.cameraFar = far;
+
+ const w = right - left;
+ const h = top - bottom;
+ const d = far - near;
+
+ const x = +(2.0 * near) / w;
+ const y = +(2.0 * near) / h * this.yScale;
+ const z = -(2.0 * far * near) / d;
+
+ const tx = (right + left) / w;
+ const ty = (top + bottom) / h;
+ const tz = -(far + near) / d;
+
+ this.projMatrix = new Matrix(4);
+
+
+ this.projMatrix.set(x, 0, 0, 0,
+ 0, -y, 0, 0,
+ tx, ty, tz, -1,
+ 0, 0, z, 0);
+
+
+ if (this._isActive()) {
+ this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
+ this._renderer.states.uPMatrix.set(this.projMatrix);
+ }
+
+ this.cameraType = 'custom';
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // Camera Orientation Methods
+ ////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Rotate camera view about arbitrary axis defined by x,y,z
+ * based on http://learnwebgl.brown37.net/07_cameras/camera_rotating_motion.html
+ * @private
+ */
+ _rotateView(a, x, y, z) {
+ let centerX = this.centerX;
+ let centerY = this.centerY;
+ let centerZ = this.centerZ;
+
+ // move center by eye position such that rotation happens around eye position
+ centerX -= this.eyeX;
+ centerY -= this.eyeY;
+ centerZ -= this.eyeZ;
+
+ const rotation = new Matrix(4); // TODO Maybe pass p5
+ rotation.rotate4x4(this._renderer._pInst._toRadians(a), x, y, z);
+
+ const rotatedCenter = [
+ centerX * rotation.mat4[0] + centerY * rotation.mat4[4] + centerZ * rotation.mat4[8],
+ centerX * rotation.mat4[1] + centerY * rotation.mat4[5] + centerZ * rotation.mat4[9],
+ centerX * rotation.mat4[2] + centerY * rotation.mat4[6] + centerZ * rotation.mat4[10]
+ ];
+
+ // add eye position back into center
+ rotatedCenter[0] += this.eyeX;
+ rotatedCenter[1] += this.eyeY;
+ rotatedCenter[2] += this.eyeZ;
+
+ this.camera(
+ this.eyeX,
+ this.eyeY,
+ this.eyeZ,
+ rotatedCenter[0],
+ rotatedCenter[1],
+ rotatedCenter[2],
+ this.upX,
+ this.upY,
+ this.upZ
+ );
+ }
+
+ /**
+ * Rotates the camera in a clockwise/counter-clockwise direction.
+ *
+ * Rolling rotates the camera without changing its orientation. The rotation
+ * happens in the camera’s "local" space.
+ *
+ * The parameter, `angle`, is the angle the camera should rotate. Passing a
+ * positive angle, as in `myCamera.roll(0.001)`, rotates the camera in counter-clockwise direction.
+ * Passing a negative angle, as in `myCamera.roll(-0.001)`, rotates the
+ * camera in clockwise direction.
+ *
+ * Note: Angles are interpreted based on the current
+ * angleMode().
+ *
+ * @method roll
+ * @param {Number} angle amount to rotate camera in current
+ * angleMode units.
+ * @example
+ * let cam;
+ * let delta = 0.01;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ * normalMaterial();
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Roll camera according to angle 'delta'
+ * cam.roll(delta);
+ *
+ * translate(0, 0, 0);
+ * box(20);
+ * translate(0, 25, 0);
+ * box(20);
+ * translate(0, 26, 0);
+ * box(20);
+ * translate(0, 27, 0);
+ * box(20);
+ * translate(0, 28, 0);
+ * box(20);
+ * translate(0,29, 0);
+ * box(20);
+ * translate(0, 30, 0);
+ * box(20);
+ * }
+ *
+ * @alt
+ * camera view rotates in counter clockwise direction with vertically stacked boxes in front of it.
+ */
+ roll(amount) {
+ const local = this._getLocalAxes();
+ const axisQuaternion = Quat.fromAxisAngle(
+ this._renderer._pInst._toRadians(amount),
+ local.z[0], local.z[1], local.z[2]);
+ // const upQuat = new p5.Quat(0, this.upX, this.upY, this.upZ);
+ const newUpVector = axisQuaternion.rotateVector(
+ new Vector(this.upX, this.upY, this.upZ));
+ this.camera(
+ this.eyeX,
+ this.eyeY,
+ this.eyeZ,
+ this.centerX,
+ this.centerY,
+ this.centerZ,
+ newUpVector.x,
+ newUpVector.y,
+ newUpVector.z
+ );
+ }
+
+ /**
+ * Rotates the camera left and right.
+ *
+ * Panning rotates the camera without changing its position. The rotation
+ * happens in the camera’s "local" space.
+ *
+ * The parameter, `angle`, is the angle the camera should rotate. Passing a
+ * positive angle, as in `myCamera.pan(0.001)`, rotates the camera to the
+ * right. Passing a negative angle, as in `myCamera.pan(-0.001)`, rotates the
+ * camera to the left.
+ *
+ * Note: Angles are interpreted based on the current
+ * angleMode().
+ *
+ * @param {Number} angle amount to rotate in the current
+ * angleMode().
+ *
+ * @example
+ * let cam;
+ * let delta = 0.001;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube goes in and out of view as the camera pans left and right.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Pan with the camera.
+ * cam.pan(delta);
+ *
+ * // Switch directions every 120 frames.
+ * if (frameCount % 120 === 0) {
+ * delta *= -1;
+ * }
+ *
+ * // Draw the box.
+ * box();
+ * }
+ */
+ pan(amount) {
+ const local = this._getLocalAxes();
+ this._rotateView(amount, local.y[0], local.y[1], local.y[2]);
+ }
+
+ /**
+ * Rotates the camera up and down.
+ *
+ * Tilting rotates the camera without changing its position. The rotation
+ * happens in the camera’s "local" space.
+ *
+ * The parameter, `angle`, is the angle the camera should rotate. Passing a
+ * positive angle, as in `myCamera.tilt(0.001)`, rotates the camera down.
+ * Passing a negative angle, as in `myCamera.tilt(-0.001)`, rotates the camera
+ * up.
+ *
+ * Note: Angles are interpreted based on the current
+ * angleMode().
+ *
+ * @param {Number} angle amount to rotate in the current
+ * angleMode().
+ *
+ * @example
+ * let cam;
+ * let delta = 0.001;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube goes in and out of view as the camera tilts up and down.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Pan with the camera.
+ * cam.tilt(delta);
+ *
+ * // Switch directions every 120 frames.
+ * if (frameCount % 120 === 0) {
+ * delta *= -1;
+ * }
+ *
+ * // Draw the box.
+ * box();
+ * }
+ */
+ tilt(amount) {
+ const local = this._getLocalAxes();
+ this._rotateView(amount, local.x[0], local.x[1], local.x[2]);
+ }
+
+ /**
+ * Points the camera at a location.
+ *
+ * `myCamera.lookAt()` changes the camera’s orientation without changing its
+ * position.
+ *
+ * The parameters, `x`, `y`, and `z`, are the coordinates in "world" space
+ * where the camera should point. For example, calling
+ * `myCamera.lookAt(10, 20, 30)` points the camera at the coordinates
+ * `(10, 20, 30)`.
+ *
+ * @for p5.Camera
+ * @param {Number} x x-coordinate of the position where the camera should look in "world" space.
+ * @param {Number} y y-coordinate of the position where the camera should look in "world" space.
+ * @param {Number} z z-coordinate of the position where the camera should look in "world" space.
+ *
+ * @example
+ * // Double-click to look at a different cube.
+ *
+ * let cam;
+ * let isLookingLeft = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at the origin.
+ * cam.lookAt(-30, 0, 0);
+ *
+ * describe(
+ * 'A red cube and a blue cube on a gray background. The camera switches focus between the cubes when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Draw the box on the left.
+ * push();
+ * // Translate the origin to the left.
+ * translate(-30, 0, 0);
+ * // Style the box.
+ * fill(255, 0, 0);
+ * // Draw the box.
+ * box(20);
+ * pop();
+ *
+ * // Draw the box on the right.
+ * push();
+ * // Translate the origin to the right.
+ * translate(30, 0, 0);
+ * // Style the box.
+ * fill(0, 0, 255);
+ * // Draw the box.
+ * box(20);
+ * pop();
+ * }
+ *
+ * // Change the camera's focus when the user double-clicks.
+ * function doubleClicked() {
+ * if (isLookingLeft === true) {
+ * cam.lookAt(30, 0, 0);
+ * isLookingLeft = false;
+ * } else {
+ * cam.lookAt(-30, 0, 0);
+ * isLookingLeft = true;
+ * }
+ * }
+ */
+ lookAt(x, y, z) {
+ this.camera(
+ this.eyeX,
+ this.eyeY,
+ this.eyeZ,
+ x,
+ y,
+ z,
+ this.upX,
+ this.upY,
+ this.upZ
+ );
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // Camera Position Methods
+ ////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Sets the position and orientation of the camera.
+ *
+ * `myCamera.camera()` allows objects to be viewed from different angles. It
+ * has nine parameters that are all optional.
+ *
+ * The first three parameters, `x`, `y`, and `z`, are the coordinates of the
+ * camera’s position in "world" space. For example, calling
+ * `myCamera.camera(0, 0, 0)` places the camera at the origin `(0, 0, 0)`. By
+ * default, the camera is placed at `(0, 0, 800)`.
+ *
+ * The next three parameters, `centerX`, `centerY`, and `centerZ` are the
+ * coordinates of the point where the camera faces in "world" space. For
+ * example, calling `myCamera.camera(0, 0, 0, 10, 20, 30)` places the camera
+ * at the origin `(0, 0, 0)` and points it at `(10, 20, 30)`. By default, the
+ * camera points at the origin `(0, 0, 0)`.
+ *
+ * The last three parameters, `upX`, `upY`, and `upZ` are the components of
+ * the "up" vector in "local" space. The "up" vector orients the camera’s
+ * y-axis. For example, calling
+ * `myCamera.camera(0, 0, 0, 10, 20, 30, 0, -1, 0)` places the camera at the
+ * origin `(0, 0, 0)`, points it at `(10, 20, 30)`, and sets the "up" vector
+ * to `(0, -1, 0)` which is like holding it upside-down. By default, the "up"
+ * vector is `(0, 1, 0)`.
+ *
+ * @for p5.Camera
+ * @param {Number} [x] x-coordinate of the camera. Defaults to 0.
+ * @param {Number} [y] y-coordinate of the camera. Defaults to 0.
+ * @param {Number} [z] z-coordinate of the camera. Defaults to 800.
+ * @param {Number} [centerX] x-coordinate of the point the camera faces. Defaults to 0.
+ * @param {Number} [centerY] y-coordinate of the point the camera faces. Defaults to 0.
+ * @param {Number} [centerZ] z-coordinate of the point the camera faces. Defaults to 0.
+ * @param {Number} [upX] x-component of the camera’s "up" vector. Defaults to 0.
+ * @param {Number} [upY] x-component of the camera’s "up" vector. Defaults to 1.
+ * @param {Number} [upZ] z-component of the camera’s "up" vector. Defaults to 0.
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * cam2 = createCamera();
+ *
+ * // Place it at the top-right: (1200, -600, 100)
+ * // Point it at the row of boxes: (-10, -10, 400)
+ * // Set its "up" vector to the default: (0, 1, 0)
+ * cam2.camera(1200, -600, 100, -10, -10, 400, 0, 1, 0);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe(
+ * 'A row of white cubes against a gray background. The camera toggles between a frontal and an aerial view when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -30);
+ * box(10);
+ * }
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
+ * }
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * cam2 = createCamera();
+ *
+ * // Place it at the right: (1200, 0, 100)
+ * // Point it at the row of boxes: (-10, -10, 400)
+ * // Set its "up" vector to the default: (0, 1, 0)
+ * cam2.camera(1200, 0, 100, -10, -10, 400, 0, 1, 0);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe(
+ * 'A row of white cubes against a gray background. The camera toggles between a static frontal view and an orbiting view when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Update cam2's position.
+ * let x = 1200 * cos(frameCount * 0.01);
+ * let y = -600 * sin(frameCount * 0.01);
+ * cam2.camera(x, y, 100, -10, -10, 400, 0, 1, 0);
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -30);
+ * box(10);
+ * }
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
+ * }
+ */
+ camera(
+ eyeX,
+ eyeY,
+ eyeZ,
+ centerX,
+ centerY,
+ centerZ,
+ upX,
+ upY,
+ upZ
+ ) {
+ if (typeof eyeX === 'undefined') {
+ eyeX = this.defaultEyeX;
+ eyeY = this.defaultEyeY;
+ eyeZ = this.defaultEyeZ;
+ centerX = eyeX;
+ centerY = eyeY;
+ centerZ = 0;
+ upX = 0;
+ upY = 1;
+ upZ = 0;
+ }
+
+ this.eyeX = eyeX;
+ this.eyeY = eyeY;
+ this.eyeZ = eyeZ;
+
+ if (typeof centerX !== 'undefined') {
+ this.centerX = centerX;
+ this.centerY = centerY;
+ this.centerZ = centerZ;
+ }
+
+ if (typeof upX !== 'undefined') {
+ this.upX = upX;
+ this.upY = upY;
+ this.upZ = upZ;
+ }
+
+ const local = this._getLocalAxes();
+
+ // the camera affects the model view matrix, insofar as it
+ // inverse translates the world to the eye position of the camera
+ // and rotates it.
+
+ this.cameraMatrix.set(local.x[0], local.y[0], local.z[0], 0,
+ local.x[1], local.y[1], local.z[1], 0,
+ local.x[2], local.y[2], local.z[2], 0,
+ 0, 0, 0, 1);
+
+
+ const tx = -eyeX;
+ const ty = -eyeY;
+ const tz = -eyeZ;
+
+ this.cameraMatrix.translate([tx, ty, tz]);
+
+ if (this._isActive()) {
+ this._renderer.states.setValue('uViewMatrix', this._renderer.states.uViewMatrix.clone());
+ this._renderer.states.uViewMatrix.set(this.cameraMatrix);
+ }
+ return this;
+ }
+
+ /**
+ * Moves the camera along its "local" axes without changing its orientation.
+ *
+ * The parameters, `x`, `y`, and `z`, are the distances the camera should
+ * move. For example, calling `myCamera.move(10, 20, 30)` moves the camera 10
+ * pixels to the right, 20 pixels down, and 30 pixels backward in its "local"
+ * space.
+ *
+ * @param {Number} x distance to move along the camera’s "local" x-axis.
+ * @param {Number} y distance to move along the camera’s "local" y-axis.
+ * @param {Number} z distance to move along the camera’s "local" z-axis.
+ * @example
+ * // Click the canvas to begin detecting key presses.
+ *
+ * let cam;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam = createCamera();
+ *
+ * // Place the camera at the top-right.
+ * cam.setPosition(400, -400, 800);
+ *
+ * // Point it at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * // Set the camera.
+ * setCamera(cam);
+ *
+ * describe(
+ * 'A white cube drawn against a gray background. The cube appears to move when the user presses certain keys.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Move the camera along its "local" axes
+ * // when the user presses certain keys.
+ *
+ * // Move horizontally.
+ * if (keyIsDown(LEFT_ARROW)) {
+ * cam.move(-1, 0, 0);
+ * }
+ * if (keyIsDown(RIGHT_ARROW)) {
+ * cam.move(1, 0, 0);
+ * }
+ *
+ * // Move vertically.
+ * if (keyIsDown(UP_ARROW)) {
+ * cam.move(0, -1, 0);
+ * }
+ * if (keyIsDown(DOWN_ARROW)) {
+ * cam.move(0, 1, 0);
+ * }
+ *
+ * // Move in/out of the screen.
+ * if (keyIsDown('i')) {
+ * cam.move(0, 0, -1);
+ * }
+ * if (keyIsDown('o')) {
+ * cam.move(0, 0, 1);
+ * }
+ *
+ * // Draw the box.
+ * box();
+ * }
+ */
+ move(x, y, z) {
+ const local = this._getLocalAxes();
+
+ // scale local axes by movement amounts
+ // based on http://learnwebgl.brown37.net/07_cameras/camera_linear_motion.html
+ const dx = [local.x[0] * x, local.x[1] * x, local.x[2] * x];
+ const dy = [local.y[0] * y, local.y[1] * y, local.y[2] * y];
+ const dz = [local.z[0] * z, local.z[1] * z, local.z[2] * z];
+
+ this.camera(
+ this.eyeX + dx[0] + dy[0] + dz[0],
+ this.eyeY + dx[1] + dy[1] + dz[1],
+ this.eyeZ + dx[2] + dy[2] + dz[2],
+ this.centerX + dx[0] + dy[0] + dz[0],
+ this.centerY + dx[1] + dy[1] + dz[1],
+ this.centerZ + dx[2] + dy[2] + dz[2],
+ this.upX,
+ this.upY,
+ this.upZ
+ );
+ }
+
+ /**
+ * Sets the camera’s position in "world" space without changing its
+ * orientation.
+ *
+ * The parameters, `x`, `y`, and `z`, are the coordinates where the camera
+ * should be placed. For example, calling `myCamera.setPosition(10, 20, 30)`
+ * places the camera at coordinates `(10, 20, 30)` in "world" space.
+ *
+ * @param {Number} x x-coordinate in "world" space.
+ * @param {Number} y y-coordinate in "world" space.
+ * @param {Number} z z-coordinate in "world" space.
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * cam2 = createCamera();
+ *
+ * // Place it closer to the origin.
+ * cam2.setPosition(0, 0, 600);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe(
+ * 'A row of white cubes against a gray background. The camera toggles the amount of zoom when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -30);
+ * box(10);
+ * }
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
+ * }
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * cam2 = createCamera();
+ *
+ * // Place it closer to the origin.
+ * cam2.setPosition(0, 0, 600);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe(
+ * 'A row of white cubes against a gray background. The camera toggles between a static view and a view that zooms in and out when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Update cam2's z-coordinate.
+ * let z = 100 * sin(frameCount * 0.01) + 700;
+ * cam2.setPosition(0, 0, z);
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 500);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -30);
+ * box(10);
+ * }
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
+ * }
+ */
+ setPosition(x, y, z) {
+ const diffX = x - this.eyeX;
+ const diffY = y - this.eyeY;
+ const diffZ = z - this.eyeZ;
+
+ this.camera(
+ x,
+ y,
+ z,
+ this.centerX + diffX,
+ this.centerY + diffY,
+ this.centerZ + diffZ,
+ this.upX,
+ this.upY,
+ this.upZ
+ );
+ }
+
+ /**
+ * Sets the camera’s position, orientation, and projection by copying another
+ * camera.
+ *
+ * The parameter, `cam`, is the `p5.Camera` object to copy. For example, calling
+ * `cam2.set(cam1)` will set `cam2` using `cam1`’s configuration.
+ *
+ * @param {p5.Camera} cam camera to copy.
+ *
+ * @example
+ * // Double-click to "reset" the camera zoom.
+ *
+ * let cam1;
+ * let cam2;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * cam1 = createCamera();
+ *
+ * // Place the camera at the top-right.
+ * cam1.setPosition(400, -400, 800);
+ *
+ * // Point it at the origin.
+ * cam1.lookAt(0, 0, 0);
+ *
+ * // Create the second camera.
+ * cam2 = createCamera();
+ *
+ * // Copy cam1's configuration.
+ * cam2.set(cam1);
+ *
+ * // Set the camera.
+ * setCamera(cam2);
+ *
+ * describe(
+ * 'A white cube drawn against a gray background. The camera slowly moves forward. The camera resets when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Update cam2's position.
+ * cam2.move(0, 0, -1);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ * // "Reset" the camera when the user double-clicks.
+ * function doubleClicked() {
+ * cam2.set(cam1);
+ * }
+ */
+ set(cam) {
+ const keyNamesOfThePropToCopy = [
+ 'eyeX', 'eyeY', 'eyeZ',
+ 'centerX', 'centerY', 'centerZ',
+ 'upX', 'upY', 'upZ',
+ 'cameraFOV', 'aspectRatio', 'cameraNear', 'cameraFar', 'cameraType',
+ 'yScale', 'useLinePerspective'
+ ];
+ for (const keyName of keyNamesOfThePropToCopy) {
+ this[keyName] = cam[keyName];
+ }
+
+ this.cameraMatrix = cam.cameraMatrix.copy();
+ this.projMatrix = cam.projMatrix.copy();
+
+ if (this._isActive()) {
+ this._renderer.states.setValue('uModelMatrix', this._renderer.states.uModelMatrix.clone());
+ this._renderer.states.setValue('uViewMatrix', this._renderer.states.uViewMatrix.clone());
+ this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
+ this._renderer.states.uModelMatrix.reset();
+ this._renderer.states.uViewMatrix.set(this.cameraMatrix);
+ this._renderer.states.uPMatrix.set(this.projMatrix);
+ }
+ }
+ /**
+ * Sets the camera’s position and orientation to values that are in-between
+ * those of two other cameras.
+ *
+ * `myCamera.slerp()` uses spherical linear interpolation to calculate a
+ * position and orientation that’s in-between two other cameras. Doing so is
+ * helpful for transitioning smoothly between two perspectives.
+ *
+ * The first two parameters, `cam0` and `cam1`, are the `p5.Camera` objects
+ * that should be used to set the current camera.
+ *
+ * The third parameter, `amt`, is the amount to interpolate between `cam0` and
+ * `cam1`. 0.0 keeps the camera’s position and orientation equal to `cam0`’s,
+ * 0.5 sets them halfway between `cam0`’s and `cam1`’s , and 1.0 sets the
+ * position and orientation equal to `cam1`’s.
+ *
+ * For example, calling `myCamera.slerp(cam0, cam1, 0.1)` sets cam’s position
+ * and orientation very close to `cam0`’s. Calling
+ * `myCamera.slerp(cam0, cam1, 0.9)` sets cam’s position and orientation very
+ * close to `cam1`’s.
+ *
+ * Note: All of the cameras must use the same projection.
+ *
+ * @param {p5.Camera} cam0 first camera.
+ * @param {p5.Camera} cam1 second camera.
+ * @param {Number} amt amount of interpolation between 0.0 (`cam0`) and 1.0 (`cam1`).
+ *
+ * @example
+ * let cam;
+ * let cam0;
+ * let cam1;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the main camera.
+ * // Keep its default settings.
+ * cam = createCamera();
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam0 = createCamera();
+ *
+ * // Create the second camera.
+ * cam1 = createCamera();
+ *
+ * // Place it at the top-right.
+ * cam1.setPosition(400, -400, 800);
+ *
+ * // Point it at the origin.
+ * cam1.lookAt(0, 0, 0);
+ *
+ * // Set the current camera to cam.
+ * setCamera(cam);
+ *
+ * describe('A white cube drawn against a gray background. The camera slowly oscillates between a frontal view and an aerial view.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Calculate the amount to interpolate between cam0 and cam1.
+ * let amt = 0.5 * sin(frameCount * 0.01) + 0.5;
+ *
+ * // Update the main camera's position and orientation.
+ * cam.slerp(cam0, cam1, amt);
+ *
+ * box();
+ * }
+ */
+ slerp(cam0, cam1, amt) {
+ // If t is 0 or 1, do not interpolate and set the argument camera.
+ if (amt === 0) {
+ this.set(cam0);
+ return;
+ } else if (amt === 1) {
+ this.set(cam1);
+ return;
+ }
+
+ // For this cameras is ortho, assume that cam0 and cam1 are also ortho
+ // and interpolate the elements of the projection matrix.
+ // Use logarithmic interpolation for interpolation.
+ if (this.projMatrix.mat4[15] !== 0) {
+ this.projMatrix.setElement(
+ 0,
+ cam0.projMatrix.mat4[0] *
+ Math.pow(cam1.projMatrix.mat4[0] / cam0.projMatrix.mat4[0], amt)
+ );
+ this.projMatrix.setElement(
+ 5,
+ cam0.projMatrix.mat4[5] *
+ Math.pow(cam1.projMatrix.mat4[5] / cam0.projMatrix.mat4[5], amt)
+ );
+ // If the camera is active, make uPMatrix reflect changes in projMatrix.
+ if (this._isActive()) {
+ this._renderer.states.setValue('uPMatrix', this.projMatrix.clone());
+ }
+ }
+
+ // prepare eye vector and center vector of argument cameras.
+ const eye0 = new Vector(cam0.eyeX, cam0.eyeY, cam0.eyeZ);
+ const eye1 = new Vector(cam1.eyeX, cam1.eyeY, cam1.eyeZ);
+ const center0 = new Vector(cam0.centerX, cam0.centerY, cam0.centerZ);
+ const center1 = new Vector(cam1.centerX, cam1.centerY, cam1.centerZ);
+
+ // Calculate the distance between eye and center for each camera.
+ // Logarithmically interpolate these with amt.
+ const dist0 = Vector.dist(eye0, center0);
+ const dist1 = Vector.dist(eye1, center1);
+ const lerpedDist = dist0 * Math.pow(dist1 / dist0, amt);
+
+ // Next, calculate the ratio to interpolate the eye and center by a constant
+ // ratio for each camera. This ratio is the same for both. Also, with this ratio
+ // of points, the distance is the minimum distance of the two points of
+ // the same ratio.
+ // With this method, if the viewpoint is fixed, linear interpolation is performed
+ // at the viewpoint, and if the center is fixed, linear interpolation is performed
+ // at the center, resulting in reasonable interpolation. If both move, the point
+ // halfway between them is taken.
+ const eyeDiff = Vector.sub(eye0, eye1);
+ const diffDiff = eye0.copy().sub(eye1).sub(center0).add(center1);
+ // Suppose there are two line segments. Consider the distance between the points
+ // above them as if they were taken in the same ratio. This calculation figures out
+ // a ratio that minimizes this.
+ // Each line segment is, a line segment connecting the viewpoint and the center
+ // for each camera.
+ const divider = diffDiff.magSq();
+ let ratio = 1; // default.
+ if (divider > 0.000001) {
+ ratio = Vector.dot(eyeDiff, diffDiff) / divider;
+ ratio = Math.max(0, Math.min(ratio, 1));
+ }
+
+ // Take the appropriate proportions and work out the points
+ // that are between the new viewpoint and the new center position.
+ const lerpedMedium = Vector.lerp(
+ Vector.lerp(eye0, center0, ratio),
+ Vector.lerp(eye1, center1, ratio),
+ amt
+ );
+
+ // Prepare each of rotation matrix from their camera matrix
+ const rotMat0 = cam0.cameraMatrix.createSubMatrix3x3();
+ const rotMat1 = cam1.cameraMatrix.createSubMatrix3x3();
+
+ // get front and up vector from local-coordinate-system.
+ const front0 = rotMat0.row(2);
+ const front1 = rotMat1.row(2);
+ const up0 = rotMat0.row(1);
+ const up1 = rotMat1.row(1);
+
+ // prepare new vectors.
+ const newFront = new Vector();
+ const newUp = new Vector();
+ const newEye = new Vector();
+ const newCenter = new Vector();
+
+ // Create the inverse matrix of mat0 by transposing mat0,
+ // and multiply it to mat1 from the right.
+ // This matrix represents the difference between the two.
+ // 'deltaRot' means 'difference of rotation matrices'.
+ const deltaRot = rotMat1.mult(rotMat0.copy().transpose()); // mat1 is 3x3
+
+ // Calculate the trace and from it the cos value of the angle.
+ // An orthogonal matrix is just an orthonormal basis. If this is not the identity
+ // matrix, it is a centered orthonormal basis plus some angle of rotation about
+ // some axis. That's the angle. Letting this be theta, trace becomes 1+2cos(theta).
+ // reference: https://en.wikipedia.org/wiki/Rotation_matrix#Determining_the_angle
+ const diag = deltaRot.diagonal();
+ let cosTheta = 0.5 * (diag[0] + diag[1] + diag[2] - 1);
+
+ // If the angle is close to 0, the two matrices are very close,
+ // so in that case we execute linearly interpolate.
+ if (1 - cosTheta < 0.0000001) {
+ // Obtain the front vector and up vector by linear interpolation
+ // and normalize them.
+ // calculate newEye, newCenter with newFront vector.
+ newFront.set(Vector.lerp(front0, front1, amt)).normalize();
+
+ newEye.set(newFront).mult(ratio * lerpedDist).add(lerpedMedium);
+ newCenter.set(newFront).mult((ratio - 1) * lerpedDist).add(lerpedMedium);
+
+ newUp.set(Vector.lerp(up0, up1, amt)).normalize();
+
+ // set the camera
+ this.camera(
+ newEye.x, newEye.y, newEye.z,
+ newCenter.x, newCenter.y, newCenter.z,
+ newUp.x, newUp.y, newUp.z
+ );
+ return;
+ }
+
+ // Calculates the axis vector and the angle of the difference orthogonal matrix.
+ // The axis vector is what I explained earlier in the comments.
+ // similar calculation is here:
+ // https://github.com/mrdoob/three.js/blob/883249620049d1632e8791732808fefd1a98c871/src/math/Quaternion.js#L294
+ let a, b, c, sinTheta;
+ let invOneMinusCosTheta = 1 / (1 - cosTheta);
+ const maxDiag = Math.max(diag[0], diag[1], diag[2]);
+ const offDiagSum13 = deltaRot.mat3[1] + deltaRot.mat3[3];
+ const offDiagSum26 = deltaRot.mat3[2] + deltaRot.mat3[6];
+ const offDiagSum57 = deltaRot.mat3[5] + deltaRot.mat3[7];
+
+ if (maxDiag === diag[0]) {
+ a = Math.sqrt((diag[0] - cosTheta) * invOneMinusCosTheta); // not zero.
+ invOneMinusCosTheta /= a;
+ b = 0.5 * offDiagSum13 * invOneMinusCosTheta;
+ c = 0.5 * offDiagSum26 * invOneMinusCosTheta;
+ sinTheta = 0.5 * (deltaRot.mat3[7] - deltaRot.mat3[5]) / a;
+
+ } else if (maxDiag === diag[1]) {
+ b = Math.sqrt((diag[1] - cosTheta) * invOneMinusCosTheta); // not zero.
+ invOneMinusCosTheta /= b;
+ c = 0.5 * offDiagSum57 * invOneMinusCosTheta;
+ a = 0.5 * offDiagSum13 * invOneMinusCosTheta;
+ sinTheta = 0.5 * (deltaRot.mat3[2] - deltaRot.mat3[6]) / b;
+
+ } else {
+ c = Math.sqrt((diag[2] - cosTheta) * invOneMinusCosTheta); // not zero.
+ invOneMinusCosTheta /= c;
+ a = 0.5 * offDiagSum26 * invOneMinusCosTheta;
+ b = 0.5 * offDiagSum57 * invOneMinusCosTheta;
+ sinTheta = 0.5 * (deltaRot.mat3[3] - deltaRot.mat3[1]) / c;
+ }
+
+ // Constructs a new matrix after interpolating the angles.
+ // Multiplying mat0 by the first matrix yields mat1, but by creating a state
+ // in the middle of that matrix, you can obtain a matrix that is
+ // an intermediate state between mat0 and mat1.
+ const angle = amt * Math.atan2(sinTheta, cosTheta);
+ const cosAngle = Math.cos(angle);
+ const sinAngle = Math.sin(angle);
+ const oneMinusCosAngle = 1 - cosAngle;
+ const ab = a * b;
+ const bc = b * c;
+ const ca = c * a;
+ // 3x3
+ const lerpedRotMat = new Matrix( [
+ cosAngle + oneMinusCosAngle * a * a,
+ oneMinusCosAngle * ab + sinAngle * c,
+ oneMinusCosAngle * ca - sinAngle * b,
+ oneMinusCosAngle * ab - sinAngle * c,
+ cosAngle + oneMinusCosAngle * b * b,
+ oneMinusCosAngle * bc + sinAngle * a,
+ oneMinusCosAngle * ca + sinAngle * b,
+ oneMinusCosAngle * bc - sinAngle * a,
+ cosAngle + oneMinusCosAngle * c * c
+ ]);
+
+ // Multiply this to mat0 from left to get the interpolated front vector.
+ // calculate newEye, newCenter with newFront vector.
+ lerpedRotMat.multiplyVec(front0, newFront); // this is vec3
+
+ newEye.set(newFront).mult(ratio * lerpedDist).add(lerpedMedium);
+ newCenter.set(newFront).mult((ratio - 1) * lerpedDist).add(lerpedMedium);
+
+ lerpedRotMat.multiplyVec(up0, newUp); // this is vec3
+
+ // We also get the up vector in the same way and set the camera.
+ // The eye position and center position are calculated based on the front vector.
+ this.camera(
+ newEye.x, newEye.y, newEye.z,
+ newCenter.x, newCenter.y, newCenter.z,
+ newUp.x, newUp.y, newUp.z
+ );
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // Camera Helper Methods
+ ////////////////////////////////////////////////////////////////////////////////
+
+ // @TODO: combine this function with _setDefaultCamera to compute these values
+ // as-needed
+ _computeCameraDefaultSettings() {
+ this.defaultAspectRatio = this._renderer.width / this._renderer.height;
+ this.defaultEyeX = 0;
+ this.defaultEyeY = 0;
+ this.defaultEyeZ = 800;
+ this.defaultCameraFOV =
+ 2 * Math.atan(this._renderer.height / 2 / this.defaultEyeZ);
+ this.defaultCenterX = 0;
+ this.defaultCenterY = 0;
+ this.defaultCenterZ = 0;
+ this.defaultCameraNear = this.defaultEyeZ * this._renderer.defaultNearScale();
+ this.defaultCameraFar = this.defaultEyeZ * this._renderer.defaultFarScale();
+ }
+
+ //detect if user didn't set the camera
+ //then call this function below
+ _setDefaultCamera() {
+ this.cameraFOV = this.defaultCameraFOV;
+ this.aspectRatio = this.defaultAspectRatio;
+ this.eyeX = this.defaultEyeX;
+ this.eyeY = this.defaultEyeY;
+ this.eyeZ = this.defaultEyeZ;
+ this.centerX = this.defaultCenterX;
+ this.centerY = this.defaultCenterY;
+ this.centerZ = this.defaultCenterZ;
+ this.upX = 0;
+ this.upY = 1;
+ this.upZ = 0;
+ this.cameraNear = this.defaultCameraNear;
+ this.cameraFar = this.defaultCameraFar;
+
+ this.perspective();
+ this.camera();
+
+ this.cameraType = 'default';
+ }
+
+ _resize() {
+ // If we're using the default camera, update the aspect ratio
+ if (this.cameraType === 'default') {
+ this._computeCameraDefaultSettings();
+ this.cameraFOV = this.defaultCameraFOV;
+ this.aspectRatio = this.defaultAspectRatio;
+ this.perspective();
+ }
+ }
+
+ /**
+ * Returns a copy of a camera.
+ * @private
+ */
+ copy() {
+ const _cam = new Camera(this._renderer);
+ _cam.cameraFOV = this.cameraFOV;
+ _cam.aspectRatio = this.aspectRatio;
+ _cam.eyeX = this.eyeX;
+ _cam.eyeY = this.eyeY;
+ _cam.eyeZ = this.eyeZ;
+ _cam.centerX = this.centerX;
+ _cam.centerY = this.centerY;
+ _cam.centerZ = this.centerZ;
+ _cam.upX = this.upX;
+ _cam.upY = this.upY;
+ _cam.upZ = this.upZ;
+ _cam.cameraNear = this.cameraNear;
+ _cam.cameraFar = this.cameraFar;
+
+ _cam.cameraType = this.cameraType;
+ _cam.useLinePerspective = this.useLinePerspective;
+
+ _cam.cameraMatrix = this.cameraMatrix.copy();
+ _cam.projMatrix = this.projMatrix.copy();
+ _cam.yScale = this.yScale;
+
+ _cam.cameraType = this.cameraType;
+
+ return _cam;
+ }
+
+ clone() {
+ return this.copy();
+ }
+
+ /**
+ * Returns a camera's local axes: left-right, up-down, and forward-backward,
+ * as defined by vectors in world-space.
+ * @private
+ */
+ _getLocalAxes() {
+ // calculate camera local Z vector
+ let z0 = this.eyeX - this.centerX;
+ let z1 = this.eyeY - this.centerY;
+ let z2 = this.eyeZ - this.centerZ;
+
+ // normalize camera local Z vector
+ const eyeDist = Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
+ if (eyeDist !== 0) {
+ z0 /= eyeDist;
+ z1 /= eyeDist;
+ z2 /= eyeDist;
+ }
+
+ // calculate camera Y vector
+ let y0 = this.upX;
+ let y1 = this.upY;
+ let y2 = this.upZ;
+
+ // compute camera local X vector as up vector (local Y) cross local Z
+ let x0 = y1 * z2 - y2 * z1;
+ let x1 = -y0 * z2 + y2 * z0;
+ let x2 = y0 * z1 - y1 * z0;
+
+ // recompute y = z cross x
+ y0 = z1 * x2 - z2 * x1;
+ y1 = -z0 * x2 + z2 * x0;
+ y2 = z0 * x1 - z1 * x0;
+
+ // cross product gives area of parallelogram, which is < 1.0 for
+ // non-perpendicular unit-length vectors; so normalize x, y here:
+ const xmag = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
+ if (xmag !== 0) {
+ x0 /= xmag;
+ x1 /= xmag;
+ x2 /= xmag;
+ }
+
+ const ymag = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
+ if (ymag !== 0) {
+ y0 /= ymag;
+ y1 /= ymag;
+ y2 /= ymag;
+ }
+
+ return {
+ x: [x0, x1, x2],
+ y: [y0, y1, y2],
+ z: [z0, z1, z2]
+ };
+ }
+
+ /**
+ * Orbits the camera about center point. For use with orbitControl().
+ * @private
+ * @param {Number} dTheta change in spherical coordinate theta
+ * @param {Number} dPhi change in spherical coordinate phi
+ * @param {Number} dRadius change in radius
+ */
+ _orbit(dTheta, dPhi, dRadius) {
+ // Calculate the vector and its magnitude from the center to the viewpoint
+ const diffX = this.eyeX - this.centerX;
+ const diffY = this.eyeY - this.centerY;
+ const diffZ = this.eyeZ - this.centerZ;
+ let camRadius = Math.hypot(diffX, diffY, diffZ);
+ // front vector. unit vector from center to eye.
+ const front = new Vector(diffX, diffY, diffZ).normalize();
+ // up vector. normalized camera's up vector.
+ const up = new Vector(this.upX, this.upY, this.upZ).normalize(); // y-axis
+ // side vector. Right when viewed from the front
+ const side = Vector.cross(up, front).normalize(); // x-axis
+ // vertical vector. normalized vector of projection of front vector.
+ const vertical = Vector.cross(side, up); // z-axis
+
+ // update camRadius
+ camRadius *= Math.pow(10, dRadius);
+ // prevent zooming through the center:
+ if (camRadius < this.cameraNear) {
+ camRadius = this.cameraNear;
+ }
+ if (camRadius > this.cameraFar) {
+ camRadius = this.cameraFar;
+ }
+
+ // calculate updated camera angle
+ // Find the angle between the "up" and the "front", add dPhi to that.
+ // angleBetween() may return negative value. Since this specification is subject to change
+ // due to version updates, it cannot be adopted, so here we calculate using a method
+ // that directly obtains the absolute value.
+ const camPhi =
+ Math.acos(Math.max(-1, Math.min(1, Vector.dot(front, up)))) + dPhi;
+ // Rotate by dTheta in the shortest direction from "vertical" to "side"
+ const camTheta = dTheta;
+
+ // Invert camera's upX, upY, upZ if dPhi is below 0 or above PI
+ if (camPhi <= 0 || camPhi >= Math.PI) {
+ this.upX *= -1;
+ this.upY *= -1;
+ this.upZ *= -1;
+ }
+
+ // update eye vector by calculate new front vector
+ up.mult(Math.cos(camPhi));
+ vertical.mult(Math.cos(camTheta) * Math.sin(camPhi));
+ side.mult(Math.sin(camTheta) * Math.sin(camPhi));
+
+ front.set(up).add(vertical).add(side);
+
+ this.eyeX = camRadius * front.x + this.centerX;
+ this.eyeY = camRadius * front.y + this.centerY;
+ this.eyeZ = camRadius * front.z + this.centerZ;
+
+ // update camera
+ this.camera(
+ this.eyeX, this.eyeY, this.eyeZ,
+ this.centerX, this.centerY, this.centerZ,
+ this.upX, this.upY, this.upZ
+ );
+ }
+
+ /**
+ * Orbits the camera about center point. For use with orbitControl().
+ * Unlike _orbit(), the direction of rotation always matches the direction of pointer movement.
+ * @private
+ * @param {Number} dx the x component of the rotation vector.
+ * @param {Number} dy the y component of the rotation vector.
+ * @param {Number} dRadius change in radius
+ */
+ _orbitFree(dx, dy, dRadius) {
+ // Calculate the vector and its magnitude from the center to the viewpoint
+ const diffX = this.eyeX - this.centerX;
+ const diffY = this.eyeY - this.centerY;
+ const diffZ = this.eyeZ - this.centerZ;
+ let camRadius = Math.hypot(diffX, diffY, diffZ);
+ // front vector. unit vector from center to eye.
+ const front = new Vector(diffX, diffY, diffZ).normalize();
+ // up vector. camera's up vector.
+ const up = new Vector(this.upX, this.upY, this.upZ);
+ // side vector. Right when viewed from the front. (like x-axis)
+ const side = Vector.cross(up, front).normalize();
+ // down vector. Bottom when viewed from the front. (like y-axis)
+ const down = Vector.cross(front, side);
+
+ // side vector and down vector are no longer used as-is.
+ // Create a vector representing the direction of rotation
+ // in the form cos(direction)*side + sin(direction)*down.
+ // Make the current side vector into this.
+ const directionAngle = Math.atan2(dy, dx);
+ down.mult(Math.sin(directionAngle));
+ side.mult(Math.cos(directionAngle)).add(down);
+ // The amount of rotation is the size of the vector (dx, dy).
+ const rotAngle = Math.sqrt(dx * dx + dy * dy);
+ // The vector that is orthogonal to both the front vector and
+ // the rotation direction vector is the rotation axis vector.
+ const axis = Vector.cross(front, side);
+
+ // update camRadius
+ camRadius *= Math.pow(10, dRadius);
+ // prevent zooming through the center:
+ if (camRadius < this.cameraNear) {
+ camRadius = this.cameraNear;
+ }
+ if (camRadius > this.cameraFar) {
+ camRadius = this.cameraFar;
+ }
+
+ // If the axis vector is likened to the z-axis, the front vector is
+ // the x-axis and the side vector is the y-axis. Rotate the up and front
+ // vectors respectively by thinking of them as rotations around the z-axis.
+
+ // Calculate the components by taking the dot product and
+ // calculate a rotation based on that.
+ const c = Math.cos(rotAngle);
+ const s = Math.sin(rotAngle);
+ const dotFront = up.dot(front);
+ const dotSide = up.dot(side);
+ const ux = dotFront * c + dotSide * s;
+ const uy = -dotFront * s + dotSide * c;
+ const uz = up.dot(axis);
+ up.x = ux * front.x + uy * side.x + uz * axis.x;
+ up.y = ux * front.y + uy * side.y + uz * axis.y;
+ up.z = ux * front.z + uy * side.z + uz * axis.z;
+ // We won't be using the side vector and the front vector anymore,
+ // so let's make the front vector into the vector from the center to the new eye.
+ side.mult(-s);
+ front.mult(c).add(side).mult(camRadius);
+
+ // it's complete. let's update camera.
+ this.camera(
+ front.x + this.centerX,
+ front.y + this.centerY,
+ front.z + this.centerZ,
+ this.centerX, this.centerY, this.centerZ,
+ up.x, up.y, up.z
+ );
+ }
+
+ /**
+ * Returns true if camera is currently attached to renderer.
+ * @private
+ */
+ _isActive() {
+ return this === this._renderer.states.curCamera;
+ }
+ }
+ function camera(p5, fn){
+ ////////////////////////////////////////////////////////////////////////////////
+ // p5.Prototype Methods
+ ////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Sets the position and orientation of the current camera in a 3D sketch.
+ *
+ * `camera()` allows objects to be viewed from different angles. It has nine
+ * parameters that are all optional.
+ *
+ * The first three parameters, `x`, `y`, and `z`, are the coordinates of the
+ * camera’s position. For example, calling `camera(0, 0, 0)` places the camera
+ * at the origin `(0, 0, 0)`. By default, the camera is placed at
+ * `(0, 0, 800)`.
+ *
+ * The next three parameters, `centerX`, `centerY`, and `centerZ` are the
+ * coordinates of the point where the camera faces. For example, calling
+ * `camera(0, 0, 0, 10, 20, 30)` places the camera at the origin `(0, 0, 0)`
+ * and points it at `(10, 20, 30)`. By default, the camera points at the
+ * origin `(0, 0, 0)`.
+ *
+ * The last three parameters, `upX`, `upY`, and `upZ` are the components of
+ * the "up" vector. The "up" vector orients the camera’s y-axis. For example,
+ * calling `camera(0, 0, 0, 10, 20, 30, 0, -1, 0)` places the camera at the
+ * origin `(0, 0, 0)`, points it at `(10, 20, 30)`, and sets the "up" vector
+ * to `(0, -1, 0)` which is like holding it upside-down. By default, the "up"
+ * vector is `(0, 1, 0)`.
+ *
+ * Note: `camera()` can only be used in WebGL mode.
+ *
+ * @method camera
+ * @for p5
+ * @param {Number} [x] x-coordinate of the camera. Defaults to 0.
+ * @param {Number} [y] y-coordinate of the camera. Defaults to 0.
+ * @param {Number} [z] z-coordinate of the camera. Defaults to 800.
+ * @param {Number} [centerX] x-coordinate of the point the camera faces. Defaults to 0.
+ * @param {Number} [centerY] y-coordinate of the point the camera faces. Defaults to 0.
+ * @param {Number} [centerZ] z-coordinate of the point the camera faces. Defaults to 0.
+ * @param {Number} [upX] x-component of the camera’s "up" vector. Defaults to 0.
+ * @param {Number} [upY] y-component of the camera’s "up" vector. Defaults to 1.
+ * @param {Number} [upZ] z-component of the camera’s "up" vector. Defaults to 0.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white cube on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Move the camera to the top-right.
+ * camera(200, -400, 800);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white cube apperas to sway left and right on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Calculate the camera's x-coordinate.
+ * let x = 400 * cos(frameCount * 0.01);
+ *
+ * // Orbit the camera around the box.
+ * camera(x, -400, 800);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ * @example
+ * // Adjust the range sliders to change the camera's position.
+ *
+ * let xSlider;
+ * let ySlider;
+ * let zSlider;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create slider objects to set the camera's coordinates.
+ * xSlider = createSlider(-400, 400, 400);
+ * xSlider.position(0, 100);
+ * xSlider.size(100);
+ * ySlider = createSlider(-400, 400, -200);
+ * ySlider.position(0, 120);
+ * ySlider.size(100);
+ * zSlider = createSlider(0, 1600, 800);
+ * zSlider.position(0, 140);
+ * zSlider.size(100);
+ *
+ * describe(
+ * 'A white cube drawn against a gray background. Three range sliders appear beneath the image. The camera position changes when the user moves the sliders.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Get the camera's coordinates from the sliders.
+ * let x = xSlider.value();
+ * let y = ySlider.value();
+ * let z = zSlider.value();
+ *
+ * // Move the camera.
+ * camera(x, y, z);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ */
+ fn.camera = function (...args) {
+ this._assert3d('camera');
+ // p5._validateParameters('camera', args);
+ this._renderer.camera(...args);
+ return this;
+ };
+
+ /**
+ * Sets a perspective projection for the current camera in a 3D sketch.
+ *
+ * In a perspective projection, shapes that are further from the camera appear
+ * smaller than shapes that are near the camera. This technique, called
+ * foreshortening, creates realistic 3D scenes. It’s applied by default in
+ * WebGL mode.
+ *
+ * `perspective()` changes the camera’s perspective by changing its viewing
+ * frustum. The frustum is the volume of space that’s visible to the camera.
+ * Its shape is a pyramid with its top cut off. The camera is placed where
+ * the top of the pyramid should be and views everything between the frustum’s
+ * top (near) plane and its bottom (far) plane.
+ *
+ * The first parameter, `fovy`, is the camera’s vertical field of view. It’s
+ * an angle that describes how tall or narrow a view the camera has. For
+ * example, calling `perspective(0.5)` sets the camera’s vertical field of
+ * view to 0.5 radians. By default, `fovy` is calculated based on the sketch’s
+ * height and the camera’s default z-coordinate, which is 800. The formula for
+ * the default `fovy` is `2 * atan(height / 2 / 800)`.
+ *
+ * The second parameter, `aspect`, is the camera’s aspect ratio. It’s a number
+ * that describes the ratio of the top plane’s width to its height. For
+ * example, calling `perspective(0.5, 1.5)` sets the camera’s field of view to
+ * 0.5 radians and aspect ratio to 1.5, which would make shapes appear thinner
+ * on a square canvas. By default, aspect is set to `width / height`.
+ *
+ * The third parameter, `near`, is the distance from the camera to the near
+ * plane. For example, calling `perspective(0.5, 1.5, 100)` sets the camera’s
+ * field of view to 0.5 radians, its aspect ratio to 1.5, and places the near
+ * plane 100 pixels from the camera. Any shapes drawn less than 100 pixels
+ * from the camera won’t be visible. By default, near is set to `0.1 * 800`,
+ * which is 1/10th the default distance between the camera and the origin.
+ *
+ * The fourth parameter, `far`, is the distance from the camera to the far
+ * plane. For example, calling `perspective(0.5, 1.5, 100, 10000)` sets the
+ * camera’s field of view to 0.5 radians, its aspect ratio to 1.5, places the
+ * near plane 100 pixels from the camera, and places the far plane 10,000
+ * pixels from the camera. Any shapes drawn more than 10,000 pixels from the
+ * camera won’t be visible. By default, far is set to `10 * 800`, which is 10
+ * times the default distance between the camera and the origin.
+ *
+ * Note: `perspective()` can only be used in WebGL mode.
+ *
+ * @method perspective
+ * @for p5
+ * @param {Number} [fovy] camera frustum vertical field of view. Defaults to
+ * `2 * atan(height / 2 / 800)`.
+ * @param {Number} [aspect] camera frustum aspect ratio. Defaults to
+ * `width / height`.
+ * @param {Number} [near] distance from the camera to the near clipping plane.
+ * Defaults to `0.1 * 800`.
+ * @param {Number} [far] distance from the camera to the far clipping plane.
+ * Defaults to `10 * 800`.
+ * @chainable
+ *
+ * @example
+ * // Double-click to squeeze the box.
+ *
+ * let isSqueezed = false;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white rectangular prism on a gray background. The box appears to become thinner when the user double-clicks.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Place the camera at the top-right.
+ * camera(400, -400, 800);
+ *
+ * if (isSqueezed === true) {
+ * // Set fovy to 0.2.
+ * // Set aspect to 1.5.
+ * perspective(0.2, 1.5);
+ * }
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ * // Change the camera's perspective when the user double-clicks.
+ * function doubleClicked() {
+ * isSqueezed = true;
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white rectangular prism on a gray background. The prism moves away from the camera until it disappears.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Place the camera at the top-right.
+ * camera(400, -400, 800);
+ *
+ * // Set fovy to 0.2.
+ * // Set aspect to 1.5.
+ * // Set near to 600.
+ * // Set far to 1200.
+ * perspective(0.2, 1.5, 600, 1200);
+ *
+ * // Move the origin away from the camera.
+ * let x = -frameCount;
+ * let y = frameCount;
+ * let z = -2 * frameCount;
+ * translate(x, y, z);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ */
+ fn.perspective = function (...args) {
+ this._assert3d('perspective');
+ // p5._validateParameters('perspective', args);
+ this._renderer.perspective(...args);
+ return this;
+ };
+
+ /**
+ * Enables or disables perspective for lines in 3D sketches.
+ *
+ * In WebGL mode, lines can be drawn with a thinner stroke when they’re
+ * further from the camera. Doing so gives them a more realistic appearance.
+ *
+ * By default, lines are drawn differently based on the type of perspective
+ * being used:
+ * - `perspective()` and `frustum()` simulate a realistic perspective. In
+ * these modes, stroke weight is affected by the line’s distance from the
+ * camera. Doing so results in a more natural appearance. `perspective()` is
+ * the default mode for 3D sketches.
+ * - `ortho()` doesn’t simulate a realistic perspective. In this mode, stroke
+ * weights are consistent regardless of the line’s distance from the camera.
+ * Doing so results in a more predictable and consistent appearance.
+ *
+ * `linePerspective()` can override the default line drawing mode.
+ *
+ * The parameter, `enable`, is optional. It’s a `Boolean` value that sets the
+ * way lines are drawn. If `true` is passed, as in `linePerspective(true)`,
+ * then lines will appear thinner when they are further from the camera. If
+ * `false` is passed, as in `linePerspective(false)`, then lines will have
+ * consistent stroke weights regardless of their distance from the camera. By
+ * default, `linePerspective()` is enabled.
+ *
+ * Calling `linePerspective()` without passing an argument returns `true` if
+ * it's enabled and `false` if not.
+ *
+ * Note: `linePerspective()` can only be used in WebGL mode.
+ *
+ * @method linePerspective
+ * @for p5
+ * @param {Boolean} enable whether to enable line perspective.
+ *
+ * @example
+ * // Double-click the canvas to toggle the line perspective.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe(
+ * 'A white cube with black edges on a gray background. Its edges toggle between thick and thin when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 600);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -40);
+ * box(10);
+ * }
+ * }
+ *
+ * // Toggle the line perspective when the user double-clicks.
+ * function doubleClicked() {
+ * let isEnabled = linePerspective();
+ * linePerspective(!isEnabled);
+ * }
+ *
+ * @example
+ * // Double-click the canvas to toggle the line perspective.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe(
+ * 'A row of cubes with black edges on a gray background. Their edges toggle between thick and thin when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Use an orthographic projection.
+ * ortho();
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 600);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -40);
+ * box(10);
+ * }
+ * }
+ *
+ * // Toggle the line perspective when the user double-clicks.
+ * function doubleClicked() {
+ * let isEnabled = linePerspective();
+ * linePerspective(!isEnabled);
+ * }
+ */
+ /**
+ * @method linePerspective
+ * @return {boolean} whether line perspective is enabled.
+ */
+ fn.linePerspective = function (enable) {
+ // p5._validateParameters('linePerspective', arguments);
+ if (!(this._renderer instanceof Renderer3D)) {
+ throw new Error('linePerspective() must be called in WebGL mode.');
+ }
+ return this._renderer.linePerspective(enable);
+ };
+
+
+ /**
+ * Sets an orthographic projection for the current camera in a 3D sketch.
+ *
+ * In an orthographic projection, shapes with the same size always appear the
+ * same size, regardless of whether they are near or far from the camera.
+ *
+ * `ortho()` changes the camera’s perspective by changing its viewing frustum
+ * from a truncated pyramid to a rectangular prism. The camera is placed in
+ * front of the frustum and views everything between the frustum’s near plane
+ * and its far plane. `ortho()` has six optional parameters to define the
+ * frustum.
+ *
+ * The first four parameters, `left`, `right`, `bottom`, and `top`, set the
+ * coordinates of the frustum’s sides, bottom, and top. For example, calling
+ * `ortho(-100, 100, 200, -200)` creates a frustum that’s 200 pixels wide and
+ * 400 pixels tall. By default, these coordinates are set based on the
+ * sketch’s width and height, as in
+ * `ortho(-width / 2, width / 2, -height / 2, height / 2)`.
+ *
+ * The last two parameters, `near` and `far`, set the distance of the
+ * frustum’s near and far plane from the camera. For example, calling
+ * `ortho(-100, 100, 200, 200, 50, 1000)` creates a frustum that’s 200 pixels
+ * wide, 400 pixels tall, starts 50 pixels from the camera, and ends 1,000
+ * pixels from the camera. By default, `near` and `far` are set to 0 and
+ * `max(width, height) + 800`, respectively.
+ *
+ * Note: `ortho()` can only be used in WebGL mode.
+ *
+ * @method ortho
+ * @for p5
+ * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 2`.
+ * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 2`.
+ * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 2`.
+ * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 2`.
+ * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to 0.
+ * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `max(width, height) + 800`.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A row of tiny, white cubes on a gray background. All the cubes appear the same size.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Apply an orthographic projection.
+ * ortho();
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 600);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -40);
+ * box(10);
+ * }
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white cube on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Apply an orthographic projection.
+ * // Center the frustum.
+ * // Set its width and height to 20.
+ * // Place its near plane 300 pixels from the camera.
+ * // Place its far plane 350 pixels from the camera.
+ * ortho(-10, 10, -10, 10, 300, 350);
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 600);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -40);
+ * box(10);
+ * }
+ * }
+ */
+ fn.ortho = function (...args) {
+ this._assert3d('ortho');
+ // p5._validateParameters('ortho', args);
+ this._renderer.ortho(...args);
+ return this;
+ };
+
+ /**
+ * Sets the frustum of the current camera in a 3D sketch.
+ *
+ * In a frustum projection, shapes that are further from the camera appear
+ * smaller than shapes that are near the camera. This technique, called
+ * foreshortening, creates realistic 3D scenes.
+ *
+ * `frustum()` changes the default camera’s perspective by changing its
+ * viewing frustum. The frustum is the volume of space that’s visible to the
+ * camera. The frustum’s shape is a pyramid with its top cut off. The camera
+ * is placed where the top of the pyramid should be and points towards the
+ * base of the pyramid. It views everything within the frustum.
+ *
+ * The first four parameters, `left`, `right`, `bottom`, and `top`, set the
+ * coordinates of the frustum’s sides, bottom, and top. For example, calling
+ * `frustum(-100, 100, 200, -200)` creates a frustum that’s 200 pixels wide
+ * and 400 pixels tall. By default, these coordinates are set based on the
+ * sketch’s width and height, as in
+ * `ortho(-width / 20, width / 20, height / 20, -height / 20)`.
+ *
+ * The last two parameters, `near` and `far`, set the distance of the
+ * frustum’s near and far plane from the camera. For example, calling
+ * `ortho(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s 200 pixels
+ * wide, 400 pixels tall, starts 50 pixels from the camera, and ends 1,000
+ * pixels from the camera. By default, near is set to `0.1 * 800`, which is
+ * 1/10th the default distance between the camera and the origin. `far` is set
+ * to `10 * 800`, which is 10 times the default distance between the camera
+ * and the origin.
+ *
+ * Note: `frustum()` can only be used in WebGL mode.
+ *
+ * @method frustum
+ * @for p5
+ * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 20`.
+ * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 20`.
+ * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 20`.
+ * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 20`.
+ * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to `0.1 * 800`.
+ * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `10 * 800`.
+ * @chainable
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A row of white cubes on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Apply the default frustum projection.
+ * frustum();
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 600);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -40);
+ * box(10);
+ * }
+ * }
+ *
+ * @example
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ * describe('A white cube on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Adjust the frustum.
+ * // Center it.
+ * // Set its width and height to 20 pixels.
+ * // Place its near plane 300 pixels from the camera.
+ * // Place its far plane 350 pixels from the camera.
+ * frustum(-10, 10, -10, 10, 300, 350);
+ *
+ * // Translate the origin toward the camera.
+ * translate(-10, 10, 600);
+ *
+ * // Rotate the coordinate system.
+ * rotateY(-0.1);
+ * rotateX(-0.1);
+ *
+ * // Draw the row of boxes.
+ * for (let i = 0; i < 6; i += 1) {
+ * translate(0, 0, -40);
+ * box(10);
+ * }
+ * }
+ */
+ fn.frustum = function (...args) {
+ this._assert3d('frustum');
+ // p5._validateParameters('frustum', args);
+ this._renderer.frustum(...args);
+ return this;
+ };
+
+ /**
+ * Creates a new p5.Camera object.
+ *
+ * The new camera is initialized with a default position `(0, 0, 800)` and a
+ * default perspective projection. Its properties can be controlled with
+ * p5.Camera methods such as
+ * `myCamera.lookAt(0, 0, 0)`.
+ *
+ * Note: Every 3D sketch starts with a default camera initialized.
+ * This camera can be controlled with the functions
+ * camera(),
+ * perspective(),
+ * ortho(), and
+ * frustum() if it's the only camera in the scene.
+ *
+ * Note: `createCamera()` can only be used in WebGL mode.
+ *
+ * @method createCamera
+ * @return {p5.Camera} the new camera.
+ * @for p5
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let usingCam1 = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * // Place it at the top-left.
+ * // Point it at the origin.
+ * cam2 = createCamera();
+ * cam2.setPosition(400, -400, 800);
+ * cam2.lookAt(0, 0, 0);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe('A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (usingCam1 === true) {
+ * setCamera(cam2);
+ * usingCam1 = false;
+ * } else {
+ * setCamera(cam1);
+ * usingCam1 = true;
+ * }
+ * }
+ */
+ fn.createCamera = function () {
+ this._assert3d('createCamera');
+
+ return this._renderer.createCamera();
+ };
+
+ /**
+ * Sets the current (active) camera of a 3D sketch.
+ *
+ * `setCamera()` allows for switching between multiple cameras created with
+ * createCamera().
+ *
+ * Note: `setCamera()` can only be used in WebGL mode.
+ *
+ * @method setCamera
+ * @param {p5.Camera} cam camera that should be made active.
+ * @for p5
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let usingCam1 = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * // Place it at the top-left.
+ * // Point it at the origin.
+ * cam2 = createCamera();
+ * cam2.setPosition(400, -400, 800);
+ * cam2.lookAt(0, 0, 0);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe('A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (usingCam1 === true) {
+ * setCamera(cam2);
+ * usingCam1 = false;
+ * } else {
+ * setCamera(cam1);
+ * usingCam1 = true;
+ * }
+ * }
+ */
+ fn.setCamera = function (cam) {
+ this._renderer.setCamera(cam);
+ };
+
+ /**
+ * A class to describe a camera for viewing a 3D sketch.
+ *
+ * Each `p5.Camera` object represents a camera that views a section of 3D
+ * space. It stores information about the camera’s position, orientation, and
+ * projection.
+ *
+ * In WebGL mode, the default camera is a `p5.Camera` object that can be
+ * controlled with the camera(),
+ * perspective(),
+ * ortho(), and
+ * frustum() functions. Additional cameras can be
+ * created with createCamera() and activated
+ * with setCamera().
+ *
+ * Note: `p5.Camera`’s methods operate in two coordinate systems:
+ * - The “world” coordinate system describes positions in terms of their
+ * relationship to the origin along the x-, y-, and z-axes. For example,
+ * calling `myCamera.setPosition()` places the camera in 3D space using
+ * "world" coordinates.
+ * - The "local" coordinate system describes positions from the camera's point
+ * of view: left-right, up-down, and forward-backward. For example, calling
+ * `myCamera.move()` moves the camera along its own axes.
+ *
+ * @class p5.Camera
+ * @constructor
+ * @param {RendererGL} rendererGL instance of WebGL renderer
+ *
+ * @example
+ * let cam;
+ * let delta = 0.001;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube goes in and out of view as the camera pans left and right.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Turn the camera left and right, called "panning".
+ * cam.pan(delta);
+ *
+ * // Switch directions every 120 frames.
+ * if (frameCount % 120 === 0) {
+ * delta *= -1;
+ * }
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ * @example
+ * // Double-click to toggle between cameras.
+ *
+ * let cam1;
+ * let cam2;
+ * let isDefaultCamera = true;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create the first camera.
+ * // Keep its default settings.
+ * cam1 = createCamera();
+ *
+ * // Create the second camera.
+ * // Place it at the top-left.
+ * // Point it at the origin.
+ * cam2 = createCamera();
+ * cam2.setPosition(400, -400, 800);
+ * cam2.lookAt(0, 0, 0);
+ *
+ * // Set the current camera to cam1.
+ * setCamera(cam1);
+ *
+ * describe(
+ * 'A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Draw the box.
+ * box();
+ * }
+ *
+ * // Toggle the current camera when the user double-clicks.
+ * function doubleClicked() {
+ * if (isDefaultCamera === true) {
+ * setCamera(cam2);
+ * isDefaultCamera = false;
+ * } else {
+ * setCamera(cam1);
+ * isDefaultCamera = true;
+ * }
+ * }
+ */
+ p5.Camera = Camera;
+
+ Renderer3D.prototype.camera = function(...args) {
+ this.states.setValue('curCamera', this.states.curCamera.clone());
+ this.states.curCamera.camera(...args);
+ };
+
+ Renderer3D.prototype.perspective = function(...args) {
+ this.states.setValue('curCamera', this.states.curCamera.clone());
+ this.states.curCamera.perspective(...args);
+ };
+
+ Renderer3D.prototype.linePerspective = function(enable) {
+ if (enable !== undefined) {
+ this.states.setValue('curCamera', this.states.curCamera.clone());
+ // Set the line perspective if enable is provided
+ this.states.curCamera.useLinePerspective = enable;
+ } else {
+ // If no argument is provided, return the current value
+ return this.states.curCamera.useLinePerspective;
+ }
+ };
+
+ Renderer3D.prototype.ortho = function(...args) {
+ this.states.setValue('curCamera', this.states.curCamera.clone());
+ this.states.curCamera.ortho(...args);
+ };
+
+ Renderer3D.prototype.frustum = function(...args) {
+ this.states.setValue('curCamera', this.states.curCamera.clone());
+ this.states.curCamera.frustum(...args);
+ };
+
+ Renderer3D.prototype.createCamera = function() {
+ // compute default camera settings, then set a default camera
+ const _cam = new Camera(this);
+ _cam._computeCameraDefaultSettings();
+ _cam._setDefaultCamera();
+
+ return _cam;
+ };
+
+ Renderer3D.prototype.setCamera = function(cam) {
+ this.states.setValue('curCamera', cam);
+
+ // set the projection matrix (which is not normally updated each frame)
+ this.states.setValue('uPMatrix', this.states.uPMatrix.clone());
+ this.states.uPMatrix.set(cam.projMatrix);
+ this.states.setValue('uViewMatrix', this.states.uViewMatrix.clone());
+ this.states.uViewMatrix.set(cam.cameraMatrix);
+ };
+
+ /**
+ * The camera’s x-coordinate.
+ *
+ * By default, the camera’s x-coordinate is set to 0 in "world" space.
+ *
+ * @property {Number} eyeX
+ * @for p5.Camera
+ * @readonly
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The text "eyeX: 0" is written in black beneath it.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Display the value of eyeX, rounded to the nearest integer.
+ * text(`eyeX: ${round(cam.eyeX)}`, 0, 45);
+ * }
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube appears to move left and right as the camera moves. The text "eyeX: X" is written in black beneath the cube. X oscillates between -25 and 25.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Calculate the new x-coordinate.
+ * let x = 25 * sin(frameCount * 0.01);
+ *
+ * // Set the camera's position.
+ * cam.setPosition(x, -400, 800);
+ *
+ * // Display the value of eyeX, rounded to the nearest integer.
+ * text(`eyeX: ${round(cam.eyeX)}`, 0, 45);
+ * }
+ */
+
+ /**
+ * The camera’s y-coordinate.
+ *
+ * By default, the camera’s y-coordinate is set to 0 in "world" space.
+ *
+ * @property {Number} eyeY
+ * @for p5.Camera
+ * @readonly
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * // Set the camera.
+ * setCamera(cam);
+ *
+ * describe(
+ * 'A white cube on a gray background. The text "eyeY: -400" is written in black beneath it.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Display the value of eyeY, rounded to the nearest integer.
+ * text(`eyeY: ${round(cam.eyeY)}`, 0, 45);
+ * }
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube appears to move up and down as the camera moves. The text "eyeY: Y" is written in black beneath the cube. Y oscillates between -374 and -425.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Calculate the new y-coordinate.
+ * let y = 25 * sin(frameCount * 0.01) - 400;
+ *
+ * // Set the camera's position.
+ * cam.setPosition(0, y, 800);
+ *
+ * // Display the value of eyeY, rounded to the nearest integer.
+ * text(`eyeY: ${round(cam.eyeY)}`, 0, 45);
+ * }
+ */
+
+ /**
+ * The camera’s z-coordinate.
+ *
+ * By default, the camera’s z-coordinate is set to 800 in "world" space.
+ *
+ * @property {Number} eyeZ
+ * @for p5.Camera
+ * @readonly
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The text "eyeZ: 800" is written in black beneath it.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Display the value of eyeZ, rounded to the nearest integer.
+ * text(`eyeZ: ${round(cam.eyeZ)}`, 0, 45);
+ * }
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at the origin.
+ * cam.lookAt(0, 0, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube appears to move forward and back as the camera moves. The text "eyeZ: Z" is written in black beneath the cube. Z oscillates between 700 and 900.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Calculate the new z-coordinate.
+ * let z = 100 * sin(frameCount * 0.01) + 800;
+ *
+ * // Set the camera's position.
+ * cam.setPosition(0, -400, z);
+ *
+ * // Display the value of eyeZ, rounded to the nearest integer.
+ * text(`eyeZ: ${round(cam.eyeZ)}`, 0, 45);
+ * }
+ */
+
+ /**
+ * The x-coordinate of the place where the camera looks.
+ *
+ * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
+ * `myCamera.centerX` is 0.
+ *
+ * @property {Number} centerX
+ * @for p5.Camera
+ * @readonly
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at (10, 20, -30).
+ * cam.lookAt(10, 20, -30);
+ *
+ * describe(
+ * 'A white cube on a gray background. The text "centerX: 10" is written in black beneath it.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Display the value of centerX, rounded to the nearest integer.
+ * text(`centerX: ${round(cam.centerX)}`, 0, 45);
+ * }
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-right.
+ * cam.setPosition(100, -400, 800);
+ *
+ * // Point the camera at (10, 20, -30).
+ * cam.lookAt(10, 20, -30);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube appears to move left and right as the camera shifts its focus. The text "centerX: X" is written in black beneath the cube. X oscillates between -15 and 35.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Calculate the new x-coordinate.
+ * let x = 25 * sin(frameCount * 0.01) + 10;
+ *
+ * // Point the camera.
+ * cam.lookAt(x, 20, -30);
+ *
+ * // Display the value of centerX, rounded to the nearest integer.
+ * text(`centerX: ${round(cam.centerX)}`, 0, 45);
+ * }
+ */
+
+ /**
+ * The y-coordinate of the place where the camera looks.
+ *
+ * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
+ * `myCamera.centerY` is 0.
+ *
+ * @property {Number} centerY
+ * @for p5.Camera
+ * @readonly
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at (10, 20, -30).
+ * cam.lookAt(10, 20, -30);
+ *
+ * describe(
+ * 'A white cube on a gray background. The text "centerY: 20" is written in black beneath it.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Display the value of centerY, rounded to the nearest integer.
+ * text(`centerY: ${round(cam.centerY)}`, 0, 45);
+ * }
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-right.
+ * cam.setPosition(100, -400, 800);
+ *
+ * // Point the camera at (10, 20, -30).
+ * cam.lookAt(10, 20, -30);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube appears to move up and down as the camera shifts its focus. The text "centerY: Y" is written in black beneath the cube. Y oscillates between -5 and 45.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Calculate the new y-coordinate.
+ * let y = 25 * sin(frameCount * 0.01) + 20;
+ *
+ * // Point the camera.
+ * cam.lookAt(10, y, -30);
+ *
+ * // Display the value of centerY, rounded to the nearest integer.
+ * text(`centerY: ${round(cam.centerY)}`, 0, 45);
+ * }
+ */
+
+ /**
+ * The y-coordinate of the place where the camera looks.
+ *
+ * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
+ * `myCamera.centerZ` is 0.
+ *
+ * @property {Number} centerZ
+ * @for p5.Camera
+ * @readonly
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-center.
+ * cam.setPosition(0, -400, 800);
+ *
+ * // Point the camera at (10, 20, -30).
+ * cam.lookAt(10, 20, -30);
+ *
+ * describe(
+ * 'A white cube on a gray background. The text "centerZ: -30" is written in black beneath it.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Display the value of centerZ, rounded to the nearest integer.
+ * text(`centerZ: ${round(cam.centerZ)}`, 0, 45);
+ * }
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Place the camera at the top-right.
+ * cam.setPosition(100, -400, 800);
+ *
+ * // Point the camera at (10, 20, -30).
+ * cam.lookAt(10, 20, -30);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube appears to move forward and back as the camera shifts its focus. The text "centerZ: Z" is written in black beneath the cube. Z oscillates between -55 and -25.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Calculate the new z-coordinate.
+ * let z = 25 * sin(frameCount * 0.01) - 30;
+ *
+ * // Point the camera.
+ * cam.lookAt(10, 20, z);
+ *
+ * // Display the value of centerZ, rounded to the nearest integer.
+ * text(`centerZ: ${round(cam.centerZ)}`, 0, 45);
+ * }
+ */
+
+ /**
+ * The x-component of the camera's "up" vector.
+ *
+ * The camera's "up" vector orients its y-axis. By default, the "up" vector is
+ * `(0, 1, 0)`, so its x-component is 0 in "local" space.
+ *
+ * @property {Number} upX
+ * @for p5.Camera
+ * @readonly
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-right: (100, -400, 800)
+ * // Point it at the origin: (0, 0, 0)
+ * // Set its "up" vector: (0, 1, 0).
+ * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The text "upX: 0" is written in black beneath it.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Display the value of upX, rounded to the nearest tenth.
+ * text(`upX: ${round(cam.upX, 1)}`, 0, 45);
+ * }
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-right: (100, -400, 800)
+ * // Point it at the origin: (0, 0, 0)
+ * // Set its "up" vector: (0, 1, 0).
+ * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube appears to rock back and forth. The text "upX: X" is written in black beneath it. X oscillates between -1 and 1.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Calculate the x-component.
+ * let x = sin(frameCount * 0.01);
+ *
+ * // Update the camera's "up" vector.
+ * cam.camera(100, -400, 800, 0, 0, 0, x, 1, 0);
+ *
+ * // Display the value of upX, rounded to the nearest tenth.
+ * text(`upX: ${round(cam.upX, 1)}`, 0, 45);
+ * }
+ */
+
+ /**
+ * The y-component of the camera's "up" vector.
+ *
+ * The camera's "up" vector orients its y-axis. By default, the "up" vector is
+ * `(0, 1, 0)`, so its y-component is 1 in "local" space.
+ *
+ * @property {Number} upY
+ * @for p5.Camera
+ * @readonly
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-right: (100, -400, 800)
+ * // Point it at the origin: (0, 0, 0)
+ * // Set its "up" vector: (0, 1, 0).
+ * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The text "upY: 1" is written in black beneath it.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Display the value of upY, rounded to the nearest tenth.
+ * text(`upY: ${round(cam.upY, 1)}`, 0, 45);
+ * }
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-right: (100, -400, 800)
+ * // Point it at the origin: (0, 0, 0)
+ * // Set its "up" vector: (0, 1, 0).
+ * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube flips upside-down periodically. The text "upY: Y" is written in black beneath it. Y oscillates between -1 and 1.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Calculate the y-component.
+ * let y = sin(frameCount * 0.01);
+ *
+ * // Update the camera's "up" vector.
+ * cam.camera(100, -400, 800, 0, 0, 0, 0, y, 0);
+ *
+ * // Display the value of upY, rounded to the nearest tenth.
+ * text(`upY: ${round(cam.upY, 1)}`, 0, 45);
+ * }
+ */
+
+ /**
+ * The z-component of the camera's "up" vector.
+ *
+ * The camera's "up" vector orients its y-axis. By default, the "up" vector is
+ * `(0, 1, 0)`, so its z-component is 0 in "local" space.
+ *
+ * @property {Number} upZ
+ * @for p5.Camera
+ * @readonly
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-right: (100, -400, 800)
+ * // Point it at the origin: (0, 0, 0)
+ * // Set its "up" vector: (0, 1, 0).
+ * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The text "upZ: 0" is written in black beneath it.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Display the value of upZ, rounded to the nearest tenth.
+ * text(`upZ: ${round(cam.upZ, 1)}`, 0, 45);
+ * }
+ *
+ * @example
+ * let cam;
+ * let font;
+ *
+ * async function setup() {
+ * // Load a font and create a p5.Font object.
+ * font = await loadFont('assets/inconsolata.otf');
+ * createCanvas(100, 100, WEBGL);
+ *
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ *
+ * // Set the camera
+ * setCamera(cam);
+ *
+ * // Place the camera at the top-right: (100, -400, 800)
+ * // Point it at the origin: (0, 0, 0)
+ * // Set its "up" vector: (0, 1, 0).
+ * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
+ *
+ * describe(
+ * 'A white cube on a gray background. The cube appears to rock back and forth. The text "upZ: Z" is written in black beneath it. Z oscillates between -1 and 1.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Style the box.
+ * fill(255);
+ *
+ * // Draw the box.
+ * box();
+ *
+ * // Style the text.
+ * textAlign(CENTER);
+ * textSize(16);
+ * textFont(font);
+ * fill(0);
+ *
+ * // Calculate the z-component.
+ * let z = sin(frameCount * 0.01);
+ *
+ * // Update the camera's "up" vector.
+ * cam.camera(100, -400, 800, 0, 0, 0, 0, 1, z);
+ *
+ * // Display the value of upZ, rounded to the nearest tenth.
+ * text(`upZ: ${round(cam.upZ, 1)}`, 0, 45);
+ * }
+ */
+ }
+
+ if(typeof p5 !== 'undefined'){
+ camera(p5, p5.prototype);
+ }
+
+ var libtess_min = {exports: {}};
+
+ /*
+
+ Copyright 2000, Silicon Graphics, Inc. All Rights Reserved.
+ Copyright 2015, Google Inc. All Rights Reserved.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice including the dates of first publication and
+ either this permission notice or a reference to http://oss.sgi.com/projects/FreeB/
+ shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ SILICON GRAPHICS, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ Original Code. The Original Code is: OpenGL Sample Implementation,
+ Version 1.2.1, released January 26, 2000, developed by Silicon Graphics,
+ Inc. The Original Code is Copyright (c) 1991-2000 Silicon Graphics, Inc.
+ Copyright in any portions created by third parties is as indicated
+ elsewhere herein. All Rights Reserved.
+ */
+
+ (function (module) {
+ var n;function t(a,b){return a.b===b.b&&a.a===b.a}function u(a,b){return a.btextDirection() function allows you to specify the direction in which text is
+ * rendered on the canvas. When provided with a direction parameter (such as "ltr" for
+ * left-to-right, "rtl" for right-to-left, or "inherit"), it updates the renderer's state with that
+ * value and applies the new setting. When called without any arguments, it returns the current text
+ * direction. This function is particularly useful for rendering text in languages with different
+ * writing directions.
+ *
+ * @method textDirection
+ * @for p5
+ *
+ * @param {String} direction - The text direction to set ("ltr", "rtl", or "inherit").
+ * @returns {String} If no arguments are provided, the current text direction, either "ltr", "rtl", or "inherit"
+ *
+ * @example
+ * function setup() {
+ * createCanvas(300, 300);
+ * background(240);
+ *
+ * textSize(32);
+ * textFont("Georgia");
+ * textAlign(LEFT, TOP);
+ *
+ * // Set text direction to right-to-left and draw Arabic text.
+ * textDirection("rtl");
+ * fill(0);
+ * text("مرحبًا!", 50, 50);
+ *
+ * // Set text direction to left-to-right and draw English text.
+ * textDirection("ltr");
+ * text("Hello, p5.js!", 50, 150);
+ *
+ * // Display the current text direction.
+ * textSize(16);
+ * fill(50);
+ * textAlign(LEFT, TOP);
+ * text("Current textDirection: " + textDirection(), 50, 250);
+ * }
+ */
+ /**
+ * @method textDirection
+ * @for p5
+ * @returns {String} The current text direction, either "ltr", "rtl", or "inherit"
+ */
+
+ /**
+ * Sets or gets a single text property for the renderer.
+ *
+ * The `textProperty()` function allows you to set or retrieve a single text-related property,
+ * such as `textAlign`, `textBaseline`, `fontStyle`, or any other property
+ * that may be part of the renderer's state, its drawing context, or the canvas style.
+ *
+ * When called with a `prop` and a `value`, the function sets the property by checking
+ * for its existence in the renderer's state, the drawing context, or the canvas style. If the property is
+ * successfully modified, the function applies the updated text properties. If called with only the
+ * `prop` parameter, the function returns the current value of that property.
+ *
+ * @method textProperty
+ * @for p5
+ *
+ * @param {String} prop - The name of the text property to set or get.
+ * @param value - The value to set for the specified text property. If omitted, the current
+ * value of the property is returned
+ * @returns If no arguments are provided, the current value of the specified text property
+ *
+ * @example
+ * function setup() {
+ * createCanvas(300, 300);
+ * background(240);
+ *
+ * // Set the text alignment to CENTER and the baseline to TOP using textProperty.
+ * textProperty("textAlign", CENTER);
+ * textProperty("textBaseline", TOP);
+ *
+ * // Set additional text properties and draw the text.
+ * textSize(32);
+ * textFont("Georgia");
+ * fill(0);
+ * text("Hello, World!", width / 2, 50);
+ *
+ * // Retrieve and display the current text properties.
+ * let currentAlign = textProperty("textAlign");
+ * let currentBaseline = textProperty("textBaseline");
+ *
+ * textSize(16);
+ * textAlign(LEFT, TOP);
+ * fill(50);
+ * text("Current textAlign: " + currentAlign, 50, 150);
+ * text("Current textBaseline: " + currentBaseline, 50, 170);
+ * }
+ */
+ /**
+ * @method textProperty
+ * @for p5
+ * @param {String} prop - The name of the text property to set or get.
+ * @returns The current value of the specified text property
+ */
+
+ /**
+ * Gets or sets text properties in batch, similar to calling `textProperty()`
+ * multiple times.
+ *
+ * If an object is passed in, `textProperty(key, value)` will be called for you
+ * on every key/value pair in the object.
+ *
+ * If no arguments are passed in, an object will be returned with all the current
+ * properties.
+ *
+ * @method textProperties
+ * @for p5
+ * @param {Object} properties An object whose keys are properties to set, and whose
+ * values are what they should be set to.
+ */
+ /**
+ * @method textProperties
+ * @for p5
+ * @returns {Object} An object with all the possible properties and their current values.
+ */
+
+ /**
+ * Computes a generic (non-tight) bounding box for a block of text.
+ *
+ * The `fontBounds()` function calculates the bounding box for the text based on the
+ * font's intrinsic metrics (such as `fontBoundingBoxAscent` and
+ * `fontBoundingBoxDescent`). Unlike `textBounds()`, which measures the exact
+ * pixel boundaries of the rendered text, `fontBounds()` provides a looser measurement
+ * derived from the font’s default spacing. This measurement is useful for layout purposes where
+ * a consistent approximation of the text's dimensions is desired.
+ *
+ * @method fontBounds
+ * @for p5
+ *
+ * @param {String} str - The text string to measure.
+ * @param {Number} x - The x-coordinate where the text is drawn.
+ * @param {Number} y - The y-coordinate where the text is drawn.
+ * @param {Number} [width] - (Optional) The maximum width available for the text block.
+ * When specified, the text may be wrapped to fit within this width.
+ * @param {Number} [height] - (Optional) The maximum height available for the text block.
+ * Any lines exceeding this height will be truncated.
+ * @returns {Object} An object with properties `x`, `y`, `w`, and `h` representing the loose
+ * bounding box of the text based on the font's intrinsic metrics.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(300, 200);
+ * background(240);
+ *
+ * textSize(32);
+ * textAlign(LEFT, TOP);
+ * textFont('Georgia');
+ *
+ * let txt = "Hello, World!";
+ * // Compute the bounding box based on the font's intrinsic metrics
+ * let bounds = fontBounds(txt, 50, 50);
+ *
+ * fill(0);
+ * text(txt, 50, 50);
+ *
+ * noFill();
+ * stroke('green');
+ * rect(bounds.x, bounds.y, bounds.w, bounds.h);
+ *
+ * noStroke();
+ * fill(50);
+ * textSize(15);
+ * text("Font Bounds: x=" + bounds.x.toFixed(1) + ", y=" + bounds.y.toFixed(1) +
+ * ", w=" + bounds.w.toFixed(1) + ", h=" + bounds.h.toFixed(1), 8, 100);
+ * }
+ */
+
+ /**
+ * Returns the loose width of a text string based on the current font.
+ *
+ * The `fontWidth()` function measures the width of the provided text string using
+ * the font's default measurement (i.e., the width property from the text metrics returned by
+ * the browser). Unlike `textWidth()`, which calculates the tight pixel boundaries
+ * of the text glyphs, `fontWidth()` uses the font's intrinsic spacing, which may include
+ * additional space for character spacing and kerning. This makes it useful for scenarios where
+ * an approximate width is sufficient for layout and positioning.
+ *
+ * @method fontWidth
+ * @for p5
+ *
+ * @param {String} theText - The text string to measure.
+ * @returns {Number} The loose width of the text in pixels.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(300, 200);
+ * background(240);
+ *
+ * textSize(32);
+ * textAlign(LEFT, TOP);
+ * textFont('Georgia');
+ *
+ * let s = "Hello, World!";
+ * let fw = fontWidth(s);
+ *
+ * fill(0);
+ * text(s, 50, 50);
+ *
+ * stroke('blue');
+ * line(50, 90, 50 + fw, 90);
+ *
+ * noStroke();
+ * fill(50);
+ * textSize(16);
+ * text("Font width: " + fw.toFixed(2) + " pixels", 50, 100);
+ * }
+ */
+
+ /**
+ * Returns the loose ascent of the text based on the font's intrinsic metrics.
+ *
+ * The `fontAscent()` function calculates the ascent of the text using the font's
+ * intrinsic metrics (e.g., `fontBoundingBoxAscent`). This value represents the space
+ * above the baseline that the font inherently occupies, and is useful for layout purposes when
+ * an approximate vertical measurement is required.
+ *
+ * @method fontAscent
+ * @for p5
+ *
+ * @returns {Number} The loose ascent value in pixels.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(300, 300);
+ * background(220);
+ *
+ * textSize(35);
+ * textAlign(LEFT, BASELINE);
+ * textFont('Georgia');
+ *
+ * let s = "Hello, p5.js!";
+ * let x = 50, y = 150;
+ *
+ * fill(0);
+ * text(s, x, y);
+ *
+ * // Get the font descent of the current font
+ * let fasc = fontAscent();
+ *
+ * // Draw a red line at the baseline and a blue line at the ascent position
+ * stroke('red');
+ * line(x, y, x + 200, y); // Baseline
+ * stroke('blue');
+ * line(x, y - fasc, x + 200, y - fasc); // Font ascent position
+ *
+ * noStroke();
+ * fill(0);
+ * textSize(16);
+ * text("fontAscent: " + fasc.toFixed(2) + " pixels", x, y + fdesc + 20);
+ * }
+ */
+
+ /**
+ * Returns the loose descent of the text based on the font's intrinsic metrics.
+ *
+ * The `fontDescent()` function calculates the descent of the text using the font's
+ * intrinsic metrics (e.g., `fontBoundingBoxDescent`). This value represents the space
+ * below the baseline that the font inherently occupies, and is useful for layout purposes when
+ * an approximate vertical measurement is required.
+ *
+ * @method fontDescent
+ * @for p5
+ *
+ * @returns {Number} The loose descent value in pixels.
+ *
+ * @example
+ * function setup() {
+ * createCanvas(300, 300);
+ * background(220);
+ *
+ * textSize(48);
+ * textAlign(LEFT, BASELINE);
+ * textFont('Georgia');
+ *
+ * let s = "Hello, p5.js!";
+ * let x = 50, y = 150;
+ *
+ * fill(0);
+ * text(s, x, y);
+ *
+ * // Get the font descent of the current font
+ * let fdesc = fontDescent();
+ *
+ * // Draw a red line at the baseline and a blue line at the descent position
+ * stroke('red');
+ * line(x, y, x + 200, y); // Baseline
+ * stroke('blue');
+ * line(x, y + fdesc, x + 200, y + fdesc); // Font descent position
+ *
+ * noStroke();
+ * fill(0);
+ * textSize(16);
+ * text("fontDescent: " + fdesc.toFixed(2) + " pixels", x, y + fdesc + 20);
+ * }
+ */
+
+ /**
+ *
+ * Sets or gets the current font weight.
+ *
+ * The textWeight() function is used to specify the weight (thickness) of the text.
+ * When a numeric value is provided, it sets the font weight to that value and updates the
+ * rendering properties accordingly (including the "font-variation-settings" on the canvas style).
+ * When called without an argument, it returns the current font weight setting.
+ *
+ * @method textWeight
+ * @for p5
+ *
+ * @param {Number} weight - The numeric weight value to set for the text.
+ * @returns {Number} If no arguments are provided, the current font weight
+ *
+ * @example
+ * function setup() {
+ * createCanvas(300, 200);
+ * background(240);
+ *
+ * // Set text alignment, size, and font
+ * textAlign(LEFT, TOP);
+ * textSize(20);
+ * textFont("Georgia");
+ *
+ * // Draw text with a normal weight (lighter appearance)
+ * push();
+ * textWeight(400); // Set font weight to 400
+ * fill(0);
+ * text("Normal", 50, 50);
+ * let normalWeight = textWeight(); // Should return 400
+ * pop();
+ *
+ * // Draw text with a bold weight (heavier appearance)
+ * push();
+ * textWeight(900); // Set font weight to 900
+ * fill(0);
+ * text("Bold", 50, 100);
+ * let boldWeight = textWeight(); // Should return 900
+ * pop();
+ *
+ * // Display the current font weight values on the canvas
+ * textSize(16);
+ * fill(50);
+ * text("Normal Weight: " + normalWeight, 150, 52);
+ * text("Bold Weight: " + boldWeight, 150, 100);
+ * }
+ *
+ * @example
+ * let font;
+ *
+ * async function setup() {
+ * createCanvas(100, 100);
+ * font = await loadFont(
+ * 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(255);
+ * textFont(font);
+ * textAlign(LEFT, TOP);
+ * textSize(35);
+ * textWeight(sin(millis() * 0.002) * 200 + 400);
+ * text('p5*js', 0, 10);
+ * describe('The text p5*js pulsing its weight over time');
+ * }
+ */
+ /**
+ * @method textWeight
+ * @for p5
+ * @returns {Number} The current font weight
+ */
+
+ // attach each text func to p5, delegating to the renderer
+ textFunctions.forEach(func => {
+ fn[func] = function (...args) {
+ if (!(func in Renderer.prototype)) {
+ throw Error(`Renderer2D.prototype.${func} is not defined.`);
+ }
+ return this._renderer[func](...args);
+ };
+ // attach also to p5.Graphics.prototype
+ p5.Graphics.prototype[func] = function (...args) {
+ return this._renderer[func](...args);
+ };
+ });
+
+ const RendererTextProps = {
+ textAlign: { default: fn.LEFT, type: 'Context2d' },
+ textBaseline: { default: fn.BASELINE, type: 'Context2d' },
+ textFont: { default: { family: 'sans-serif' } },
+ textLeading: { default: 15 },
+ textSize: { default: 12 },
+ textWrap: { default: fn.WORD },
+ fontStretch: { default: fn.NORMAL, isShorthand: true }, // font-stretch: { default: normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded }
+ fontWeight: { default: fn.NORMAL, isShorthand: true }, // font-stretch: { default: normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded }
+ lineHeight: { default: fn.NORMAL, isShorthand: true }, // line-height: { default: normal | number | length | percentage }
+ fontVariant: { default: fn.NORMAL, isShorthand: true }, // font-variant: { default: normal | small-caps }
+ fontStyle: { default: fn.NORMAL, isShorthand: true }, // font-style: { default: normal | italic | oblique } [was 'textStyle' in v1]
+ direction: { default: 'inherit' } // direction: { default: inherit | ltr | rtl }
+ };
+
+ // note: font must be first here otherwise it may reset other properties
+ const ContextTextProps = ['font', 'direction', 'fontKerning', 'fontStretch', 'fontVariantCaps', 'letterSpacing', 'textAlign', 'textBaseline', 'textRendering', 'wordSpacing'];
+
+ // shorthand font properties that can be set with context2d.font
+ const ShorthandFontProps = Object.keys(RendererTextProps)
+ .filter(p => RendererTextProps[p].isShorthand);
+
+ // allowable values for font-stretch property for context2d.font
+ const FontStretchKeys = ['ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'];
+
+ let contextQueue, cachedDiv; // lazy
+
+ ////////////////////////////// start API ///////////////////////////////
+
+ Renderer.prototype.text = function (str, x, y, width, height) {
+
+ let setBaseline = this.textDrawingContext().textBaseline; // store baseline
+
+ // adjust {x,y,w,h} properties based on rectMode
+ ({ x, y, width, height } = this._handleRectMode(x, y, width, height));
+
+ // parse the lines according to width, height & linebreaks
+ let lines = this._processLines(str, width, height);
+
+ // add the adjusted positions [x,y] to each line
+ lines = this._positionLines(x, y, width, height, lines);
+
+ // render each line at the adjusted position
+ lines.forEach(line => this._renderText(line.text, line.x, line.y));
+
+ this.textDrawingContext().textBaseline = setBaseline; // restore baseline
+ };
+
+ /**
+ * Computes the precise (tight) bounding box for a block of text
+ * @param {String} str - the text to measure
+ * @param {Number} x - the x-coordinate of the text
+ * @param {Number} y - the y-coordinate of the text
+ * @param {Number} width - the max width of the text block
+ * @param {Number} height - the max height of the text block
+ * @returns - a bounding box object for the text block: {x,y,w,h}
+ * @private
+ */
+ Renderer.prototype.textBounds = function (str, x, y, width, height) {
+ // delegate to _textBoundsSingle for measuring
+ return this._computeBounds(
+ textCoreConstants._TEXT_BOUNDS,
+ str,
+ x, y,
+ width, height
+ ).bounds;
+ };
+
+ /**
+ * Computes a generic (non-tight) bounding box for a block of text
+ * @param {String} str - the text to measure
+ * @param {Number} x - the x-coordinate of the text
+ * @param {Number} y - the y-coordinate of the text
+ * @param {Number} width - the max width of the text block
+ * @param {Number} height - the max height of the text block
+ * @returns - a bounding box object for the text block: {x,y,w,h}
+ * @private
+ */
+ Renderer.prototype.fontBounds = function (str, x, y, width, height) {
+ // delegate to _fontBoundsSingle for measuring
+ return this._computeBounds(
+ textCoreConstants._FONT_BOUNDS,
+ str,
+ x, y,
+ width, height
+ ).bounds;
+ };
+
+ /**
+ * Get the width of a text string in pixels (tight bounds)
+ * @param {String} theText
+ * @returns - the width of the text in pixels
+ * @private
+ */
+ Renderer.prototype.textWidth = function (theText) {
+ let lines = this._processLines(theText);
+ // return the max width of the lines (using tight bounds)
+ return Math.max(...lines.map(l => this._textWidthSingle(l)));
+ };
+
+ /**
+ * Get the width of a text string in pixels (loose bounds)
+ * @param {String} theText
+ * @returns - the width of the text in pixels
+ * @private
+ */
+ Renderer.prototype.fontWidth = function (theText) {
+ // return the max width of the lines (using loose bounds)
+ let lines = this._processLines(theText);
+ return Math.max(...lines.map(l => this._fontWidthSingle(l)));
+ };
+
+ /**
+ * @param {*} txt - optional text to measure, if provided will be
+ * used to compute the ascent, otherwise the font's ascent will be used
+ * @returns - the ascent of the text
+ * @private
+ */
+ Renderer.prototype.textAscent = function (txt = '') {
+ if (!txt.length) return this.fontAscent();
+ return this.textDrawingContext().measureText(txt).actualBoundingBoxAscent;
+ };
+
+ /**
+ * @returns - returns the ascent for the current font
+ * @private
+ */
+ Renderer.prototype.fontAscent = function () {
+ return this.textDrawingContext().measureText('_').fontBoundingBoxAscent;
+ };
+
+ /**
+ * @param {*} txt - optional text to measure, if provided will
+ * be used to compute the descent, otherwise the font's descent will be used
+ * @returns - the descent of the text
+ * @private
+ */
+ Renderer.prototype.textDescent = function (txt = '') {
+ if (!txt.length) return this.fontDescent();
+ return this.textDrawingContext().measureText(txt).actualBoundingBoxDescent;
+ };
+
+ Renderer.prototype.fontDescent = function () {
+ return this.textDrawingContext().measureText('_').fontBoundingBoxDescent;
+ };
+
+
+ // setters/getters for text properties //////////////////////////
+
+ Renderer.prototype.textAlign = function (h, v) {
+
+ // the setter
+ if (typeof h !== 'undefined') {
+ this.states.setValue('textAlign', h);
+ if (typeof v !== 'undefined') {
+ if (v === fn.CENTER) {
+ v = textCoreConstants._CTX_MIDDLE;
+ }
+ this.states.setValue('textBaseline', v);
+ }
+ return this._applyTextProperties();
+ }
+ // the getter
+ return {
+ horizontal: this.states.textAlign,
+ vertical: this.states.textBaseline
+ };
+ };
+
+ Renderer.prototype._currentTextFont = function () {
+ return this.states.textFont.font || this.states.textFont.family;
+ };
+
+ /**
+ * Set the font and [size] and [options] for rendering text
+ * @param {p5.Font | string} font - the font to use for rendering text
+ * @param {Number} size - the size of the text, can be a number or a css-style string
+ * @param {Object} options - additional options for rendering text, see FontProps
+ * @private
+ */
+ Renderer.prototype.textFont = function (font, size, options) {
+
+ if (arguments.length === 0) {
+ return this._currentTextFont();
+ }
+
+ let family = font;
+
+ // do we have a custon loaded font ?
+ if (font instanceof p5.Font) {
+ family = font.face.family;
+ }
+ else if (font.data instanceof Uint8Array) {
+ family = font.name.fontFamily;
+ if (font.name?.fontSubfamily) {
+ family += '-' + font.name.fontSubfamily;
+ }
+ }
+ else if (typeof font === 'string') {
+ // direct set the font-string if it contains size
+ if (typeof size === 'undefined' && /[.0-9]+(%|em|p[xt])/.test(family)) {
+ //console.log('direct set font-string: ', family);
+ ({ family, size } = this._directSetFontString(family));
+ }
+ }
+
+ if (typeof family !== 'string') throw Error('null font in textFont()');
+
+ // handle two-arg case: textFont(font, options)
+ if (arguments.length === 2 && typeof size === 'object') {
+ options = size;
+ size = undefined;
+ }
+
+ // update font properties in this.states
+ this.states.setValue('textFont', { font, family, size });
+
+ // convert/update the size in this.states
+ if (typeof size !== 'undefined') {
+ this._setTextSize(size);
+ }
+
+ // apply any options to this.states
+ if (typeof options === 'object') {
+ this.textProperties(options);
+ }
+
+ return this._applyTextProperties();
+ };
+
+ Renderer.prototype._directSetFontString = function (font, debug = 0) {
+ if (debug) console.log('_directSetFontString"' + font + '"');
+
+ let defaults = ShorthandFontProps.reduce((props, p) => {
+ props[p] = RendererTextProps[p].default;
+ return props;
+ }, {});
+
+ let el = this._cachedDiv(defaults);
+ el.style.font = font;
+ let style = getComputedStyle(el);
+ ShorthandFontProps.forEach(prop => {
+ this.states[prop] = style[prop];
+ if (debug) console.log(' this.states.' + prop + '="' + style[prop] + '"');
+ });
+
+ return { family: style.fontFamily, size: style.fontSize };
+ };
+
+ Renderer.prototype.textLeading = function (leading) {
+ // the setter
+ if (typeof leading === 'number') {
+ this.states.setValue('leadingSet', true);
+ this.states.setValue('textLeading', leading);
+ return this._applyTextProperties();
+ }
+ // the getter
+ return this.states.textLeading;
+ };
+
+ Renderer.prototype.textWeight = function (weight) {
+ // the setter
+ if (typeof weight === 'number') {
+ this.states.setValue('fontWeight', weight);
+ this._applyTextProperties();
+
+ // Safari works without weight set in the canvas style attribute, and actually
+ // has buggy behavior if it is present, using the wrong weight when drawing
+ // multiple times with different weights
+ if (!p5.prototype._isSafari()) {
+ this._setCanvasStyleProperty('font-variation-settings', `"wght" ${weight}`);
+ }
+ return;
+ }
+ // the getter
+ return this.states.fontWeight;
+ };
+
+ /**
+ * @param {*} size - the size of the text, can be a number or a css-style string
+ * @private
+ */
+ Renderer.prototype.textSize = function (size) {
+
+ // the setter
+ if (typeof size !== 'undefined') {
+ this._setTextSize(size);
+ return this._applyTextProperties();
+ }
+ // the getter
+ return this.states.textSize;
+ };
+
+ Renderer.prototype.textStyle = function (style) {
+
+ // the setter
+ if (typeof style !== 'undefined') {
+ this.states.setValue('fontStyle', style);
+ return this._applyTextProperties();
+ }
+ // the getter
+ return this.states.fontStyle;
+ };
+
+ Renderer.prototype.textWrap = function (wrapStyle) {
+
+ if (wrapStyle === fn.WORD || wrapStyle === fn.CHAR) {
+ this.states.setValue('textWrap', wrapStyle);
+ // no need to apply text properties here as not a context property
+ return this._pInst;
+ }
+ return this.states.textWrap;
+ };
+
+ Renderer.prototype.textDirection = function (direction) {
+
+ if (typeof direction !== 'undefined') {
+ this.states.setValue('direction', direction);
+ return this._applyTextProperties();
+ }
+ return this.states.direction;
+ };
+
+ /**
+ * Sets/gets a single text property for the renderer (eg. fontStyle, fontStretch, etc.)
+ * The property to be set can be a mapped or unmapped property on `this.states` or a property
+ * on `this.textDrawingContext()` or on `this.canvas.style`
+ * The property to get can exist in `this.states` or `this.textDrawingContext()` or `this.canvas.style`
+ * @private
+ */
+ Renderer.prototype.textProperty = function (prop, value, opts) {
+
+ let modified = false, debug = opts?.debug || false;
+
+ // getter: return option from this.states or this.textDrawingContext()
+ if (typeof value === 'undefined') {
+ let props = this.textProperties();
+ if (prop in props) return props[prop];
+ throw Error('Unknown text option "' + prop + '"'); // FES?
+ }
+
+ // set the option in this.states if it exists
+ if (prop in this.states && this.states[prop] !== value) {
+ this.states[prop] = value;
+ modified = true;
+ if (debug) {
+ console.log('this.states.' + prop + '="' + options[prop] + '"');
+ }
+ }
+ // does it exist in CanvasRenderingContext2D ?
+ else if (prop in this.textDrawingContext()) {
+ this._setContextProperty(prop, value, debug);
+ modified = true;
+ }
+ // does it exist in the canvas.style ?
+ else if (prop in this.textCanvas().style) {
+ this._setCanvasStyleProperty(prop, value, debug);
+ modified = true;
+ }
+ else {
+ console.warn('Ignoring unknown text option: "' + prop + '"\n'); // FES?
+ }
+
+ return modified ? this._applyTextProperties() : this._pInst;
+ };
+
+ /**
+ * Batch set/get text properties for the renderer.
+ * The properties can be either on `states` or `drawingContext`
+ * @private
+ */
+ Renderer.prototype.textProperties = function (properties) {
+
+ // setter
+ if (typeof properties !== 'undefined') {
+ Object.keys(properties).forEach(opt => {
+ this.textProperty(opt, properties[opt]);
+ });
+ return this._pInst;
+ }
+
+ // getter: get props from drawingContext
+ let context = this.textDrawingContext();
+ properties = ContextTextProps.reduce((props, p) => {
+ props[p] = context[p];
+ return props;
+ }, {});
+
+ // add renderer props
+ Object.keys(RendererTextProps).forEach(p => {
+ if (RendererTextProps[p]?.type === 'Context2d') {
+ properties[p] = context[p];
+ }
+ else { // a renderer.states property
+ if (p === 'textFont') {
+ // avoid circular ref. inside textFont
+ let current = this._currentTextFont();
+ if (typeof current === 'object' && '_pInst' in current) {
+ current = Object.assign({}, current);
+ delete current._pInst;
+ }
+ properties[p] = current;
+ }
+ else {
+ properties[p] = this.states[p];
+ }
+ }
+ });
+
+ return properties;
+ };
+
+ Renderer.prototype.textMode = function () { /* no-op for processing api */ };
+
+ /////////////////////////////// end API ////////////////////////////////
+
+ Renderer.prototype._currentTextFont = function () {
+ return this.states.textFont.font || this.states.textFont.family;
+ };
+
+ /*
+ Compute the bounds for a block of text based on the specified
+ measure function, either _textBoundsSingle or _fontBoundsSingle
+ * @private
+ */
+ Renderer.prototype._computeBounds = function (
+ type,
+ str,
+ x, y,
+ width, height,
+ opts
+ ) {
+
+ let context = this.textDrawingContext();
+ let setBaseline = context.textBaseline;
+ let { textLeading, textAlign } = this.states;
+
+ // adjust width, height based on current rectMode
+ ({ width, height } = this._rectModeAdjust(x, y, width, height));
+
+ // parse the lines according to the width & linebreaks
+ let lines = this._processLines(str, width, height);
+
+ // get the adjusted positions [x,y] for each line
+ let boxes = lines.map((line, i) => this[type].bind(this)
+ (line, x, y + i * textLeading));
+
+ if (lines.length > 1 && typeof width !== 'undefined') { // fix for #7984
+ // adjust the bounding boxes for horizontal text alignment in 2d
+ // the WebGL mode version does additional alignment adjustments
+ boxes.forEach(bb => bb.x += p5.Renderer2D.prototype._xAlignOffset.call(this, textAlign, width));
+ }
+
+ // adjust the bounding boxes for vertical text alignment in 2d
+ // the WebGL mode version does additional alignment adjustments
+ p5.Renderer2D.prototype._yAlignOffset.call(this, boxes, height || 0); // fix for #7984
+
+ // get the bounds for the text block
+ let bounds = boxes[0];
+ if (lines.length > 1) {
+
+ // get the bounds for the multi-line text block
+ bounds = this._aggregateBounds(boxes);
+
+ // align the multi-line bounds
+ if (!opts?.ignoreRectMode) {
+ this._rectModeAlign(bounds, width || 0, height || 0);
+ }
+ }
+
+ context.textBaseline = setBaseline; // restore baseline
+
+ return { bounds, lines };
+ };
+
+ /*
+ Adjust width, height of bounds based on current rectMode
+ * @private
+ */
+ Renderer.prototype._rectModeAdjust = function (x, y, width, height) {
+
+ if (typeof width !== 'undefined') {
+ switch (this.states.rectMode) {
+ case fn.CENTER:
+ break;
+ case fn.CORNERS:
+ width -= x;
+ height -= y;
+ break;
+ case fn.RADIUS:
+ width *= 2;
+ height *= 2;
+ break;
+ }
+ }
+ return { x, y, width, height };
+ };
+
+ /*
+ Attempts to set a property directly on the canvas.style object
+ * @private
+ */
+ Renderer.prototype._setCanvasStyleProperty = function (opt, val, debug) {
+
+ let value = val.toString(); // ensure its a string
+
+ if (debug) console.log('canvas.style.' + opt + '="' + value + '"');
+
+ // handle variable fonts options
+ if (opt === FontVariationSettings) {
+ this._handleFontVariationSettings(value);
+ }
+
+ // lets try to set it on the canvas style
+ this.textCanvas().style[opt] = value;
+
+ // check if the value was set successfully
+ if (this.textCanvas().style[opt] !== value) ;
+ };
+
+ /*
+ Parses the fontVariationSettings string and sets the font properties, only font-weight
+ working consistently across browsers at present
+ * @private
+ */
+ Renderer.prototype._handleFontVariationSettings = function (
+ value, debug = false
+ ) {
+ // check if the value is a string or an object
+ if (typeof value === 'object') {
+ value = Object.keys(value).map(k => k + ' ' + value[k]).join(', ');
+ }
+ let values = value.split(CommaDelimRe);
+ values.forEach(v => {
+ v = v.replace(/["']/g, ''); // remove quotes
+ let matches = VariableAxesRe.exec(v);
+ //console.log('matches: ', matches);
+ if (matches && matches.length) {
+ let axis = matches[0];
+ // get the value to 3 digits of precision with no trailing zeros
+ let val = parseFloat(parseFloat(v.replace(axis, '').trim()).toFixed(3));
+ switch (axis) {
+ case 'wght':
+ if (debug) console.log('setting font-weight=' + val);
+ // manually set the font-weight via the font string
+ if (this.states.fontWeight !== val) this.textWeight(val);
+ return val;
+ case 'wdth':
+ break;
+ case 'ital':
+ if (debug) console.log('setting font-style=' + (val ? 'italic' : 'normal'));
+ break;
+ case 'slnt':
+ if (debug) console.log('setting font-style=' + (val ? 'oblique' : 'normal'));
+ break;
+ case 'opsz':
+ if (debug) console.log('setting font-optical-size=' + val);
+ break;
+ }
+ }
+ });
+ };
+
+
+
+
+ /*
+ For properties not directly managed by the renderer in this.states
+ we check if it has a mapping to a property in this.states
+ Otherwise, add the property to the context-queue for later application
+ */
+ Renderer.prototype._setContextProperty = function (prop, val, debug = false) {
+
+ // check if the value is actually different, else short-circuit
+ if (this.textDrawingContext()[prop] === val) {
+ return this._pInst;
+ }
+
+ // otherwise, we will set the property directly on the `this.textDrawingContext()`
+ // by adding [property, value] to context-queue for later application
+ (contextQueue ??= []).push([prop, val]);
+
+ if (debug) console.log('queued context2d.' + prop + '="' + val + '"');
+ };
+
+ /*
+ Adjust parameters (x,y,w,h) based on current rectMode
+ */
+ Renderer.prototype._handleRectMode = function (x, y, width, height) {
+
+ let rectMode = this.states.rectMode;
+
+ if (typeof width !== 'undefined') {
+ switch (rectMode) {
+ case fn.RADIUS:
+ width *= 2;
+ x -= width / 2;
+ if (typeof height !== 'undefined') {
+ height *= 2;
+ y -= height / 2;
+ }
+ break;
+ case fn.CENTER:
+ x -= width / 2;
+ if (typeof height !== 'undefined') {
+ y -= height / 2;
+ }
+ break;
+ case fn.CORNERS:
+ width -= x;
+ if (typeof height !== 'undefined') {
+ height -= y;
+ }
+ break;
+ }
+ }
+ return { x, y, width, height };
+ };
+
+ /*
+ Get the computed font-size in pixels for a given size string
+ @param {String} size - the font-size string to compute
+ @returns {number} - the computed font-size in pixels
+ * @private
+ */
+ Renderer.prototype._fontSizePx = function (
+ theSize,
+ { family } = this.states.textFont
+ ) {
+ const isNumString = num => !isNaN(num) && num.trim() !== '';
+
+ // check for a number in a string, eg '12'
+ if (isNumString(theSize)) {
+ return parseFloat(theSize);
+ }
+ let ele = this._cachedDiv({ fontSize: theSize });
+ ele.style.fontSize = theSize;
+ ele.style.fontFamily = family;
+ let fontSizeStr = getComputedStyle(ele).fontSize;
+ let fontSize = parseFloat(fontSizeStr);
+ if (typeof fontSize !== 'number') {
+ throw Error('textSize: invalid font-size');
+ }
+ return fontSize;
+ };
+
+ Renderer.prototype._cachedDiv = function (props) {
+ if (typeof cachedDiv === 'undefined') {
+ let ele = document.createElement('div');
+ ele.ariaHidden = 'true';
+ ele.style.display = 'none';
+ Object.entries(props).forEach(([prop, val]) => {
+ ele.style[prop] = val;
+ });
+ this.textCanvas().appendChild(ele);
+ cachedDiv = ele;
+ }
+ return cachedDiv;
+ };
+
+
+ /*
+ Aggregate the bounding boxes of multiple lines of text
+ @param {Array} bboxes - the bounding boxes to aggregate
+ @returns {object} - the aggregated bounding box
+ * @private
+ */
+ Renderer.prototype._aggregateBounds = function (bboxes) {
+ // loop over the bounding boxes to get the min/max x/y values
+ let minX = Math.min(...bboxes.map(b => b.x));
+ let minY = Math.min(...bboxes.map(b => b.y));
+ let maxY = Math.max(...bboxes.map(b => b.y + b.h));
+ let maxX = Math.max(...bboxes.map(b => b.x + b.w));
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
+ };
+
+ // Renderer.prototype._aggregateBounds = function (tx, ty, bboxes) {
+ // let x = Math.min(...bboxes.map(b => b.x));
+ // let y = Math.min(...bboxes.map(b => b.y));
+ // // the width is the max of the x-offset + the box width
+ // let w = Math.max(...bboxes.map(b => (b.x - tx) + b.w));
+ // let h = bboxes[bboxes.length - 1].y - bboxes[0].y + bboxes[bboxes.length - 1].h;
+
+
+ // return { x, y, w, h };
+ // };
+
+ /*
+ Process the text string to handle line-breaks and text wrapping
+ @param {String} str - the text to process
+ @param {Number} width - the width to wrap the text to
+ @returns {array} - the processed lines of text
+ * @private
+ */
+ Renderer.prototype._processLines = function (str, width, height) {
+
+ if (typeof width !== 'undefined') { // only for text with bounds
+ let drawingContext = this.textDrawingContext();
+ if (drawingContext.textBaseline === fn.BASELINE) {
+ this.drawingContext.textBaseline = fn.TOP;
+ }
+ }
+
+ let lines = this._splitOnBreaks(str.toString());
+ let hasLineBreaks = lines.length > 1;
+ let hasWidth = typeof width !== 'undefined';
+ let exceedsWidth = hasWidth &&
+ lines.some(l => this._textWidthSingle(l) > width);
+ let { textLeading: leading, textWrap } = this.states;
+
+ //if (!hasLineBreaks && !exceedsWidth) return lines; // a single-line
+ if (hasLineBreaks || exceedsWidth) {
+ if (hasWidth) lines = this._lineate(textWrap, lines, width);
+ }
+
+ // handle height truncation
+ if (hasWidth && typeof height !== 'undefined') {
+
+ if (typeof leading === 'undefined') {
+ throw Error('leading is required if height is specified');
+ }
+
+ // truncate lines that exceed the height
+ for (let i = 0; i < lines.length; i++) {
+ let lh = leading * (i + 1);
+ if (lh > height) {
+ //console.log('TRUNCATING: ', i, '-', lines.length, '"' + lines.slice(i) + '"');
+ lines = lines.slice(0, i);
+ break;
+ }
+ }
+ }
+
+ return lines;
+ };
+
+ /*
+ Get the x-offset for text given the width and textAlign property
+ */
+ Renderer.prototype._xAlignOffset = function (textAlign, width) {
+ switch (textAlign) {
+ case fn.LEFT:
+ return 0;
+ case fn.CENTER:
+ return width / 2;
+ case fn.RIGHT:
+ return width;
+ case textCoreConstants.START:
+ return 0;
+ case textCoreConstants.END:
+ throw new Error('textBounds: END not yet supported for textAlign');
+ default:
+ return 0;
+ }
+ };
+
+ /*
+ Align the bounding box based on the current rectMode setting
+ */
+ Renderer.prototype._rectModeAlign = function (bb, width, height) {
+ if (typeof width !== 'undefined') {
+
+ switch (this.states.rectMode) {
+ case fn.CENTER:
+ bb.x -= (width - bb.w) / 2;
+ bb.y -= (height - bb.h) / 2;
+ break;
+ case fn.CORNERS:
+ bb.w += bb.x;
+ bb.h += bb.y;
+ break;
+ case fn.RADIUS:
+ bb.x -= (width - bb.w) / 2;
+ bb.y -= (height - bb.h) / 2;
+ bb.w /= 2;
+ bb.h /= 2;
+ break;
+ }
+ return bb;
+ }
+ };
+
+ Renderer.prototype._rectModeAlignRevert = function (bb, width, height) {
+ if (typeof width !== 'undefined') {
+
+ switch (this.states.rectMode) {
+ case fn.CENTER:
+ bb.x += (width - bb.w) / 2;
+ bb.y += (height - bb.h) / 2;
+ break;
+ case fn.CORNERS:
+ bb.w -= bb.x;
+ bb.h -= bb.y;
+ break;
+ case fn.RADIUS:
+ bb.x += (width - bb.w) / 2;
+ bb.y += (height - bb.h) / 2;
+ bb.w *= 2;
+ bb.h *= 2;
+ break;
+ }
+ return bb;
+ }
+ };
+
+ /*
+ Get the (tight) width of a single line of text
+ */
+ Renderer.prototype._textWidthSingle = function (s) {
+ let metrics = this.textDrawingContext().measureText(s);
+ let abl = metrics.actualBoundingBoxLeft;
+ let abr = metrics.actualBoundingBoxRight;
+ return abr + abl;
+ };
+
+ /*
+ Get the (loose) width of a single line of text as specified by the font
+ */
+ Renderer.prototype._fontWidthSingle = function (s) {
+ return this.textDrawingContext().measureText(s).width;
+ };
+
+ /*
+ Get the (tight) bounds of a single line of text based on its actual bounding box
+ */
+ Renderer.prototype._textBoundsSingle = function (s, x = 0, y = 0) {
+
+ let metrics = this.textDrawingContext().measureText(s);
+ let asc = metrics.actualBoundingBoxAscent;
+ let desc = metrics.actualBoundingBoxDescent;
+ let abl = metrics.actualBoundingBoxLeft;
+ let abr = metrics.actualBoundingBoxRight;
+ return { x: x - abl, y: y - asc, w: abr + abl, h: asc + desc };
+ };
+
+ /*
+ Get the (loose) bounds of a single line of text based on its font's bounding box
+ */
+ Renderer.prototype._fontBoundsSingle = function (s, x = 0, y = 0) {
+
+ let metrics = this.textDrawingContext().measureText(s);
+ let asc = metrics.fontBoundingBoxAscent;
+ let desc = metrics.fontBoundingBoxDescent;
+ x -= this._xAlignOffset(this.states.textAlign, metrics.width);
+ return { x, y: y - asc, w: metrics.width, h: asc + desc }; };
+
+ /*
+ Set the textSize property in `this.states` if it has changed
+ @param {number | string} theSize - the font-size to set
+ @returns {boolean} - true if the size was changed, false otherwise
+ */
+ Renderer.prototype._setTextSize = function (theSize) {
+
+ if (typeof theSize === 'string') {
+ // parse the size string via computed style, eg '2em'
+ theSize = this._fontSizePx(theSize);
+ }
+
+ // should be a number now
+ if (typeof theSize === 'number') {
+
+ // set it in `this.states` if its been changed
+ if (this.states.textSize !== theSize) {
+ this.states.setValue('textSize', theSize);
+
+ // handle leading here, if not set otherwise
+ if (!this.states.leadingSet) {
+ this.states.setValue('textLeading', this.states.textSize * LeadingScale);
+ }
+ return true; // size was changed
+ }
+ }
+ else {
+ console.warn('textSize: invalid size: ' + theSize);
+ }
+
+ return false;
+ };
+
+ /*
+ Split the lines of text based on the width and the textWrap property
+ @param {Array} lines - the lines of text to split
+ @param {Number} maxWidth - the maximum width of the lines
+ @param {Object} opts - additional options for splitting the lines
+ @returns {array} - the split lines of text
+ * @private
+ */
+ Renderer.prototype._lineate = function (
+ textWrap,
+ lines,
+ maxWidth = Infinity,
+ opts = {}
+ ) {
+
+ let splitter = opts.splitChar ?? (textWrap === fn.WORD ? ' ' : '');
+ let line, testLine, testWidth, words, newLines = [];
+
+ for (let lidx = 0; lidx < lines.length; lidx++) {
+ line = '';
+ words = lines[lidx].split(splitter);
+ for (let widx = 0; widx < words.length; widx++) {
+ testLine = `${line + words[widx]}` + splitter;
+ testWidth = this._textWidthSingle(testLine);
+ if (line.length > 0 && testWidth > maxWidth) {
+ newLines.push(line.trim());
+ line = `${words[widx]}` + splitter;
+ } else {
+ line = testLine;
+ }
+ }
+ newLines.push(line.trim());
+ }
+ return newLines;
+ };
+
+ /*
+ Split the text into lines based on line-breaks and tabs
+ */
+ Renderer.prototype._splitOnBreaks = function (s) {
+ if (!s || s.length === 0) return [''];
+ return s.replace(TabsRe, ' ').split(LinebreakRe);
+ };
+
+ /*
+ Parse the font-family string to handle complex names, fallbacks, etc.
+ */
+ Renderer.prototype._parseFontFamily = function (familyStr) {
+
+ let parts = familyStr.split(CommaDelimRe);
+ let family = parts.map(part => {
+ part = part.trim();
+ if ((part.indexOf(' ') > -1 || SpecialCharRe.test(part)) && !QuotedRe.test(part)) {
+ part = `"${part}"`; // quote font names with spaces
+ }
+ return part;
+ }).join(', ');
+
+ return family;
+ };
+
+ Renderer.prototype._applyFontString = function () {
+ /*
+ Create the font-string according to the CSS font-string specification:
+ If font is specified as a shorthand for several font-related properties, then:
+ - it must include values for: + * + * If you want to apply shaders to strokes or images, use the following methods: + * - strokeShader() : Applies a shader to the stroke (outline) of shapes, allowing independent control over the stroke rendering using shaders. + * - imageShader() : Applies a shader to images or textures, controlling how the shader modifies their appearance during rendering. + * + *
+ *for fallback description + this.dummyDOM.querySelector(`#${cnvId}`).innerHTML = html; + } else { + //create description container +
for fallback description before outputs + this.dummyDOM + .querySelector(`#${cnvId}accessibleOutput`) + .insertAdjacentHTML('beforebegin', html); + } + } else { + //if describeElement() has already created the container and added a table of elements + //create fallback description
before the table + this.dummyDOM + .querySelector('#' + cnvId + fallbackTableId) + .insertAdjacentHTML( + 'beforebegin', + `
` + ); + } + //if the container for the description exists + this.descriptions.fallback = this.dummyDOM.querySelector( + `#${cnvId}${fallbackDescId}` + ); + this.descriptions.fallback.innerHTML = text; + return; + } else if (type === 'label') { + //if there is no label container + if (!this.dummyDOM.querySelector(`#${cnvId + labelContainer}`)) { + let html = `for label description + this.dummyDOM + .querySelector('#' + cnvId) + .insertAdjacentHTML('afterend', html); + } else { + //create label container +
for label description before outputs + this.dummyDOM + .querySelector(`#${cnvId}accessibleOutputLabel`) + .insertAdjacentHTML('beforebegin', html); + } + } else if (this.dummyDOM.querySelector(`#${cnvId + labelTableId}`)) { + //if describeElement() has already created the container and added a table of elements + //create label description
before the table + this.dummyDOM + .querySelector(`#${cnvId + labelTableId}`) + .insertAdjacentHTML( + 'beforebegin', + `
` + ); + } + this.descriptions.label = this.dummyDOM.querySelector( + '#' + cnvId + labelDescId + ); + this.descriptions.label.innerHTML = text; + return; + } + }; + + /* + * Helper functions for describeElement(). + */ + + //check that name is not LABEL or FALLBACK and ensure text ends with colon + function _elementName(name) { + if (name === 'label' || name === 'fallback') { + throw new Error('element name should not be LABEL or FALLBACK'); + } + //check if last character of string n is '.', ';', or ',' + if (name.endsWith('.') || name.endsWith(';') || name.endsWith(',')) { + //replace last character with ':' + name = name.replace(/.$/, ':'); + } else if (!name.endsWith(':')) { + //if string n does not end with ':' + //add ':'' at the end of string + name = name + ':'; + } + return name; + } + + //creates HTML structure for element descriptions + fn._describeElementHTML = function(type, name, text) { + const cnvId = this.canvas.id; + if (type === 'fallback') { + //if there is no description container + if (!this.dummyDOM.querySelector(`#${cnvId + descContainer}`)) { + //if there are no accessible outputs (see textOutput() and gridOutput()) + let html = `
+ * // Example 1: A soft vertical fade using smoothstep (no uniforms)
+ *
+ * let fadeShader;
+ *
+ * function fadeCallback() {
+ * getColor((inputs) => {
+ * // x goes from 0 → 1 across the canvas
+ * let x = inputs.texCoord.x;
+ *
+ * // smoothstep creates a soft transition instead of a hard edge
+ * let t = smoothstep(0.25, 0.35, x);
+ *
+ * // Use t directly as brightness
+ * return [t, t, t, 1];
+ * });
+ * }
+ *
+ * function setup() {
+ * createCanvas(300, 200, WEBGL);
+ * fadeShader = baseFilterShader().modify(fadeCallback);
+ * }
+ *
+ * function draw() {
+ * background(0);
+ * filter(fadeShader);
+ * }
+ *
+ *
+ * // Example 2: Animate the smooth transition using a uniform
+ *
+ * let animatedShader;
+ *
+ * function animatedFadeCallback() {
+ * const time = uniformFloat(() => millis() * 0.001);
+ *
+ * getColor((inputs) => {
+ * let x = inputs.texCoord.x;
+ *
+ * // Move the smoothstep band back and forth over time
+ * let center = 0.5 + 0.25 * sin(time);
+ * let t = smoothstep(center - 0.05, center + 0.05, x);
+ *
+ * return [t, t, t, 1];
+ * });
+ * }
+ *
+ * function setup() {
+ * createCanvas(300, 200, WEBGL);
+ * animatedShader = baseFilterShader().modify(animatedFadeCallback);
+ * }
+ *
+ * function draw() {
+ * background(0);
+ * filter(animatedShader);
+ * }
+ *
+ *
+ * // A filter shader (using p5.strands) which will
+ * // sample and invert the color of each pixel
+ * // from the canvas.
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ * let myShader = buildFilterShader(buildIt);
+ *
+ * background("white");
+ * fill("red");
+ * circle(0, 0, 50);
+ *
+ * filter(myShader); //Try commenting this out!
+ *
+ * describe("A cyan circle on black background");
+ * }
+ *
+ * function buildIt() {
+ * filterColor.begin();
+ *
+ * //Sample the color of the pixel from the
+ * //canvas at the same coordinate.
+ * let c = getTexture(filterColor.canvasContent,
+ * filterColor.texCoord);
+ *
+ * //Make a new color by inverting r, g, and b
+ * let newColor = [1 - c.r, 1 - c.g, 1 - c.b, c.a];
+ *
+ * //Finally, use it for this pixel!
+ * filterColor.set(newColor);
+ *
+ * filterColor.end();
+ * }
+ *
+ *
+ *
+ * @example
+ *
+ * // This primitive edge-detection filter samples
+ * // and compares the colors of the current pixel
+ * // on the canvas, and a little to the right.
+ * // It marks if they differ much.
+ * let myShader;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ * myShader = buildFilterShader(myShaderBuilder);
+ * describe("A rough partial outline of a square rotating around a circle");
+ * }
+ *
+ * function draw() {
+ * drawADesign();
+ *
+ * filter(myShader); // try commenting this out
+ * }
+ *
+ * function myShaderBuilder() {
+ * filterColor.begin();
+ *
+ * //The position of the current pixel...
+ * let coordHere = filterColor.texCoord;
+ * //and some small amount to the right.
+ * let coordRight = coordHere + [0.01, 0];
+ *
+ * //The canvas content is a texture.
+ * let cnvTex = filterColor.canvasContent;
+ *
+ * //Sample the colors from it at our two positions
+ * let colorHere = getTexture(cnvTex, coordHere);
+ * let colorRight = getTexture(cnvTex, coordRight);
+ *
+ * // Calculate a (very rough) color difference.
+ * let difference = length(colorHere - colorRight);
+ *
+ * //We'll use a black color by default...
+ * let resultColor = [0, 0, 0, 1];
+ * //or white if the samples were different.
+ * if (difference > 0.3) {
+ * resultColor = [1, 1, 1, 1];
+ * }
+ * filterColor.set(resultColor);
+ *
+ * filterColor.end();
+ * }
+ *
+ * //Draw a few shapes, just to test the filter with
+ * function drawADesign() {
+ * background(50);
+ * noStroke();
+ * lights();
+ * sphere(20);
+ * rotate(frameCount / 300);
+ * square(0, 0, 30);
+ * }
+ *
+ *